5.6 KiB
5. The limits: origin, size, and which one actually fires
Previous: 4. STOMP · Next: 6. Choosing
Origin: checked by default, and the check is real
The WebSocket handshake is an ordinary HTTP GET, and the browser's same-origin policy does not
apply to it. Without a server-side check, a page on any site could open a WebSocket to your
server with the user's cookies attached. Spring checks Origin by default.
WebSocketHandshakeOriginTest
varies only that header (handshake-origin.txt):
no Origin header -> HTTP/1.1 101
Origin: http://localhost -> HTTP/1.1 101
Origin: https://evil... -> HTTP/1.1 403
The middle row is the policy working. The first row is the one to understand: a client that
sends no Origin at all is allowed through, which is why every Java test in this module connects
without ceremony — and why Origin is a browser control, not an authentication mechanism.
For a real cross-origin front end use setAllowedOriginPatterns("https://app.example.com");
setAllowedOrigins("*") is rejected outright when credentials are allowed.
Size: two limits, and the documented one is not the one that fires
WebSocketTransportRegistration.setMessageSizeLimit defaults to 64 KB and is what every
guide tells you to raise. Underneath it sits the servlet container's own WebSocket buffer, which
/diag reports as:
"ws.container.defaultMaxTextMessageBufferSize":8192
Spring's SubProtocolWebSocketHandler does not support partial messages, so the container has to
assemble each text message whole before Spring sees it. Cross the container's ceiling and the
connection is closed — WebSocket status 1009, The decoded text message was too big for the output buffer and the endpoint does not support partial messages — before Spring's
limit is ever consulted.
StompSizeLimitDefaultBufferTest
binary-searches for the exact ceiling
(websocket-size-limits.txt):
| container buffer | largest body delivered | smallest rejected |
|---|---|---|
| 8 192 (Tomcat default) | 16 459 | 16 522 |
| 16 384 | 16 446 | 16 496 |
| 32 768 | 32 615 | 32 665 |
| 65 536 | 65 350 | 65 400 |
Body length is the JSON string field; the STOMP frame adds about 90 bytes of headers on top, so each row is a frame just under 16 KB, 16 KB, 32 KB and 64 KB respectively.
Read that table honestly: the ceiling behaves like max(16 KB, buffer). Setting the buffer to
16 KB changes nothing from the default; above 16 KB the ceiling follows it. I have not
reduced the 16 KB floor to a line in Tomcat's source and will not claim a mechanism I have
not read. What is certain, and is the point:
Spring's 64 KB
messageSizeLimitis not reached on a stock Tomcat. The real ceiling is about 16 KB, it is enforced by the container, and raising Spring's limit alone does nothing whatsoever.
The fix is a ServletServerContainerFactoryBean
(WebSocketBufferConfig, active
under the bigframes profile):
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(256 * 1024);
container.setMaxBinaryMessageBufferSize(256 * 1024);
return container;
}
The two failures look different, and only one is diagnosable
With the buffer at 256 KB, Spring's limit finally decides — and it fails better:
60 KB body -> delivered, body length 61440
80 KB body -> NOT delivered, connection dropped
org.springframework.messaging.simp.stomp.StompConversionException: The configured STOMP buffer size limit of 65536 bytes has been exceeded
Spring logs the exception, names the limit, and sends a STOMP ERROR frame the client can see.
The container's refusal does none of that. So the practical advice is not just "raise the buffer"
— it is raise the container buffer above Spring's limit deliberately, so that the limit
that fires is the one that tells you it fired.
The client has the same limit
A WebSocketStompClient built on StandardWebSocketClient inherits the same 8 KB default,
and hits it on its own session first. That produced a false positive while writing these tests:
a message that looked rejected by the server was actually refused by the client.
SizeProbe raises the client side to 1 MB
before measuring anything.
Send buffer and send time
Two limits that bite in production rather than in tests, both set in WebSocketConfig:
setSendBufferSizeLimit(512 KB here) — how much undelivered output Spring will hold for one slow client before closing its session. Without it, one client on hotel wifi can grow the heap until the process dies.setSendTimeLimit(20 s here) — how long a single send may take before the session is closed.
Both exist because a broadcast is only as fast as its slowest subscriber, and a WebSocket session has no equivalent of TCP's back-pressure reaching your application code. This is precisely the gap RSocket closes, and the reason the next article exists.
Previous: 4. STOMP · Next: 6. Choosing