WebSockets - MaxFramePayloadLength behaviour when using WebSocketServerSpec.compress(true)
joebyneil opened this issue · comments
Expected Behavior
When using compression, payloads handled by the server should be rejected if the uncompressed frame size is greater than the maxFramePayloadLength.
Unsure if this is an expected behaviour but thought I would raise for discussion.
Actual Behavior
Frames passed onto handler are larger than the configured maxFramePayloadLength.
This case also occurs regardless of value passed to WebSocketFrameAggregator(maxContentLength)
via aggregateFrames(maxContentLength)
.
Steps to Reproduce
This test is modified version of existing tests in WebsocketTest.java
// Passes
@Test
void testServerMaxFramePayloadLengthSuccessWithoutCompress() {
String repeat = new String(new char[300]).replace("\0", "m");
doTestServerMaxFramePayloadLength(320,
Flux.just("m", repeat), Flux.just("m", repeat), 2, false);
}
// Passes
@Test
void testServerMaxFramePayloadLengthFailedWithoutCompress() {
String repeat = new String(new char[300]).replace("\0", "m");
doTestServerMaxFramePayloadLength(8,
Flux.just("m", repeat), Flux.just("m"), 2, false);
}
// This test fails, but is identical to above other than compression
@Test
void testServerMaxFramePayloadLengthFailedWithCompress() {
String repeat = new String(new char[300]).replace("\0", "m");
doTestServerMaxFramePayloadLength(8,
Flux.just("m", repeat), Flux.just("m"), 2, true);
}
private void doTestServerMaxFramePayloadLength(int maxFramePayloadLength, Flux<String> input, Flux<String> expectation, int count, boolean compress) {
disposableServer =
createServer()
.handle((req, res) -> res.sendWebsocket((in, out) ->
out.sendObject(in.aggregateFrames()
.receiveFrames()
.map(WebSocketFrame::content)
.map(byteBuf ->
byteBuf.readCharSequence(byteBuf.readableBytes(), Charset.defaultCharset()).toString())
.map(TextWebSocketFrame::new)),
WebsocketServerSpec.builder().maxFramePayloadLength(maxFramePayloadLength).compress(compress).build()))
.bindNow();
AtomicReference<List<String>> output = new AtomicReference<>(new ArrayList<>());
createClient(disposableServer.port())
.websocket(WebsocketClientSpec.builder().compress(compress).build())
.uri("/")
.handle((in, out) -> out.sendString(input)
.then(in.aggregateFrames()
.receiveFrames()
.map(WebSocketFrame::content)
.map(byteBuf ->
byteBuf.readCharSequence(byteBuf.readableBytes(), Charset.defaultCharset()).toString())
.take(count)
.doOnNext(s -> output.get().add(s))
.then()))
.blockLast(Duration.ofSeconds(30));
List<String> test = expectation.collectList().block(Duration.ofSeconds(30));
assertThat(output.get()).isEqualTo(test);
}
Possible Solution
This could be expected behaviour, but wanted to confirm.
Maybe one idea could be to have a seperate limit for final message size and frame size limits, or otherwise make it clearer that the application may need to confirm the payload size when using compression.
Your Environment
Tested within the reactor-netty repo on commit: ab6c07aa2b5d6ef522f741bd09cadbbfc04cbb50
- Other relevant libraries versions (eg.
netty
, ...): 4.1.107 final - JVM version (
java -version
): openjdk version "17.0.6" 2023-01-17 LTS - OS and version (eg.
uname -a
): Darwin Joebys-Laptop.local 23.4.0 Darwin Kernel Version 23.4.0: Wed Feb 21 21:44:43 PST 2024; root:xnu-10063.101.15~2/RELEASE_ARM64_T6000 arm64
Thanks for your help!
@joebyneil reactor.netty.http.websocket.WebsocketSpec#maxFramePayloadLength
specifies the limit for the incoming packet, regardless whether it is compressed or not. May be we need to improve the javadoc description.
About reactor.netty.http.websocket.WebsocketInbound#aggregateFrames(int)
it explicitly describes that the aggregation is related to fragmented frames: https://projectreactor.io/docs/netty/release/api/reactor/netty/http/websocket/WebsocketInbound.html#aggregateFrames-int-
Thanks for your reply, I suspected this might be the case.
Updating javadocs sounds good.
I do think it would be useful to have the option to limit uncompressed size, but it's also easy enough to just add that check in our application code.
Thanks for your help!