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!