1
0
Files
2026-09-04 00:52:18 +05:30

120 lines
5.6 KiB
Markdown

# 5. The limits: origin, size, and which one actually fires
Previous: [4. STOMP](04-stomp.md) · Next: [6. Choosing](06-choosing.md)
---
## 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`](../src/test/java/com/ankurm/ssews/WebSocketHandshakeOriginTest.java)
varies only that header ([`handshake-origin.txt`](output/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`](../src/test/java/com/ankurm/ssews/StompSizeLimitDefaultBufferTest.java)
binary-searches for the exact ceiling
([`websocket-size-limits.txt`](output/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 `messageSizeLimit` is 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`](../src/main/java/com/ankurm/ssews/WebSocketBufferConfig.java), active
under the `bigframes` profile):
```java
@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`](../src/test/java/com/ankurm/ssews/SizeProbe.java) 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](06-choosing.md) exists.
---
Previous: [4. STOMP](04-stomp.md) · Next: [6. Choosing](06-choosing.md)