If a WebFilter
mutates an inbound request, then the tomcat/jetty/undertow websocket request upgrade strategies throw a ClassCastException
, which results in an HTTP 500 error being returned for the websocket upgrade request.
For example, consider the following simple filter that mutates a request...
@Bean
WebFilter webFilter() {
return (exchange, chain) -> chain.filter(exchange.mutate()
// Any mutation of the request will cause an exception during the websocket handshake
.request(exchange.getRequest().mutate()
.headers(headers -> headers.set("ExampleHeader", "ExampleHeaderValue"))
.build())
.build());
}
And this simple websocket handler...
@Bean
HandlerMapping handlerMapping() {
return new SimpleUrlHandlerMapping(
Map.of("/ws", (WebSocketHandler) (session -> Mono.empty())),
-1);
}
When an request is made to the websocket endpoint, the websocket upgrade request will fail. The server side receives this exception:
2020-12-20 12:21:49.203 ERROR 12488 --- [o-auto-1-exec-1] a.w.r.e.AbstractErrorWebExceptionHandler : [6a9e2e7a] 500 Server Error for HTTP GET "/ws"
java.lang.ClassCastException: class org.springframework.http.server.reactive.TomcatHttpHandlerAdapter$TomcatServerHttpRequest cannot be cast to class javax.servlet.http.HttpServletRequest (org.springframework.http.server.reactive.TomcatHttpHandlerAdapter$TomcatServerHttpRequest and javax.servlet.http.HttpServletRequest are in unnamed module of loader 'app')
at org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy.getNativeRequest(TomcatRequestUpgradeStrategy.java:159) ~[spring-webflux-5.3.2.jar:5.3.2]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ com.example.mutatedwebsocketupgradefailure.MutatedWebsocketUpgradeFailureApplication$$Lambda$747/0x0000000800436840 [DefaultWebFilterChain]
|_ checkpoint ⇢ HTTP GET "/ws" [ExceptionHandlingWebHandler]
Stack trace:
at org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy.getNativeRequest(TomcatRequestUpgradeStrategy.java:159) ~[spring-webflux-5.3.2.jar:5.3.2]
at org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy.upgrade(TomcatRequestUpgradeStrategy.java:134) ~[spring-webflux-5.3.2.jar:5.3.2]
at org.springframework.web.reactive.socket.server.support.HandshakeWebSocketService.lambda$handleRequest$1(HandshakeWebSocketService.java:235) ~[spring-webflux-5.3.2.jar:5.3.2]
The problem is that *RequestUpgradeStrategy.getNativeRequest
incorrectly assumes that ((AbstractServerHttpRequest) request).getNativeRequest()
returns an HttpServletRequest
. For mutated requests, the value returned will be the ServerHttpRequest
before mutation, not a HttpServletRequest
.
One naïve solution is to make the *RequestUpgradeStrategy.getNativeRequest
methods recursively search through the mutated requests to find the real underlying native request....
private static HttpServletRequest getNativeRequest(ServerHttpRequest request) {
if (request instanceof AbstractServerHttpRequest) {
Object nativeRequest = ((AbstractServerHttpRequest) request).getNativeRequest();
if (nativeRequest instanceof HttpServletRequest) {
return (HttpServletRequest) nativeRequest;
} else if (nativeRequest instanceof ServerHttpRequest) {
return getNativeRequest((ServerHttpRequest) nativeRequest);
}
else {
throw new IllegalArgumentException(
"Couldn't find HttpServletRequest in " + request.getClass().getName());
}
}
else if (request instanceof ServerHttpRequestDecorator) {
return getNativeRequest(((ServerHttpRequestDecorator) request).getDelegate());
}
else {
throw new IllegalArgumentException(
"Couldn't find HttpServletRequest in " + request.getClass().getName());
}
}
I only tested on tomcat, but from looking at the code, it probably affects tomcat, jetty, and undertow. Also a similar recursive search might be needed for getNativeResponse
(not sure?)
Here is an example application and unit test that demonstrates the problem: mutated-websocket-upgrade-failure.zip
Comment From: rstoyanchev
This was a bug in DefaultServerHttpRequestBuilder
to return the original request rather than its native request.
Comment From: philsttr
Thanks @rstoyanchev ! You rock!