120 lines
5.6 KiB
Markdown
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)
|