How do you set up a raw (low-level) WebSocket endpoint in Spring, without STOMP?
answer
- @EnableWebSocket + WebSocketConfigurer
- registerWebSocketHandlers -> registry.addHandler(handler, "/ws")
- TextWebSocketHandler = text frames
- .setAllowedOrigins / .withSockJS()
- raw frames, no STOMP/broker
basics
~10 sAdd @EnableWebSocket on a config class that implements WebSocketConfigurer, then override registerWebSocketHandlers to map your handler (a TextWebSocketHandler subclass) to a URL path with registry.addHandler(handler, "/ws").
solid answer
~40 sYou enable the low-level API with @EnableWebSocket on a @Configuration class that implements WebSocketConfigurer. Spring then calls registerWebSocketHandlers(WebSocketHandlerRegistry registry), where you bind a WebSocketHandler to one or more URL paths: registry.addHandler(myHandler, "/ws"). The handler is usually a subclass of TextWebSocketHandler (for text frames) or AbstractWebSocketHandler. You can chain .setAllowedOrigins(...) for CORS, .addInterceptors(new HttpSessionHandshakeInterceptor()) to carry data across the handshake, and .withSockJS() to enable the SockJS fallback for browsers/proxies that block native WebSockets. This is the bare-frame API: no subprotocol, no message broker, no destinations — you receive and send raw WebSocket frames yourself. Choose it when you want full control or a custom protocol; choose STOMP (@EnableWebSocketMessageBroker) when you want pub/sub, destinations, and @MessageMapping.
code
java · 27 lines@Configuration
@EnableWebSocket
public class WsConfig implements WebSocketConfigurer {
private final ChatHandler chatHandler;
public WsConfig(ChatHandler chatHandler) {
this.chatHandler = chatHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatHandler, "/ws/chat")
.setAllowedOrigins("https://app.example.com")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.withSockJS(); // optional fallback
}
}
@Component
class ChatHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message)
throws Exception {
session.sendMessage(new TextMessage("echo: " + message.getPayload()));
}
}go deeper
Know the trio: @EnableWebSocket, implement WebSocketConfigurer, registry.addHandler(handler, path). Know TextWebSocketHandler handles text.
Explain registration options (origins, interceptors, SockJS) and that handlers are singletons so state lives in the session.
Contrast raw vs STOMP explicitly and justify choosing low-level for custom protocols; know SockJS implications and CORS security.
Frame the decision around protocol ownership, operational simplicity, and interop; know that raw scales the same way but you own routing/broadcast fan-out.
## What "raw WebSocket" means WebSocket is a protocol (RFC 6455) that upgrades a single HTTP connection into a persistent, full-duplex TCP channel over which either side can push **frames** at any time. Spring offers two layers on top of it: - **Low-level / raw API** — you handle individual WebSocket frames yourself. This leaf. - **STOMP messaging** (`@EnableWebSocketMessageBroker`) — a higher-level pub/sub protocol *on top of* WebSocket with destinations, a broker, and `@MessageMapping` controllers. Raw handling means *no subprotocol semantics*: a message is just text or bytes; there is no built-in routing, acknowledgement, or broadcast — you write all of that. ## The three pieces you always wire up 1. **`@EnableWebSocket`** — a class-level annotation on a `@Configuration` bean. It imports Spring's WebSocket infrastructure (the `WebSocketHandlerMapping`, handshake handler, etc.). Without it, none of the registration callbacks run. 2. **`WebSocketConfigurer`** — an interface your config implements. Its single method `registerWebSocketHandlers(WebSocketHandlerRegistry registry)` is Spring's callback where you declare path→handler mappings. 3. **A `WebSocketHandler`** — the object that receives lifecycle and message callbacks. In practice you extend `TextWebSocketHandler` (text frames) or `BinaryWebSocketHandler`, both of which extend `AbstractWebSocketHandler`, which implements the raw `WebSocketHandler` interface. ## Registration options on the registry `registry.addHandler(handler, "/ws", "/events")` maps one handler to one or more paths and returns a `WebSocketHandlerRegistration` you can fluently configure: - **`.setAllowedOrigins("https://app.example.com")`** / `.setAllowedOriginPatterns(...)` — cross-origin control. Since Spring 4.1.5 the default only allows same-origin; a wildcard `*` must be set deliberately. This is a real security control, not cosmetic. - **`.addInterceptors(HandshakeInterceptor...)`** — hooks that run during the HTTP upgrade (e.g. `HttpSessionHandshakeInterceptor` copies HTTP-session attributes into the WebSocket session). - **`.setHandshakeHandler(...)`** — customize principal/subprotocol negotiation. - **`.withSockJS()`** — enable the SockJS fallback: if the browser or an intermediary can't do native WebSocket, SockJS emulates it over XHR-streaming/long-polling. This changes the client library you must use. ## Bean or new instance? Declare the handler as a `@Bean` (or instantiate a Spring-managed one) so it can inject collaborators. WebSocket handlers are **singletons shared across all sessions**, so keep per-connection state in the `WebSocketSession`, not in handler fields. ## When to use raw vs STOMP - **Raw**: custom binary/text protocol, minimal overhead, a simple echo/notification channel, or interop with a non-STOMP client. You own framing and routing. - **STOMP**: you want topics/queues, broadcast, subscription semantics, `@MessageMapping`, an external broker (RabbitMQ/ActiveMQ). Don't reinvent that on raw frames. ## Common gotchas - Forgetting `@EnableWebSocket` → registration callback never fires, endpoint 404s. - Storing per-user state in handler fields → data bleeds across sessions (singleton!). - Not setting allowed origins in production → either blocked (default) or wide open (`*`). - Assuming the URL after `.withSockJS()` behaves like a native `ws://` endpoint — SockJS adds its own URL suffixes and requires the SockJS client.
- What does .withSockJS() actually change for the client?It enables SockJS server-side emulation and requires the client to use the SockJS JavaScript library against an HTTP(S) URL (not raw ws://). If native WebSocket is available SockJS uses it; otherwise it falls back to XHR-streaming or long-polling, so the app keeps working behind proxies that block WebSocket.
- Why should the handler avoid instance fields for per-connection data?A registered WebSocketHandler is a singleton shared by every session, so mutable instance fields are shared across all clients. Per-connection state belongs in session.getAttributes() or a concurrent map keyed by session id.
saying these in an interview costs you the question
- Thinking @EnableWebSocket alone creates an endpoint without implementing WebSocketConfigurer/registerWebSocketHandlers
- Confusing raw WebSocketHandler with STOMP @MessageMapping / @EnableWebSocketMessageBroker
- Believing allowed origins default to wildcard (they default to same-origin)