Closed
Description
Affects: Spring Boot 3.2.3
I have the following code which was taken from the Spring Boot docs:
@Bean
RouterFunction<ServerResponse> spaRouter(@Value("classpath:/static/index.html") Resource homepageHtml)
{
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
return route().resource(spaPredicate, homepageHtml).build();
}
This should theoretically be serving the index.html
page for any URL that does not match the /api
, /error
or a list of extensions.
However, there is a problem with the RequestPredicates#test
method:
@Override
public boolean test(ServerRequest request) {
String pathExtension = UriUtils.extractFileExtension(request.path());
return this.extensionPredicate.test(pathExtension);
}
The assumption seems to be that pathExtension
would always contain some value.
Unfortunately this is not true. We are using an Angular frontend which has it's own routing system.
The URL coming from the there is http://localhost:8080/app/users
and obviously fails with a NPE, because the pathExtension
is null
.
Click here to see the full log
15:52:19.021 09-03-2024 | DEBUG | reactor-http-epoll-5 | o.s.web.server.adapter.HttpWebHandlerAdapter | [9b716a53-23] HTTP GET "/app/users"
15:52:19.022 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RequestPredicates | Method "GET" matches against value "GET"
15:52:19.022 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RequestPredicates | Pattern "/" does not match against value "/app/users"
15:52:19.022 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RequestPredicates | Pattern "/api/**" does not match against value "/app/users"
15:52:19.022 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RequestPredicates | Pattern "/error" does not match against value "/app/users"
15:52:19.022 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RouterFunctions | [9b716a53-23] Matched org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler$$Lambda/0x00007957a0c82c08@14996466
15:52:19.023 09-03-2024 | DEBUG | reactor-http-epoll-5 | o.s.b.a.w.r.error.AbstractErrorWebExceptionHandler | [9b716a53-23] Resolved [NullPointerException: null] for HTTP GET /app/users
15:52:19.023 09-03-2024 | ERROR | reactor-http-epoll-5 | o.s.b.a.w.r.error.AbstractErrorWebExceptionHandler | [9b716a53-23] 500 Server Error for HTTP GET "/app/users"
java.lang.NullPointerException: null
at java.base/java.util.ImmutableCollections$ListN.indexOf(ImmutableCollections.java:723)
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Assembly trace from producer [reactor.core.publisher.FluxConcatMapNoPrefetch] :
reactor.core.publisher.Flux.concatMap(Flux.java:4042)
org.springframework.web.reactive.function.server.RouterFunctionBuilder$BuiltRouterFunction.route(RouterFunctionBuilder.java:427)
Error has been observed at the following site(s):
*______Flux.concatMap ⇢ at org.springframework.web.reactive.function.server.RouterFunctionBuilder$BuiltRouterFunction.route(RouterFunctionBuilder.java:427)
|_ Flux.next ⇢ at org.springframework.web.reactive.function.server.RouterFunctionBuilder$BuiltRouterFunction.route(RouterFunctionBuilder.java:428)
*__________Mono.defer ⇢ at org.springframework.web.reactive.function.server.RouterFunctions$DifferentComposedRouterFunction.route(RouterFunctions.java:1153)
*_________Flux.concat ⇢ at org.springframework.web.reactive.function.server.RouterFunctions$DifferentComposedRouterFunction.route(RouterFunctions.java:1153)
|_ Flux.next ⇢ at org.springframework.web.reactive.function.server.RouterFunctions$DifferentComposedRouterFunction.route(RouterFunctions.java:1154)
|_ Mono.map ⇢ at org.springframework.web.reactive.function.server.RouterFunctions$DifferentComposedRouterFunction.route(RouterFunctions.java:1155)
|_ Mono.doOnNext ⇢ at org.springframework.web.reactive.function.server.support.RouterFunctionMapping.getHandlerInternal(RouterFunctionMapping.java:157)
|_ Mono.map ⇢ at org.springframework.web.reactive.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:187)
*______Flux.concatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:150)
|_ Flux.next ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:151)
|_ Mono.switchIfEmpty ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:152)
|_ Mono.onErrorResume ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:153)
|_ Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handle(DispatcherHandler.java:154)
*__________Mono.error ⇢ at org.springframework.web.reactive.DispatcherHandler.lambda$handle$1(DispatcherHandler.java:153)
|_ Mono.onErrorResume ⇢ at org.springframework.web.reactive.DispatcherHandler.handleResultMono(DispatcherHandler.java:168)
|_ Mono.flatMap ⇢ at org.springframework.web.reactive.DispatcherHandler.handleResultMono(DispatcherHandler.java:172)
*__________Mono.error ⇢ at org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.handleException(RequestMappingHandlerAdapter.java:322)
*__________Mono.defer ⇢ at org.springframework.web.server.handler.DefaultWebFilterChain.filter(DefaultWebFilterChain.java:106)
|_ Mono.doOnError ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:84)
|_ Mono.onErrorResume ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:85)
|_ Mono.doOnError ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler.handle(ExceptionHandlingWebHandler.java:84)
*__________Mono.error ⇢ at org.springframework.web.server.handler.ExceptionHandlingWebHandler$CheckpointInsertingHandler.handle(ExceptionHandlingWebHandler.java:106)
|_ checkpoint ⇢ HTTP GET "/app/users" [ExceptionHandlingWebHandler]
Original Stack Trace:
at java.base/java.util.ImmutableCollections$ListN.indexOf(ImmutableCollections.java:723)
at java.base/java.util.ImmutableCollections$AbstractImmutableList.contains(ImmutableCollections.java:331)
at org.springframework.web.reactive.function.server.RequestPredicates$PathExtensionPredicate.test(RequestPredicates.java:868)
at org.springframework.web.reactive.function.server.RequestPredicates$RequestModifyingPredicate$1.testInternal(RequestPredicates.java:477)
at org.springframework.web.reactive.function.server.RequestPredicates$OrRequestPredicate.testInternal(RequestPredicates.java:1098)
at org.springframework.web.reactive.function.server.RequestPredicates$NegateRequestPredicate.testInternal(RequestPredicates.java:1041)
at org.springframework.web.reactive.function.server.RequestPredicates$RequestModifyingPredicate.test(RequestPredicates.java:486)
at org.springframework.web.reactive.function.server.PredicateResourceLookupFunction.apply(PredicateResourceLookupFunction.java:48)
at org.springframework.web.reactive.function.server.PredicateResourceLookupFunction.apply(PredicateResourceLookupFunction.java:33)
at org.springframework.web.reactive.function.server.RouterFunctions$ResourcesRouterFunction.route(RouterFunctions.java:1304)
at org.springframework.web.reactive.function.server.RouterFunctionBuilder$BuiltRouterFunction.lambda$route$0(RouterFunctionBuilder.java:427)
at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onNext(FluxConcatMapNoPrefetch.java:183)
at reactor.core.publisher.FluxIterable$IterableSubscription.slowPath(FluxIterable.java:335)
at reactor.core.publisher.FluxIterable$IterableSubscription.request(FluxIterable.java:294)
at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.request(FluxConcatMapNoPrefetch.java:337)
at reactor.core.publisher.MonoNext$NextSubscriber.request(MonoNext.java:108)
at reactor.core.publisher.FluxConcatArray$ConcatArraySubscriber.request(FluxConcatArray.java:278)
at reactor.core.publisher.MonoNext$NextSubscriber.request(MonoNext.java:108)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171)
at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:144)
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.request(FluxMapFuseable.java:171)
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.request(Operators.java:2331)
at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.request(FluxConcatMapNoPrefetch.java:339)
at reactor.core.publisher.MonoNext$NextSubscriber.request(MonoNext.java:108)
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.set(Operators.java:2367)
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onSubscribe(Operators.java:2241)
at reactor.core.publisher.MonoNext$NextSubscriber.onSubscribe(MonoNext.java:70)
at reactor.core.publisher.FluxConcatMapNoPrefetch$FluxConcatMapNoPrefetchSubscriber.onSubscribe(FluxConcatMapNoPrefetch.java:164)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:201)
at reactor.core.publisher.FluxIterable.subscribe(FluxIterable.java:83)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
at reactor.core.publisher.MonoDefer.subscribe(MonoDefer.java:53)
at reactor.core.publisher.Mono.subscribe(Mono.java:4563)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:265)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
at reactor.core.publisher.Mono.subscribe(Mono.java:4563)
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:265)
at reactor.core.publisher.MonoIgnoreThen.subscribe(MonoIgnoreThen.java:51)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
at reactor.core.publisher.MonoDeferContextual.subscribe(MonoDeferContextual.java:55)
at reactor.core.publisher.InternalMonoOperator.subscribe(InternalMonoOperator.java:76)
at reactor.netty.http.server.HttpServer$HttpServerHandle.onStateChange(HttpServer.java:1169)
at reactor.netty.ReactorNetty$CompositeConnectionObserver.onStateChange(ReactorNetty.java:710)
at reactor.netty.transport.ServerTransport$ChildObserver.onStateChange(ServerTransport.java:481)
at reactor.netty.http.server.HttpServerOperations.onInboundNext(HttpServerOperations.java:652)
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:114)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at reactor.netty.http.server.HttpTrafficHandler.channelRead(HttpTrafficHandler.java:238)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436)
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:346)
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:318)
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412)
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440)
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420)
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:800)
at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:509)
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:407)
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997)
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
at java.base/java.lang.Thread.run(Thread.java:1583)
15:52:19.024 09-03-2024 | DEBUG | reactor-http-epoll-5 | o.springframework.core.codec.CharSequenceEncoder | [9b716a53-23] Writing "<html><body><h1>Whitelabel Error Page</h1><p>This application has no configured error view, so you a (truncated)..."
15:52:19.024 09-03-2024 | DEBUG | reactor-http-epoll-5 | o.s.web.server.adapter.HttpWebHandlerAdapter | [9b716a53-23] Completed 500 INTERNAL_SERVER_ERROR
15:52:19.100 09-03-2024 | DEBUG | reactor-http-epoll-5 | o.s.web.server.adapter.HttpWebHandlerAdapter | [9b716a53-24] HTTP GET "/favicon.ico"
15:52:19.100 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RequestPredicates | Method "GET" matches against value "GET"
15:52:19.100 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RequestPredicates | Pattern "/" does not match against value "/favicon.ico"
15:52:19.100 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RequestPredicates | Pattern "/api/**" does not match against value "/favicon.ico"
15:52:19.100 09-03-2024 | TRACE | reactor-http-epoll-5 | o.s.web.reactive.function.server.RequestPredicates | Pattern "/error" does not match against value "/favicon.ico"
15:52:19.100 09-03-2024 | DEBUG | reactor-http-epoll-5 | o.s.web.reactive.handler.SimpleUrlHandlerMapping | [9b716a53-24] Mapped to ResourceWebHandler [classpath [static/]]
15:52:19.101 09-03-2024 | DEBUG | reactor-http-epoll-5 | o.s.http.codec.ResourceHttpMessageWriter | [9b716a53-24] Zero-copy [class path resource [static/favicon.ico]]
15:52:19.101 09-03-2024 | DEBUG | reactor-http-epoll-5 | o.s.web.server.adapter.HttpWebHandlerAdapter | [9b716a53-24] Completed 200 OK
The fix would be to verify if pathExtension
is not null before calling this.extensionPredicate.test()
.
Maybe something like this:
String pathExtension = UriUtils.extractFileExtension(request.path());
if(pathExtension == null) {
return false;
}
return this.extensionPredicate.test(pathExtension);
// OR
String pathExtension = UriUtils.extractFileExtension(request.path());
return this.extensionPredicate.test(pathExtension != null ? pathExtension : "");