From f068f9fc525ba16353c09ac4ddc48bb0cc1df6d5 Mon Sep 17 00:00:00 2001 From: Julien Eyraud Date: Fri, 17 Jul 2020 00:45:10 +0200 Subject: [PATCH 1/2] Add properties for Netty HttpDecoderSpec See gh-22367 --- .../autoconfigure/web/ServerProperties.java | 65 +++++++++++++++++++ .../NettyWebServerFactoryCustomizer.java | 35 ++++++++-- .../NettyWebServerFactoryCustomizerTests.java | 31 +++++++++ 3 files changed, 125 insertions(+), 6 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index 04c0d81a5a3..cd004a92c3c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -1396,6 +1396,31 @@ public class ServerProperties { */ private Duration connectionTimeout; + /** + * The maximum chunk size that can be decoded for the HTTP request. + */ + private DataSize maxChunkSize; + + /** + * The maximum length that can be decoded for the HTTP request's initial line. + */ + private DataSize maxInitialLineLength; + + /** + * Configure whether or not to validate headers when decoding requests. + */ + private Boolean validateHeaders; + + /** + * The maximum length of the content of the HTTP/2.0 clear-text upgrade request. + */ + private DataSize h2cMaxContentLength; + + /** + * The initial buffer size for HTTP request decoding. + */ + private DataSize initialBufferSize; + public Duration getConnectionTimeout() { return this.connectionTimeout; } @@ -1404,6 +1429,46 @@ public class ServerProperties { this.connectionTimeout = connectionTimeout; } + public DataSize getMaxChunkSize() { + return this.maxChunkSize; + } + + public void setMaxChunkSize(DataSize maxChunkSize) { + this.maxChunkSize = maxChunkSize; + } + + public DataSize getMaxInitialLineLength() { + return this.maxInitialLineLength; + } + + public void setMaxInitialLineLength(DataSize maxInitialLineLength) { + this.maxInitialLineLength = maxInitialLineLength; + } + + public Boolean getValidateHeaders() { + return this.validateHeaders; + } + + public void setValidateHeaders(Boolean validateHeaders) { + this.validateHeaders = validateHeaders; + } + + public DataSize getH2cMaxContentLength() { + return this.h2cMaxContentLength; + } + + public void setH2cMaxContentLength(DataSize h2cMaxContentLength) { + this.h2cMaxContentLength = h2cMaxContentLength; + } + + public DataSize getInitialBufferSize() { + return this.initialBufferSize; + } + + public void setInitialBufferSize(DataSize initialBufferSize) { + this.initialBufferSize = initialBufferSize; + } + } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java index f2478d8e2d9..4920057cdc6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java @@ -17,6 +17,8 @@ package org.springframework.boot.autoconfigure.web.embedded; import java.time.Duration; +import java.util.Objects; +import java.util.stream.Stream; import io.netty.channel.ChannelOption; @@ -27,7 +29,6 @@ import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory import org.springframework.boot.web.server.WebServerFactoryCustomizer; import org.springframework.core.Ordered; import org.springframework.core.env.Environment; -import org.springframework.util.unit.DataSize; /** * Customization for Netty-specific features. @@ -58,11 +59,16 @@ public class NettyWebServerFactoryCustomizer public void customize(NettyReactiveWebServerFactory factory) { factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders()); PropertyMapper propertyMapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); - propertyMapper.from(this.serverProperties::getMaxHttpHeaderSize) - .to((maxHttpRequestHeaderSize) -> customizeMaxHttpHeaderSize(factory, maxHttpRequestHeaderSize)); ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); propertyMapper.from(nettyProperties::getConnectionTimeout).whenNonNull() .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); + if (Stream + .of(this.serverProperties.getMaxHttpHeaderSize(), nettyProperties.getH2cMaxContentLength(), + nettyProperties.getMaxChunkSize(), nettyProperties.getMaxInitialLineLength(), + nettyProperties.getValidateHeaders(), nettyProperties.getInitialBufferSize()) + .anyMatch(Objects::nonNull)) { + customizeRequestDecoder(factory, propertyMapper); + } } private boolean getOrDeduceUseForwardHeaders() { @@ -73,9 +79,26 @@ public class NettyWebServerFactoryCustomizer return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } - private void customizeMaxHttpHeaderSize(NettyReactiveWebServerFactory factory, DataSize maxHttpHeaderSize) { - factory.addServerCustomizers((httpServer) -> httpServer.httpRequestDecoder( - (httpRequestDecoderSpec) -> httpRequestDecoderSpec.maxHeaderSize((int) maxHttpHeaderSize.toBytes()))); + private void customizeRequestDecoder(NettyReactiveWebServerFactory factory, PropertyMapper propertyMapper) { + factory.addServerCustomizers((httpServer) -> httpServer.httpRequestDecoder((httpRequestDecoderSpec) -> { + propertyMapper.from(this.serverProperties.getMaxHttpHeaderSize()).whenNonNull() + .to((maxHttpRequestHeader) -> httpRequestDecoderSpec + .maxHeaderSize((int) maxHttpRequestHeader.toBytes())); + ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); + propertyMapper.from(nettyProperties.getMaxChunkSize()).whenNonNull() + .to((maxChunkSize) -> httpRequestDecoderSpec.maxChunkSize((int) maxChunkSize.toBytes())); + propertyMapper.from(nettyProperties.getMaxInitialLineLength()).whenNonNull() + .to((maxInitialLineLength) -> httpRequestDecoderSpec + .maxInitialLineLength((int) maxInitialLineLength.toBytes())); + propertyMapper.from(nettyProperties.getH2cMaxContentLength()).whenNonNull() + .to((h2cMaxContentLength) -> httpRequestDecoderSpec + .h2cMaxContentLength((int) h2cMaxContentLength.toBytes())); + propertyMapper.from(nettyProperties.getInitialBufferSize()).whenNonNull().to( + (initialBufferSize) -> httpRequestDecoderSpec.initialBufferSize((int) initialBufferSize.toBytes())); + propertyMapper.from(nettyProperties.getValidateHeaders()).whenNonNull() + .to(httpRequestDecoderSpec::validateHeaders); + return httpRequestDecoderSpec; + })); } private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java index 4fb11185edf..cba78dc385f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.MockitoAnnotations; +import reactor.netty.http.server.HttpRequestDecoderSpec; import reactor.netty.http.server.HttpServer; import org.springframework.boot.autoconfigure.web.ServerProperties; @@ -33,6 +34,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyS import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory; import org.springframework.boot.web.embedded.netty.NettyServerCustomizer; import org.springframework.mock.env.MockEnvironment; +import org.springframework.util.unit.DataSize; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -107,6 +109,35 @@ class NettyWebServerFactoryCustomizerTests { verifyConnectionTimeout(factory, 1000); } + @Test + void setHttpRequestDecoder() { + ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); + nettyProperties.setValidateHeaders(true); + nettyProperties.setInitialBufferSize(DataSize.ofBytes(512)); + nettyProperties.setH2cMaxContentLength(DataSize.ofKilobytes(1)); + nettyProperties.setMaxChunkSize(DataSize.ofKilobytes(16)); + nettyProperties.setMaxInitialLineLength(DataSize.ofKilobytes(32)); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + verify(factory, times(1)).addServerCustomizers(this.customizerCaptor.capture()); + NettyServerCustomizer serverCustomizer = this.customizerCaptor.getValue(); + HttpServer httpServer = serverCustomizer.apply(HttpServer.create()); + HttpRequestDecoderSpec decoder = httpServer.configuration().decoder(); + assertThat(decoder.validateHeaders()).isTrue(); + assertThat(decoder.initialBufferSize()).isEqualTo(nettyProperties.getInitialBufferSize().toBytes()); + assertThat(decoder.h2cMaxContentLength()).isEqualTo(nettyProperties.getH2cMaxContentLength().toBytes()); + assertThat(decoder.maxChunkSize()).isEqualTo(nettyProperties.getMaxChunkSize().toBytes()); + assertThat(decoder.maxInitialLineLength()).isEqualTo(nettyProperties.getMaxInitialLineLength().toBytes()); + } + + @Test + void shouldNotSetAnyHttpRequestDecoderProperties() { + this.serverProperties.setMaxHttpHeaderSize(null); + NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); + this.customizer.customize(factory); + verify(factory, never()).addServerCustomizers(this.customizerCaptor.capture()); + } + private void verifyConnectionTimeout(NettyReactiveWebServerFactory factory, Integer expected) { if (expected == null) { verify(factory, never()).addServerCustomizers(any(NettyServerCustomizer.class)); From 0e8bf94289d20ff4fe10ef2867a0e6706e9f413f Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Fri, 17 Jul 2020 20:19:19 +0100 Subject: [PATCH 2/2] Polish "Add properties for Netty HttpDecoderSpec" See gh-22367 --- .../autoconfigure/web/ServerProperties.java | 56 +++++++++---------- .../NettyWebServerFactoryCustomizer.java | 22 +++----- .../web/ServerPropertiesTests.java | 31 ++++++++++ .../NettyWebServerFactoryCustomizerTests.java | 18 ++---- 4 files changed, 71 insertions(+), 56 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java index cd004a92c3c..9740226d8cf 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/ServerProperties.java @@ -1397,29 +1397,29 @@ public class ServerProperties { private Duration connectionTimeout; /** - * The maximum chunk size that can be decoded for the HTTP request. + * Maximum content length of an H2C upgrade request. */ - private DataSize maxChunkSize; + private DataSize h2cMaxContentLength = DataSize.ofBytes(0); /** - * The maximum length that can be decoded for the HTTP request's initial line. + * Initial buffer size for HTTP request decoding. */ - private DataSize maxInitialLineLength; + private DataSize initialBufferSize = DataSize.ofBytes(128); /** - * Configure whether or not to validate headers when decoding requests. + * Maximum chunk size that can be decoded for an HTTP request. */ - private Boolean validateHeaders; + private DataSize maxChunkSize = DataSize.ofKilobytes(8); /** - * The maximum length of the content of the HTTP/2.0 clear-text upgrade request. + * Maximum length that can be decoded for an HTTP request's initial line. */ - private DataSize h2cMaxContentLength; + private DataSize maxInitialLineLength = DataSize.ofKilobytes(4); /** - * The initial buffer size for HTTP request decoding. + * Whether to validate headers when decoding requests. */ - private DataSize initialBufferSize; + private boolean validateHeaders = true; public Duration getConnectionTimeout() { return this.connectionTimeout; @@ -1429,6 +1429,22 @@ public class ServerProperties { this.connectionTimeout = connectionTimeout; } + public DataSize getH2cMaxContentLength() { + return this.h2cMaxContentLength; + } + + public void setH2cMaxContentLength(DataSize h2cMaxContentLength) { + this.h2cMaxContentLength = h2cMaxContentLength; + } + + public DataSize getInitialBufferSize() { + return this.initialBufferSize; + } + + public void setInitialBufferSize(DataSize initialBufferSize) { + this.initialBufferSize = initialBufferSize; + } + public DataSize getMaxChunkSize() { return this.maxChunkSize; } @@ -1445,30 +1461,14 @@ public class ServerProperties { this.maxInitialLineLength = maxInitialLineLength; } - public Boolean getValidateHeaders() { + public boolean isValidateHeaders() { return this.validateHeaders; } - public void setValidateHeaders(Boolean validateHeaders) { + public void setValidateHeaders(boolean validateHeaders) { this.validateHeaders = validateHeaders; } - public DataSize getH2cMaxContentLength() { - return this.h2cMaxContentLength; - } - - public void setH2cMaxContentLength(DataSize h2cMaxContentLength) { - this.h2cMaxContentLength = h2cMaxContentLength; - } - - public DataSize getInitialBufferSize() { - return this.initialBufferSize; - } - - public void setInitialBufferSize(DataSize initialBufferSize) { - this.initialBufferSize = initialBufferSize; - } - } /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java index 4920057cdc6..2e807bd6f97 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizer.java @@ -17,8 +17,6 @@ package org.springframework.boot.autoconfigure.web.embedded; import java.time.Duration; -import java.util.Objects; -import java.util.stream.Stream; import io.netty.channel.ChannelOption; @@ -62,13 +60,7 @@ public class NettyWebServerFactoryCustomizer ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); propertyMapper.from(nettyProperties::getConnectionTimeout).whenNonNull() .to((connectionTimeout) -> customizeConnectionTimeout(factory, connectionTimeout)); - if (Stream - .of(this.serverProperties.getMaxHttpHeaderSize(), nettyProperties.getH2cMaxContentLength(), - nettyProperties.getMaxChunkSize(), nettyProperties.getMaxInitialLineLength(), - nettyProperties.getValidateHeaders(), nettyProperties.getInitialBufferSize()) - .anyMatch(Objects::nonNull)) { - customizeRequestDecoder(factory, propertyMapper); - } + customizeRequestDecoder(factory, propertyMapper); } private boolean getOrDeduceUseForwardHeaders() { @@ -79,6 +71,11 @@ public class NettyWebServerFactoryCustomizer return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE); } + private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { + factory.addServerCustomizers((httpServer) -> httpServer.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, + (int) connectionTimeout.toMillis())); + } + private void customizeRequestDecoder(NettyReactiveWebServerFactory factory, PropertyMapper propertyMapper) { factory.addServerCustomizers((httpServer) -> httpServer.httpRequestDecoder((httpRequestDecoderSpec) -> { propertyMapper.from(this.serverProperties.getMaxHttpHeaderSize()).whenNonNull() @@ -95,15 +92,10 @@ public class NettyWebServerFactoryCustomizer .h2cMaxContentLength((int) h2cMaxContentLength.toBytes())); propertyMapper.from(nettyProperties.getInitialBufferSize()).whenNonNull().to( (initialBufferSize) -> httpRequestDecoderSpec.initialBufferSize((int) initialBufferSize.toBytes())); - propertyMapper.from(nettyProperties.getValidateHeaders()).whenNonNull() + propertyMapper.from(nettyProperties.isValidateHeaders()).whenNonNull() .to(httpRequestDecoderSpec::validateHeaders); return httpRequestDecoderSpec; })); } - private void customizeConnectionTimeout(NettyReactiveWebServerFactory factory, Duration connectionTimeout) { - factory.addServerCustomizers((httpServer) -> httpServer.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, - (int) connectionTimeout.toMillis())); - } - } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java index c479305808f..cbc47a09e50 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/ServerPropertiesTests.java @@ -43,6 +43,8 @@ import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.util.thread.ThreadPool; import org.junit.jupiter.api.Test; +import reactor.netty.http.HttpDecoderSpec; +import reactor.netty.http.server.HttpRequestDecoderSpec; import org.springframework.boot.autoconfigure.web.ServerProperties.Tomcat.Accesslog; import org.springframework.boot.context.properties.bind.Bindable; @@ -533,6 +535,35 @@ class ServerPropertiesTests { .isEqualTo(UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); } + @Test + void nettyMaxChunkSizeMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getMaxChunkSize().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_MAX_CHUNK_SIZE); + } + + @Test + void nettyMaxInitialLineLenghtMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getMaxInitialLineLength().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_MAX_INITIAL_LINE_LENGTH); + } + + @Test + void nettyValidateHeadersMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().isValidateHeaders()).isEqualTo(HttpDecoderSpec.DEFAULT_VALIDATE_HEADERS); + } + + @Test + void nettyH2cMaxContentLengthMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getH2cMaxContentLength().toBytes()) + .isEqualTo(HttpRequestDecoderSpec.DEFAULT_H2C_MAX_CONTENT_LENGTH); + } + + @Test + void nettyInitialBufferSizeMatchesHttpDecoderSpecDefault() { + assertThat(this.properties.getNetty().getInitialBufferSize().toBytes()) + .isEqualTo(HttpDecoderSpec.DEFAULT_INITIAL_BUFFER_SIZE); + } + private Connector getDefaultConnector() throws Exception { return new Connector(TomcatServletWebServerFactory.DEFAULT_PROTOCOL); } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java index cba78dc385f..884861cb7b6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/web/embedded/NettyWebServerFactoryCustomizerTests.java @@ -110,9 +110,9 @@ class NettyWebServerFactoryCustomizerTests { } @Test - void setHttpRequestDecoder() { + void configureHttpRequestDecoder() { ServerProperties.Netty nettyProperties = this.serverProperties.getNetty(); - nettyProperties.setValidateHeaders(true); + nettyProperties.setValidateHeaders(false); nettyProperties.setInitialBufferSize(DataSize.ofBytes(512)); nettyProperties.setH2cMaxContentLength(DataSize.ofKilobytes(1)); nettyProperties.setMaxChunkSize(DataSize.ofKilobytes(16)); @@ -123,28 +123,20 @@ class NettyWebServerFactoryCustomizerTests { NettyServerCustomizer serverCustomizer = this.customizerCaptor.getValue(); HttpServer httpServer = serverCustomizer.apply(HttpServer.create()); HttpRequestDecoderSpec decoder = httpServer.configuration().decoder(); - assertThat(decoder.validateHeaders()).isTrue(); + assertThat(decoder.validateHeaders()).isFalse(); assertThat(decoder.initialBufferSize()).isEqualTo(nettyProperties.getInitialBufferSize().toBytes()); assertThat(decoder.h2cMaxContentLength()).isEqualTo(nettyProperties.getH2cMaxContentLength().toBytes()); assertThat(decoder.maxChunkSize()).isEqualTo(nettyProperties.getMaxChunkSize().toBytes()); assertThat(decoder.maxInitialLineLength()).isEqualTo(nettyProperties.getMaxInitialLineLength().toBytes()); } - @Test - void shouldNotSetAnyHttpRequestDecoderProperties() { - this.serverProperties.setMaxHttpHeaderSize(null); - NettyReactiveWebServerFactory factory = mock(NettyReactiveWebServerFactory.class); - this.customizer.customize(factory); - verify(factory, never()).addServerCustomizers(this.customizerCaptor.capture()); - } - private void verifyConnectionTimeout(NettyReactiveWebServerFactory factory, Integer expected) { if (expected == null) { verify(factory, never()).addServerCustomizers(any(NettyServerCustomizer.class)); return; } - verify(factory, times(1)).addServerCustomizers(this.customizerCaptor.capture()); - NettyServerCustomizer serverCustomizer = this.customizerCaptor.getValue(); + verify(factory, times(2)).addServerCustomizers(this.customizerCaptor.capture()); + NettyServerCustomizer serverCustomizer = this.customizerCaptor.getAllValues().get(0); HttpServer httpServer = serverCustomizer.apply(HttpServer.create()); Map, ?> options = httpServer.configuration().options(); assertThat(options.get(ChannelOption.CONNECT_TIMEOUT_MILLIS)).isEqualTo(expected);