Description
https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html documents server.connection-timeout
as "Time that connectors wait for another HTTP request before closing the connection. When not set, the connector's container-specific default is used. Use a value of -1 to indicate no (that is, an infinite) timeout".
Support for this property was added for Netty, in Issue #15368 / PR #15385
This was done using ChannelOption.CONNECT_TIMEOUT_MILLIS
, which does not implement the described functionality. As far as I can tell, CONNECT_TIMEOUT_MILLIS
is the timeout for the TCP connection handshake.
The correct way to implement the documented functionality (i.e. an idle/keep-alive timeout) appears to be using Netty's IdleStateHandler.
An example of doing this in a WebServerFactoryCustomizer
might look something like this:
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
@Configuration
public class NettyConfig {
@Bean
public WebServerFactoryCustomizer<NettyReactiveWebServerFactory> idleTimeoutCustomizer(
@Value("${server.netty.idle-timeout}") Duration idleTimeout
) {
return factory ->
factory.addServerCustomizers(server ->
server.tcpConfiguration(tcp ->
tcp.bootstrap(bootstrap -> bootstrap.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel channel) {
channel.pipeline().addLast(
new IdleStateHandler(0, 0, idleTimeout.toNanos(), NANOSECONDS) {
private final AtomicBoolean closed = new AtomicBoolean();
@Override
protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) {
if (closed.compareAndSet(false, true)) {
ctx.close();
}
}
}
);
}
}))));
}
}