From a7723c438116b4f5f71fdaba206f1dae0b4c926b Mon Sep 17 00:00:00 2001 From: Andriy Redko Date: Fri, 4 Oct 2024 12:44:27 -0400 Subject: [PATCH] Support allowPartialChunks in the HttpDecoderSpec (#3453) Signed-off-by: Andriy Redko --- .../reactor/netty/http/HttpDecoderSpec.java | 32 +++++++++++++++++-- .../netty/http/client/HttpClientConfig.java | 6 ++-- .../http/client/HttpResponseDecoderSpec.java | 7 ++-- .../http/server/HttpRequestDecoderSpec.java | 4 ++- .../netty/http/server/HttpServerConfig.java | 6 ++-- .../netty/http/HttpDecoderSpecTest.java | 30 ++++++++++++++++- .../netty/http/client/HttpClientTest.java | 6 +++- .../netty/http/server/HttpServerTests.java | 6 +++- 8 files changed, 84 insertions(+), 13 deletions(-) diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/HttpDecoderSpec.java b/reactor-netty-http/src/main/java/reactor/netty/http/HttpDecoderSpec.java index a378d8a541..6ed2d7835d 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/HttpDecoderSpec.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/HttpDecoderSpec.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2024 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ public abstract class HttpDecoderSpec> implements S public static final boolean DEFAULT_VALIDATE_HEADERS = true; public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128; public static final boolean DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS = false; + public static final boolean DEFAULT_ALLOW_PARTIAL_CHUNKS = true; protected int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH; protected int maxHeaderSize = DEFAULT_MAX_HEADER_SIZE; @@ -45,6 +46,7 @@ public abstract class HttpDecoderSpec> implements S protected int initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE; protected boolean allowDuplicateContentLengths = DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS; protected int h2cMaxContentLength; + protected boolean allowPartialChunks = DEFAULT_ALLOW_PARTIAL_CHUNKS; /** * Configure the maximum length that can be decoded for the HTTP request's initial @@ -225,6 +227,30 @@ public int h2cMaxContentLength() { return h2cMaxContentLength; } + /** + * Configure whether chunks can be split into multiple messages, if their chunk size exceeds the size of the + * input buffer. Defaults to {@link #DEFAULT_ALLOW_PARTIAL_CHUNKS}. + * + * @param allowPartialChunks set to {@code false} to only allow sending whole chunks down the pipeline. + * @return this option builder for further configuration + * @since 1.1.23 + */ + public T allowPartialChunks(boolean allowPartialChunks) { + this.allowPartialChunks = allowPartialChunks; + return get(); + } + + /** + * Return the configuration whether chunks can be split into multiple messages, if their chunk size + * exceeds the size of the input buffer. + * + * @return the configuration whether to allow duplicate {@code Content-Length} headers + * @since 1.1.23 + */ + public boolean allowPartialChunks() { + return allowPartialChunks; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -240,7 +266,8 @@ public boolean equals(Object o) { validateHeaders == that.validateHeaders && initialBufferSize == that.initialBufferSize && allowDuplicateContentLengths == that.allowDuplicateContentLengths && - h2cMaxContentLength == that.h2cMaxContentLength; + h2cMaxContentLength == that.h2cMaxContentLength && + allowPartialChunks == that.allowPartialChunks; } @Override @@ -253,6 +280,7 @@ public int hashCode() { result = 31 * result + initialBufferSize; result = 31 * result + Boolean.hashCode(allowDuplicateContentLengths); result = 31 * result + h2cMaxContentLength; + result = 31 * result + Boolean.hashCode(allowPartialChunks); return result; } } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java index 9b8da39879..beb20b9584 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConfig.java @@ -653,7 +653,8 @@ static void configureHttp11OrH2CleartextPipeline( .setMaxChunkSize(decoder.maxChunkSize()) .setValidateHeaders(decoder.validateHeaders()) .setInitialBufferSize(decoder.initialBufferSize()) - .setAllowDuplicateContentLengths(decoder.allowDuplicateContentLengths()); + .setAllowDuplicateContentLengths(decoder.allowDuplicateContentLengths()) + .setAllowPartialChunks(decoder.allowPartialChunks()); HttpClientCodec httpClientCodec = new HttpClientCodec(decoderConfig, decoder.failOnMissingResponse, decoder.parseHttpAfterConnectRequest); @@ -715,7 +716,8 @@ static void configureHttp11Pipeline(ChannelPipeline p, .setMaxChunkSize(decoder.maxChunkSize()) .setValidateHeaders(decoder.validateHeaders()) .setInitialBufferSize(decoder.initialBufferSize()) - .setAllowDuplicateContentLengths(decoder.allowDuplicateContentLengths()); + .setAllowDuplicateContentLengths(decoder.allowDuplicateContentLengths()) + .setAllowPartialChunks(decoder.allowPartialChunks()); p.addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.HttpCodec, new HttpClientCodec(decoderConfig, decoder.failOnMissingResponse, decoder.parseHttpAfterConnectRequest)); diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpResponseDecoderSpec.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpResponseDecoderSpec.java index 664a1f35b6..697a2c6fdc 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpResponseDecoderSpec.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpResponseDecoderSpec.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2023 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2024 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package reactor.netty.http.client; import reactor.netty.http.HttpDecoderSpec; -import reactor.netty.http.server.HttpRequestDecoderSpec; /** * A configuration builder to fine tune the {@link io.netty.handler.codec.http.HttpClientCodec} @@ -33,6 +32,7 @@ * {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}4096 * {@link #DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST}false * {@link #DEFAULT_VALIDATE_HEADERS}true + * {@link #DEFAULT_ALLOW_PARTIAL_CHUNKS}true * * * @author Violeta Georgieva @@ -110,7 +110,7 @@ public int hashCode() { } /** - * Build a {@link HttpRequestDecoderSpec}. + * Build a {@link HttpResponseDecoderSpec}. */ HttpResponseDecoderSpec build() { HttpResponseDecoderSpec decoder = new HttpResponseDecoderSpec(); @@ -123,6 +123,7 @@ HttpResponseDecoderSpec build() { decoder.failOnMissingResponse = failOnMissingResponse; decoder.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest; decoder.h2cMaxContentLength = h2cMaxContentLength; + decoder.allowPartialChunks = allowPartialChunks; return decoder; } } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpRequestDecoderSpec.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpRequestDecoderSpec.java index 6ca93af796..c416c5a60c 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpRequestDecoderSpec.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpRequestDecoderSpec.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018-2021 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2018-2024 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ * {@link #DEFAULT_MAX_HEADER_SIZE}8192 * {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}4096 * {@link #DEFAULT_VALIDATE_HEADERS}true + * {@link #DEFAULT_ALLOW_PARTIAL_CHUNKS}true * * * @author Simon Baslé @@ -66,6 +67,7 @@ HttpRequestDecoderSpec build() { decoder.validateHeaders = validateHeaders; decoder.allowDuplicateContentLengths = allowDuplicateContentLengths; decoder.h2cMaxContentLength = h2cMaxContentLength; + decoder.allowPartialChunks = allowPartialChunks; return decoder; } } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java index a746dc7ba5..11f59cef73 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java @@ -650,7 +650,8 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p, .setMaxChunkSize(decoder.maxChunkSize()) .setValidateHeaders(decoder.validateHeaders()) .setInitialBufferSize(decoder.initialBufferSize()) - .setAllowDuplicateContentLengths(decoder.allowDuplicateContentLengths()); + .setAllowDuplicateContentLengths(decoder.allowDuplicateContentLengths()) + .setAllowPartialChunks(decoder.allowPartialChunks()); HttpServerCodec httpServerCodec = new HttpServerCodec(decoderConfig); @@ -736,7 +737,8 @@ static void configureHttp11Pipeline(ChannelPipeline p, .setMaxChunkSize(decoder.maxChunkSize()) .setValidateHeaders(decoder.validateHeaders()) .setInitialBufferSize(decoder.initialBufferSize()) - .setAllowDuplicateContentLengths(decoder.allowDuplicateContentLengths()); + .setAllowDuplicateContentLengths(decoder.allowDuplicateContentLengths()) + .setAllowPartialChunks(decoder.allowPartialChunks()); p.addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.HttpCodec, new HttpServerCodec(decoderConfig)) diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/HttpDecoderSpecTest.java b/reactor-netty-http/src/test/java/reactor/netty/http/HttpDecoderSpecTest.java index 4c81c5a72e..5b22c42d3b 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/HttpDecoderSpecTest.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/HttpDecoderSpecTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-2024 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -43,6 +43,7 @@ void maxInitialLineLength() { checkDefaultValidateHeaders(conf); checkDefaultInitialBufferSize(conf); checkDefaultAllowDuplicateContentLengths(conf); + checkDefaultAllowPartialChunks(conf); } @Test @@ -71,6 +72,7 @@ void maxHeaderSize() { checkDefaultValidateHeaders(conf); checkDefaultInitialBufferSize(conf); checkDefaultAllowDuplicateContentLengths(conf); + checkDefaultAllowPartialChunks(conf); } @Test @@ -100,6 +102,7 @@ void maxChunkSize() { checkDefaultValidateHeaders(conf); checkDefaultInitialBufferSize(conf); checkDefaultAllowDuplicateContentLengths(conf); + checkDefaultAllowPartialChunks(conf); } @Test @@ -129,6 +132,7 @@ void validateHeaders() { checkDefaultMaxChunkSize(conf); checkDefaultInitialBufferSize(conf); checkDefaultAllowDuplicateContentLengths(conf); + checkDefaultAllowPartialChunks(conf); } @Test @@ -144,6 +148,7 @@ void initialBufferSize() { checkDefaultMaxChunkSize(conf); checkDefaultValidateHeaders(conf); checkDefaultAllowDuplicateContentLengths(conf); + checkDefaultAllowPartialChunks(conf); } @Test @@ -172,6 +177,23 @@ void allowDuplicateContentLengths() { checkDefaultMaxChunkSize(conf); checkDefaultValidateHeaders(conf); checkDefaultInitialBufferSize(conf); + checkDefaultAllowPartialChunks(conf); + } + + @Test + void allowPartialChunks() { + checkDefaultAllowPartialChunks(conf); + + conf.allowPartialChunks(false); + + assertThat(conf.allowPartialChunks()).as("allow partial chunks").isFalse(); + + checkDefaultMaxInitialLineLength(conf); + checkDefaultMaxHeaderSize(conf); + checkDefaultMaxChunkSize(conf); + checkDefaultValidateHeaders(conf); + checkDefaultInitialBufferSize(conf); + checkDefaultAllowDuplicateContentLengths(conf); } public static void checkDefaultMaxInitialLineLength(HttpDecoderSpec conf) { @@ -211,6 +233,12 @@ public static void checkDefaultAllowDuplicateContentLengths(HttpDecoderSpec c .isFalse(); } + public static void checkDefaultAllowPartialChunks(HttpDecoderSpec conf) { + assertThat(conf.allowPartialChunks()).as("default allow partial chunks") + .isEqualTo(HttpDecoderSpec.DEFAULT_ALLOW_PARTIAL_CHUNKS) + .isTrue(); + } + private static final class HttpDecoderSpecImpl extends HttpDecoderSpec { @Override diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java b/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java index 43bc5cce11..5fff508aba 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/client/HttpClientTest.java @@ -1701,6 +1701,7 @@ void httpClientResponseConfigInjectAttributes() { AtomicBoolean validate = new AtomicBoolean(); AtomicInteger chunkSize = new AtomicInteger(); AtomicBoolean allowDuplicateContentLengths = new AtomicBoolean(); + AtomicBoolean allowPartialChunks = new AtomicBoolean(true); disposableServer = createServer() .handle((req, resp) -> req.receive() @@ -1715,7 +1716,8 @@ void httpClientResponseConfigInjectAttributes() { .initialBufferSize(10) .failOnMissingResponse(true) .parseHttpAfterConnectRequest(true) - .allowDuplicateContentLengths(true)) + .allowDuplicateContentLengths(true) + .allowPartialChunks(false)) .doOnConnected(c -> { channelRef.set(c.channel()); HttpClientCodec codec = c.channel() @@ -1725,6 +1727,7 @@ void httpClientResponseConfigInjectAttributes() { chunkSize.set((Integer) getValueReflection(decoder, "maxChunkSize", 2)); validate.set((Boolean) getValueReflection(decoder, "validateHeaders", 2)); allowDuplicateContentLengths.set((Boolean) getValueReflection(decoder, "allowDuplicateContentLengths", 2)); + allowPartialChunks.set((Boolean) getValueReflection(decoder, "allowPartialChunks", 2)); }) .post() .uri("/") @@ -1738,6 +1741,7 @@ void httpClientResponseConfigInjectAttributes() { assertThat(chunkSize).as("line length").hasValue(789); assertThat(validate).as("validate headers").isFalse(); assertThat(allowDuplicateContentLengths).as("allow duplicate Content-Length").isTrue(); + assertThat(allowPartialChunks).as("allow partial chunks").isFalse(); } private Object getValueReflection(Object obj, String fieldName, int superLevel) { diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerTests.java b/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerTests.java index 4213d73a14..a391896fc7 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerTests.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerTests.java @@ -975,6 +975,7 @@ void httpServerRequestConfigInjectAttributes() { AtomicBoolean validate = new AtomicBoolean(); AtomicInteger chunkSize = new AtomicInteger(); AtomicBoolean allowDuplicateContentLengths = new AtomicBoolean(); + AtomicBoolean allowPartialChunks = new AtomicBoolean(true); disposableServer = createServer() .httpRequestDecoder(opt -> opt.maxInitialLineLength(123) @@ -982,7 +983,8 @@ void httpServerRequestConfigInjectAttributes() { .maxChunkSize(789) .validateHeaders(false) .initialBufferSize(10) - .allowDuplicateContentLengths(true)) + .allowDuplicateContentLengths(true) + .allowPartialChunks(false)) .handle((req, resp) -> req.receive().then(resp.sendNotFound())) .doOnConnection(c -> { channelRef.set(c.channel()); @@ -993,6 +995,7 @@ void httpServerRequestConfigInjectAttributes() { chunkSize.set((Integer) getValueReflection(decoder, "maxChunkSize", 2)); validate.set((Boolean) getValueReflection(decoder, "validateHeaders", 2)); allowDuplicateContentLengths.set((Boolean) getValueReflection(decoder, "allowDuplicateContentLengths", 2)); + allowPartialChunks.set((Boolean) getValueReflection(decoder, "allowPartialChunks", 2)); }) .bindNow(); @@ -1009,6 +1012,7 @@ void httpServerRequestConfigInjectAttributes() { assertThat(chunkSize).as("line length").hasValue(789); assertThat(validate).as("validate headers").isFalse(); assertThat(allowDuplicateContentLengths).as("allow duplicate Content-Length").isTrue(); + assertThat(allowPartialChunks).as("allow partial chunks").isFalse(); } private Object getValueReflection(Object obj, String fieldName, int superLevel) {