skip to content

How do you expose a reactive WebSocketHandler at a URL in Spring WebFlux — what beans (SimpleUrlHandlerMapping, WebSocketHandlerAdapter, HandshakeWebSocketService) are involved and what does each do?

level: seniorimportance: must knowfreq 50%

answer

  1. SimpleUrlHandlerMapping: path -> WebSocketHandler, set low order
  2. WebSocketHandlerAdapter: HandlerAdapter that supports WebSocketHandler
  3. HandshakeWebSocketService = default WebSocketService, does upgrade
  4. RequestUpgradeStrategy per server (Netty/Tomcat/Jetty/Undertow)
  5. Both beans required; NOT @EnableWebSocket (that's Servlet)

basics

~20 s

Register a SimpleUrlHandlerMapping bean that maps URL paths to your WebSocketHandler instances (with a high order so it wins over annotated controllers), plus a WebSocketHandlerAdapter bean. The adapter uses a WebSocketService — by default HandshakeWebSocketService — to perform the HTTP-to-WebSocket upgrade handshake.

solid answer

~40 s

WebFlux routes requests through DispatcherHandler, which asks its HandlerMappings for a handler and then a HandlerAdapter that supports it. For raw WebSockets you provide two beans. First, a SimpleUrlHandlerMapping whose urlMap maps path patterns (e.g. "/ws/echo") to WebSocketHandler beans; give it a low order value (e.g. -1) so it is consulted before RequestMappingHandlerMapping, otherwise an @Controller could shadow the path. Second, a WebSocketHandlerAdapter, which is the HandlerAdapter that recognizes WebSocketHandler and delegates to a WebSocketService. Its default WebSocketService is HandshakeWebSocketService, which validates the upgrade request (Upgrade/Connection headers, WebSocket version, key) and delegates the actual protocol upgrade to a server-specific RequestUpgradeStrategy (Reactor Netty, Tomcat, Jetty, Undertow). After a successful upgrade it invokes your handler's handle(session).

code

java · 25 lines
java
import org.springframework.context.annotation.*;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.socket.WebSocketHandler;
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
import java.util.Map;

@Configuration
public class WebSocketConfig {

    // 1) Route URLs to WebSocketHandler beans; low order beats @Controller mapping.
    @Bean
    public HandlerMapping webSocketMapping(EchoHandler echoHandler) {
        SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
        mapping.setUrlMap(Map.<String, WebSocketHandler>of("/ws/echo", echoHandler));
        mapping.setOrder(-1);
        return mapping;
    }

    // 2) Adapter that invokes WebSocketHandler; default ctor uses HandshakeWebSocketService.
    @Bean
    public WebSocketHandlerAdapter handlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

go deeper

for a junior

Know you need a URL mapping and an adapter bean to expose a WebSocketHandler.

for a middle

Name SimpleUrlHandlerMapping + WebSocketHandlerAdapter and set the mapping order.

for a senior

Explain the DispatcherHandler mapping/adapter flow and HandshakeWebSocketService delegating to a RequestUpgradeStrategy.

for a principal

Discuss per-server upgrade strategies, custom WebSocketService configuration (frame limits, sub-protocols), and why it differs from the Servlet annotation model.

## The request flow in WebFlux Every request in WebFlux enters `DispatcherHandler`, which holds ordered lists of: - **`HandlerMapping`** beans — map a request to a *handler object*. - **`HandlerAdapter`** beans — know how to *invoke* a given handler type. - **`HandlerResultHandler`** beans — process the result. For annotated controllers this is `RequestMappingHandlerMapping` + `RequestMappingHandlerAdapter`. For raw WebSockets you plug in the WebSocket equivalents. ## Bean 1 — SimpleUrlHandlerMapping (the routing) `SimpleUrlHandlerMapping` maps **URL path patterns → handler objects**. For WebSockets the handler objects are your `WebSocketHandler` instances: ```java @Bean public HandlerMapping webSocketMapping(EchoHandler echo, ChatHandler chat) { Map<String, WebSocketHandler> map = new LinkedHashMap<>(); map.put("/ws/echo", echo); map.put("/ws/chat", chat); SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setUrlMap(map); mapping.setOrder(-1); // IMPORTANT: before annotated-controller mapping return mapping; } ``` **Order matters.** `RequestMappingHandlerMapping` has order 0 by default. If your `SimpleUrlHandlerMapping` has a higher (later) order, an unrelated controller mapping might match first or a not-found could short-circuit. Setting `order` to a negative value (e.g. `-1` or `Ordered.HIGHEST_PRECEDENCE`-ish) ensures the WebSocket path is resolved first. ## Bean 2 — WebSocketHandlerAdapter (the invocation) `WebSocketHandlerAdapter` is the `HandlerAdapter` that `supports(...)` a `WebSocketHandler`. When `DispatcherHandler` finds that the mapped handler is a `WebSocketHandler`, this adapter is selected. It delegates to a `WebSocketService`: ```java @Bean public WebSocketHandlerAdapter handlerAdapter() { return new WebSocketHandlerAdapter(); // default WebSocketService = new HandshakeWebSocketService() } ``` The no-arg constructor internally creates a `HandshakeWebSocketService`. You can pass a custom one: `new WebSocketHandlerAdapter(myWebSocketService)`. ## Bean 3 (implicit) — HandshakeWebSocketService (the upgrade) `HandshakeWebSocketService` implements `WebSocketService`. Its `handleRequest(exchange, handler)`: 1. **Validates the handshake**: the request must be a GET with `Upgrade: websocket` and `Connection: upgrade`, a valid `Sec-WebSocket-Version` (13), and `Sec-WebSocket-Key`. Invalid requests get 400/405/etc. 2. **Negotiates sub-protocol** if the handler declares `getSubProtocols()`. 3. **Delegates the actual upgrade** to a `RequestUpgradeStrategy` chosen for the running server — `ReactorNettyRequestUpgradeStrategy`, `TomcatRequestUpgradeStrategy`, `JettyRequestUpgradeStrategy`, `UndertowRequestUpgradeStrategy`. By default it detects the server from the classpath (`ServerLoader`). 4. On success, builds the `WebSocketSession` and calls `handler.handle(session)`, subscribing to the returned `Mono<Void>`. You usually don't declare `HandshakeWebSocketService` explicitly — it comes free with the default adapter — but you would if you need to configure a specific `RequestUpgradeStrategy`, custom `sessionAttributePredicate`, or handshake limits (e.g., max frame size on Reactor Netty via the strategy). ## Full minimal config ```java @Configuration public class WsConfig { @Bean HandlerMapping wsMapping(EchoHandler echo) { SimpleUrlHandlerMapping m = new SimpleUrlHandlerMapping(); m.setUrlMap(Map.of("/ws/echo", echo)); m.setOrder(-1); return m; } @Bean WebSocketHandlerAdapter wsAdapter() { return new WebSocketHandlerAdapter(); } } ``` That plus the `EchoHandler` bean is a complete, working reactive WebSocket endpoint. ## Gotchas - **Forgetting the WebSocketHandlerAdapter bean** → the mapping resolves the handler but `DispatcherHandler` finds no adapter that supports `WebSocketHandler`, yielding an error/500. Both beans are required. - **Order not set** → intermittent 404s or the path handled by the wrong mapping. - **This is NOT `@EnableWebSocket`/`WebSocketConfigurer`** — that is the Servlet stack. WebFlux uses these beans directly; there is no annotation-based enable for raw reactive WebSockets. - **Path matching** uses `PathPattern` matching in WebFlux (not AntPathMatcher), so patterns like `/ws/{room}` work and path variables are available via `session.getHandshakeInfo()`/exchange attributes. - **CORS / auth** happen before the upgrade at the Spring Security boundary (different leaf) — the handshake is an ordinary HTTP GET first.

  • Why set a negative order on the SimpleUrlHandlerMapping?
    RequestMappingHandlerMapping (annotated controllers) has order 0. A negative order makes the WebSocket mapping consulted first, so the upgrade path isn't shadowed or 404'd by controller routing.
  • What is the role of RequestUpgradeStrategy, and how is it chosen?
    It performs the server-specific HTTP-to-WebSocket protocol upgrade. HandshakeWebSocketService picks the strategy matching the running server (Reactor Netty, Tomcat, Jetty, Undertow) via classpath detection, unless you configure one explicitly.
  • What breaks if you register the mapping but forget the WebSocketHandlerAdapter?
    DispatcherHandler resolves the WebSocketHandler but finds no HandlerAdapter that supports it, so the request fails. Both beans are mandatory.

saying these in an interview costs you the question

  • Using @EnableWebSocket / WebSocketConfigurer (Servlet stack) for WebFlux
  • Registering only the mapping and omitting WebSocketHandlerAdapter
  • Leaving the mapping at default order so controllers shadow the path
  • Thinking HandshakeWebSocketService does the messaging rather than just the upgrade
  • Believing WebSocketHandler beans are auto-mapped by URL without SimpleUrlHandlerMapping

context