skip to content

Raw WebSocket Handling

The low-level API gives you a handler and a session after the HTTP upgrade handshake, with no messaging semantics on top. Interviewers use it to check you understand what STOMP is adding before you reach for it.

part ofSpring Frameworkoverview, primer and where to startread it →
on this pageshow

explore

questions

5

How do you set up a raw (low-level) WebSocket endpoint in Spring, without STOMP?

level: juniorimportance: must knowfreq 55%

answer

  1. @EnableWebSocket + WebSocketConfigurer
  2. registerWebSocketHandlers -> registry.addHandler(handler, "/ws")
  3. TextWebSocketHandler = text frames
  4. .setAllowedOrigins / .withSockJS()
  5. raw frames, no STOMP/broker

basics

~10 s

Add @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 s

You 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
java
@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

for a junior

Know the trio: @EnableWebSocket, implement WebSocketConfigurer, registry.addHandler(handler, path). Know TextWebSocketHandler handles text.

for a middle

Explain registration options (origins, interceptors, SockJS) and that handlers are singletons so state lives in the session.

for a senior

Contrast raw vs STOMP explicitly and justify choosing low-level for custom protocols; know SockJS implications and CORS security.

for a principal

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)

context

open as a page

Walk through the lifecycle callbacks of a WebSocketHandler / TextWebSocketHandler.

level: middleimportance: must knowfreq 50%

basics

~20 s

afterConnectionEstablished runs when a client connects; handleMessage (or handleTextMessage in TextWebSocketHandler) runs per incoming frame; handleTransportError on I/O errors; afterConnectionClosed when the socket closes. You typically track sessions in the first and remove them in the last.

open as a page

Explain the WebSocket HTTP upgrade handshake and how you hook into it in Spring (HandshakeInterceptor).

level: seniorimportance: should knowfreq 40%

basics

~20 s

A WebSocket connection starts as an HTTP GET with Upgrade: websocket and Connection: Upgrade headers plus a Sec-WebSocket-Key. The server replies 101 Switching Protocols, after which the same TCP socket carries WebSocket frames. In Spring you hook this with a HandshakeInterceptor to inspect/authorize the request and pass attributes into the session.

open as a page

Is WebSocketSession.sendMessage thread-safe, and how do you safely send to a session from multiple threads?

level: seniorimportance: should knowfreq 35%

basics

~10 s

No. Concurrent sendMessage calls on the same WebSocketSession are not safe and can corrupt the frame stream or throw. Wrap the session in a ConcurrentWebSocketSessionDecorator, which serializes sends and buffers them with size/time limits.

open as a page

When would you choose the raw low-level WebSocket API over STOMP, and what do you give up?

level: principalimportance: should knowfreq 30%

basics

~20 s

Use the raw API when you need a custom or minimal protocol, tight control over frames, or interop with a non-STOMP client. You give up STOMP's built-in destinations, pub/sub broadcast, subscriptions, @MessageMapping routing, acks, and broker integration — you'd have to build session tracking and fan-out yourself.

open as a page