Add the sse-websocket module
This commit is contained in:
119
sse-websocket/docs/05-limits.md
Normal file
119
sse-websocket/docs/05-limits.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user