Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NPE when using pathExtension predicate for routes that have no file extensions #32404

Closed
steve-todorov opened this issue Mar 9, 2024 · 2 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Milestone

Comments

@steve-todorov
Copy link

steve-todorov commented Mar 9, 2024

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 : "");

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 9, 2024
@sdeleuze sdeleuze self-assigned this Mar 9, 2024
@sdeleuze sdeleuze added in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Mar 9, 2024
@sdeleuze sdeleuze added this to the 6.1.5 milestone Mar 11, 2024
@sdeleuze sdeleuze added type: enhancement A general enhancement and removed type: bug A general bug labels Mar 12, 2024
@sdeleuze sdeleuze modified the milestones: 6.1.5, 6.2.0-M1 Mar 12, 2024
@sdeleuze
Copy link
Contributor

I will implement the proposed behavior in 6.2 as this is a breaking change, and will fix the code sample in Spring Framework documentation via #32423. With 6.1, you should be able to fix this by using Arrays.asList instead of List.of.

@steve-todorov
Copy link
Author

Thanks for the quick fix! Looking forward to the released fix. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

3 participants