diff --git a/src/main/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfiguration.java b/src/main/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfiguration.java
new file mode 100644
index 0000000000..1e1689f627
--- /dev/null
+++ b/src/main/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfiguration.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2011-2017 Pivotal Software Inc, 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package reactor.ipc.netty.http.server;
+
+import java.util.function.Function;
+
+import io.netty.util.AttributeKey;
+import reactor.ipc.netty.tcp.TcpServer;
+
+/**
+ * A configuration builder to fine tune the {@link io.netty.handler.codec.http.HttpServerCodec}
+ * (or more precisely the {@link io.netty.handler.codec.http.HttpServerCodec.HttpServerRequestDecoder}).
+ *
+ * Defaults are accessible as constants {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}, {@link #DEFAULT_MAX_HEADER_SIZE}
+ * and {@link #DEFAULT_MAX_CHUNK_SIZE}.
+ *
+ * @author Simon Baslé
+ */
+public final class HttpRequestDecoderConfiguration {
+
+ public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4096;
+ public static final int DEFAULT_MAX_HEADER_SIZE = 8192;
+ public static final int DEFAULT_MAX_CHUNK_SIZE = 8192;
+ public static final boolean DEFAULT_VALIDATE_HEADERS = true;
+ public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128;
+
+ static final AttributeKey MAX_INITIAL_LINE_LENGTH = AttributeKey.newInstance("httpCodecMaxInitialLineLength");
+ static final AttributeKey MAX_HEADER_SIZE = AttributeKey.newInstance("httpCodecMaxHeaderSize");
+ static final AttributeKey MAX_CHUNK_SIZE = AttributeKey.newInstance("httpCodecMaxChunkSize");
+ static final AttributeKey VALIDATE_HEADERS = AttributeKey.newInstance("httpCodecValidateHeaders");
+ static final AttributeKey INITIAL_BUFFER_SIZE = AttributeKey.newInstance("httpCodecInitialBufferSize");
+
+ int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH;
+ int maxHeaderSize = DEFAULT_MAX_HEADER_SIZE;
+ int maxChunkSize = DEFAULT_MAX_CHUNK_SIZE;
+ boolean validateHeaders = DEFAULT_VALIDATE_HEADERS;
+ int initialBufferSize = DEFAULT_INITIAL_BUFFER_SIZE;
+
+ /**
+ * Configure the maximum length that can be decoded for the HTTP request's initial
+ * line. Defaults to {@link #DEFAULT_MAX_INITIAL_LINE_LENGTH}.
+ *
+ * @param value the value for the maximum initial line length (strictly positive)
+ * @return this option builder for further configuration
+ */
+ public HttpRequestDecoderConfiguration maxInitialLineLength(int value) {
+ if (value <= 0) {
+ throw new IllegalArgumentException(
+ "maxInitialLineLength must be strictly positive");
+ }
+ this.maxInitialLineLength = value;
+ return this;
+ }
+
+ /**
+ * Configure the maximum header size that can be decoded for the HTTP request.
+ * Defaults to {@link #DEFAULT_MAX_HEADER_SIZE}.
+ *
+ * @param value the value for the maximum header size (strictly positive)
+ * @return this option builder for further configuration
+ */
+ public HttpRequestDecoderConfiguration maxHeaderSize(int value) {
+ if (value <= 0) {
+ throw new IllegalArgumentException("maxHeaderSize must be strictly positive");
+ }
+ this.maxHeaderSize = value;
+ return this;
+ }
+
+ /**
+ * Configure the maximum chunk size that can be decoded for the HTTP request.
+ * Defaults to {@link #DEFAULT_MAX_CHUNK_SIZE}.
+ *
+ * @param value the value for the maximum chunk size (strictly positive)
+ * @return this option builder for further configuration
+ */
+ public HttpRequestDecoderConfiguration maxChunkSize(int value) {
+ if (value <= 0) {
+ throw new IllegalArgumentException("maxChunkSize must be strictly positive");
+ }
+ this.maxChunkSize = value;
+ return this;
+ }
+
+ /**
+ * Configure whether or not to validate headers when decoding requests. Defaults to
+ * #DEFAULT_VALIDATE_HEADERS.
+ *
+ * @param validate true to validate headers, false otherwise
+ * @return this option builder for further configuration
+ */
+ public HttpRequestDecoderConfiguration validateHeaders(boolean validate) {
+ this.validateHeaders = validate;
+ return this;
+ }
+
+ /**
+ * Configure the initial buffer size for HTTP request decoding. Defaults to
+ * {@link #DEFAULT_INITIAL_BUFFER_SIZE}.
+ *
+ * @param value the initial buffer size to use (strictly positive)
+ * @return
+ */
+ public HttpRequestDecoderConfiguration initialBufferSize(int value) {
+ if (value <= 0) {
+ throw new IllegalArgumentException("initialBufferSize must be strictly positive");
+ }
+ this.initialBufferSize = value;
+ return this;
+ }
+
+ /**
+ * Build a {@link Function} that applies the http request decoder configuration to a
+ * {@link TcpServer} by enriching its attributes.
+ */
+ Function build() {
+ return tcp -> tcp.selectorAttr(MAX_INITIAL_LINE_LENGTH, maxInitialLineLength)
+ .selectorAttr(MAX_HEADER_SIZE, maxHeaderSize)
+ .selectorAttr(MAX_CHUNK_SIZE, maxChunkSize)
+ .selectorAttr(VALIDATE_HEADERS, validateHeaders)
+ .selectorAttr(INITIAL_BUFFER_SIZE, initialBufferSize);
+ }
+
+}
diff --git a/src/main/java/reactor/ipc/netty/http/server/HttpServer.java b/src/main/java/reactor/ipc/netty/http/server/HttpServer.java
index b887440ae5..73ba3a5133 100644
--- a/src/main/java/reactor/ipc/netty/http/server/HttpServer.java
+++ b/src/main/java/reactor/ipc/netty/http/server/HttpServer.java
@@ -235,6 +235,18 @@ public final HttpServer noCompression() {
return tcpConfiguration(COMPRESS_ATTR_DISABLE);
}
+ /**
+ * Configure the {@link io.netty.handler.codec.http.HttpServerCodec}'s request decoding options.
+ *
+ * @param requestDecoderOptions a function to mutate the provided Http request decoder options
+ * @return a new {@link HttpServer}
+ */
+ public final HttpServer httpRequestDecoder(Function requestDecoderOptions) {
+ return tcpConfiguration(
+ requestDecoderOptions.apply(new HttpRequestDecoderConfiguration())
+ .build());
+ }
+
/**
* Disable support for the {@code "Forwarded"} and {@code "X-Forwarded-*"}
* HTTP request headers.
diff --git a/src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java b/src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java
index 18c13939c3..32161626cd 100644
--- a/src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java
+++ b/src/main/java/reactor/ipc/netty/http/server/HttpServerBind.java
@@ -33,6 +33,17 @@
import reactor.ipc.netty.resources.LoopResources;
import reactor.ipc.netty.tcp.TcpServer;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.INITIAL_BUFFER_SIZE;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.MAX_CHUNK_SIZE;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.MAX_HEADER_SIZE;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.MAX_INITIAL_LINE_LENGTH;
+import static reactor.ipc.netty.http.server.HttpRequestDecoderConfiguration.VALIDATE_HEADERS;
+
/**
* @author Stephane Maldini
*/
@@ -81,18 +92,24 @@ public Mono extends DisposableServer> bind(TcpServer delegate) {
.channel(loops.onServerChannel(elg));
}
- Integer minCompressionSize = (Integer) b.config()
- .attrs()
- .get(PRODUCE_GZIP);
+ Integer minCompressionSize = getAttributeValue(b, PRODUCE_GZIP, null);
+
+ Integer line = getAttributeValue(b, MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_INITIAL_LINE_LENGTH);
+
+ Integer header = getAttributeValue(b, MAX_HEADER_SIZE, DEFAULT_MAX_HEADER_SIZE);
+
+ Integer chunk = getAttributeValue(b, MAX_CHUNK_SIZE, DEFAULT_MAX_CHUNK_SIZE);
- b.attr(PRODUCE_GZIP, null);
+ Boolean validate = getAttributeValue(b, VALIDATE_HEADERS, DEFAULT_VALIDATE_HEADERS);
+
+ Integer buffer = getAttributeValue(b, INITIAL_BUFFER_SIZE, DEFAULT_INITIAL_BUFFER_SIZE);
BootstrapHandlers.updateConfiguration(b,
NettyPipeline.HttpInitializer,
(listener, channel) -> {
ChannelPipeline p = channel.pipeline();
- p.addLast(NettyPipeline.HttpCodec, new HttpServerCodec());
+ p.addLast(NettyPipeline.HttpCodec, new HttpServerCodec(line, header, chunk, validate, buffer));
if (minCompressionSize != null && minCompressionSize >= 0) {
p.addLast(NettyPipeline.CompressionHandler,
@@ -108,4 +125,12 @@ public Mono extends DisposableServer> bind(TcpServer delegate) {
static final AttributeKey PRODUCE_GZIP =
AttributeKey.newInstance("produceGzip");
+
+ private T getAttributeValue(ServerBootstrap bootstrap, AttributeKey attributeKey, T defaultValue) {
+ T result = bootstrap.config().attrs().get(attributeKey) != null
+ ? (T) bootstrap.config().attrs().get(attributeKey)
+ : defaultValue;
+ bootstrap.attr(attributeKey, null);
+ return result;
+ }
}
diff --git a/src/test/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfigurationTest.java b/src/test/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfigurationTest.java
new file mode 100644
index 0000000000..a9e8f6ae6e
--- /dev/null
+++ b/src/test/java/reactor/ipc/netty/http/server/HttpRequestDecoderConfigurationTest.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2011-2017 Pivotal Software Inc, 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.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package reactor.ipc.netty.http.server;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
+public class HttpRequestDecoderConfigurationTest {
+
+ private HttpRequestDecoderConfiguration conf;
+
+ @Before
+ public void init() {
+ conf = new HttpRequestDecoderConfiguration();
+ }
+
+ @Test
+ public void maxInitialLineLength() {
+ conf.maxInitialLineLength(123);
+
+ assertThat(conf.maxInitialLineLength).as("initial line length").isEqualTo(123);
+
+ assertThat(conf.maxHeaderSize).as("default header size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE);
+ assertThat(conf.maxChunkSize).as("default chunk size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE);
+ assertThat(conf.validateHeaders).as("default validate headers").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS);
+ assertThat(conf.initialBufferSize).as("default initial buffer sizez").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE);
+ }
+
+ @Test
+ public void maxInitialLineLengthBadValues() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> conf.maxInitialLineLength(0))
+ .as("rejects 0")
+ .withMessage("maxInitialLineLength must be strictly positive");
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> conf.maxInitialLineLength(-1))
+ .as("rejects negative")
+ .withMessage("maxInitialLineLength must be strictly positive");
+ }
+
+ @Test
+ public void maxHeaderSize() {
+ conf.maxHeaderSize(123);
+
+ assertThat(conf.maxHeaderSize).as("header size").isEqualTo(123);
+
+ assertThat(conf.maxInitialLineLength).as("default initial line length").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH);
+ assertThat(conf.maxChunkSize).as("default chunk size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE);
+ assertThat(conf.validateHeaders).as("default validate headers").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS);
+ assertThat(conf.initialBufferSize).as("default initial buffer sizez").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE);
+ }
+
+ @Test
+ public void maxHeaderSizeBadValues() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> conf.maxHeaderSize(0))
+ .as("rejects 0")
+ .withMessage("maxHeaderSize must be strictly positive");
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> conf.maxHeaderSize(-1))
+ .as("rejects negative")
+ .withMessage("maxHeaderSize must be strictly positive");
+ }
+
+ @Test
+ public void maxChunkSize() {
+ conf.maxChunkSize(123);
+
+ assertThat(conf.maxChunkSize).as("chunk size").isEqualTo(123);
+
+ assertThat(conf.maxInitialLineLength).as("default initial line length").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH);
+ assertThat(conf.maxHeaderSize).as("default header size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE);
+ assertThat(conf.validateHeaders).as("default validate headers").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS);
+ assertThat(conf.initialBufferSize).as("default initial buffer sizez").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE);
+ }
+
+ @Test
+ public void maxChunkSizeBadValues() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> conf.maxChunkSize(0))
+ .as("rejects 0")
+ .withMessage("maxChunkSize must be strictly positive");
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> conf.maxChunkSize(-1))
+ .as("rejects negative")
+ .withMessage("maxChunkSize must be strictly positive");
+ }
+
+ @Test
+ public void validateHeaders() {
+ conf.validateHeaders(false);
+
+ assertThat(conf.validateHeaders).as("validate headers").isFalse();
+
+ assertThat(conf.maxInitialLineLength).as("default initial line length").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH);
+ assertThat(conf.maxHeaderSize).as("default header size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE);
+ assertThat(conf.maxChunkSize).as("default chunk size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE);
+ assertThat(conf.initialBufferSize).as("default initial buffer sizez").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_INITIAL_BUFFER_SIZE);
+ }
+
+ @Test
+ public void initialBufferSize() {
+ conf.initialBufferSize(123);
+
+ assertThat(conf.initialBufferSize).as("initial buffer size").isEqualTo(123);
+
+ assertThat(conf.maxInitialLineLength).as("default initial line length").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_INITIAL_LINE_LENGTH);
+ assertThat(conf.maxHeaderSize).as("default header size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_HEADER_SIZE);
+ assertThat(conf.maxChunkSize).as("default chunk size").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_MAX_CHUNK_SIZE);
+ assertThat(conf.validateHeaders).as("default validate headers").isEqualTo(HttpRequestDecoderConfiguration.DEFAULT_VALIDATE_HEADERS);
+ }
+
+ @Test
+ public void initialBufferSizeBadValues() {
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> conf.initialBufferSize(0))
+ .as("rejects 0")
+ .withMessage("initialBufferSize must be strictly positive");
+
+ assertThatExceptionOfType(IllegalArgumentException.class)
+ .isThrownBy(() -> conf.initialBufferSize(-1))
+ .as("rejects negative")
+ .withMessage("initialBufferSize must be strictly positive");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/reactor/ipc/netty/http/server/HttpServerTests.java b/src/test/java/reactor/ipc/netty/http/server/HttpServerTests.java
index a894abbd59..e0cb72f008 100644
--- a/src/test/java/reactor/ipc/netty/http/server/HttpServerTests.java
+++ b/src/test/java/reactor/ipc/netty/http/server/HttpServerTests.java
@@ -17,6 +17,7 @@
package reactor.ipc.netty.http.server;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
@@ -55,7 +56,9 @@
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpObjectDecoder;
import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
@@ -966,4 +969,65 @@ public void redirectTests(String url) {
}
}
+
+ @Test
+ public void httpServerRequestConfigInjectAttributes() {
+ AtomicReference channelRef = new AtomicReference<>();
+ HttpServer server =
+ HttpServer.create()
+ .httpRequestDecoder(opt -> opt.maxInitialLineLength(123)
+ .maxHeaderSize(456)
+ .maxChunkSize(789)
+ .validateHeaders(false)
+ .initialBufferSize(10))
+ .handler((req, resp) -> resp.sendNotFound())
+ .tcpConfiguration(tcp -> tcp.doOnConnection(c -> channelRef.set(c.channel())))
+ .wiretap();
+
+ DisposableServer ds = server.bindNow();
+
+ HttpClient.prepare()
+ .addressSupplier(ds::address)
+ .post()
+ .uri("/")
+ .send(ByteBufFlux.fromString(Mono.just("bodysample")))
+ .responseContent()
+ .aggregate()
+ .asString()
+ .block();
+
+ assertThat(channelRef.get()).isNotNull();
+ Channel c = channelRef.get();
+ HttpServerCodec codec = c.pipeline().get(HttpServerCodec.class);
+ HttpObjectDecoder decoder = (HttpObjectDecoder) getValueReflection(codec, "inboundHandler", 1);
+ int chunkSize = (Integer) getValueReflection(decoder, "maxChunkSize", 2);
+ boolean validate = (Boolean) getValueReflection(decoder, "validateHeaders", 2);
+
+ ds.disposeNow();
+
+ assertThat(chunkSize).as("line length").isEqualTo(789);
+ assertThat(validate).as("validate headers").isFalse();
+ }
+
+ private Object getValueReflection(Object obj, String fieldName, int superLevel) {
+ try {
+ Field field;
+ if (superLevel == 1) {
+ field = obj.getClass()
+ .getSuperclass()
+ .getDeclaredField(fieldName);
+ }
+ else {
+ field = obj.getClass()
+ .getSuperclass()
+ .getSuperclass()
+ .getDeclaredField(fieldName);
+ }
+ field.setAccessible(true);
+ return field.get(obj);
+ }
+ catch(NoSuchFieldException | IllegalAccessException e) {
+ return null;
+ }
+ }
}