uriTagValue,
boolean validate) {
return new Http3ServerConnectionHandler(
- new Http3Codec(accessLogEnabled, accessLog, compressionOptions, compressPredicate, decoder, encoder, formDecoderProvider, forwardedHeaderHandler,
- httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize,
- opsFactory, readTimeout, requestTimeout, uriTagValue, validate));
+ new Http3Codec(accessLogEnabled, accessLog, compressionOptions, compressPredicate, decoder, encoder, errorLogEnabled, errorLog,
+ formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder,
+ minCompressionSize, opsFactory, readTimeout, requestTimeout, uriTagValue, validate));
}
}
\ No newline at end of file
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java
index 36b32ae7a0..ff38cb99c8 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java
@@ -50,6 +50,10 @@
import reactor.netty.http.server.logging.AccessLog;
import reactor.netty.http.server.logging.AccessLogArgProvider;
import reactor.netty.http.server.logging.AccessLogFactory;
+import reactor.netty.http.server.logging.error.ErrorLog;
+import reactor.netty.http.server.logging.error.ErrorLogArgProvider;
+import reactor.netty.http.server.logging.error.ErrorLogEvent;
+import reactor.netty.http.server.logging.error.ErrorLogFactory;
import reactor.netty.internal.util.Metrics;
import reactor.netty.tcp.SslProvider;
import reactor.netty.tcp.TcpServer;
@@ -415,6 +419,77 @@ public final HttpServer cookieCodec(ServerCookieEncoder encoder, ServerCookieDec
return dup;
}
+ /**
+ * Enable or disable the error log. If enabled, the default log system will be used.
+ *
+ * Example:
+ *
+ * {@code
+ * HttpServer.create()
+ * .port(8080)
+ * .route(r -> r.get("/hello",
+ * (req, res) -> res.header(CONTENT_TYPE, TEXT_PLAIN)
+ * .sendString(Mono.just("Hello World!"))))
+ * .errorLog(true)
+ * .bindNow()
+ * .onDispose()
+ * .block();
+ * }
+ *
+ *
+ *
+ * Note that this method takes precedence over the {@value reactor.netty.ReactorNetty#ERROR_LOG_ENABLED} system property.
+ * By default, error logs are formatted as {@code [{datetime}] [pid {pid}] [client {remote address}] {error message}}.
+ *
+ * @param enable enable or disable the error log
+ * @return a new {@link HttpServer}
+ * @since 1.2.6
+ */
+ public final HttpServer errorLog(boolean enable) {
+ HttpServer dup = duplicate();
+ dup.configuration().errorLog = null;
+ dup.configuration().errorLogEnabled = enable;
+ return dup;
+ }
+
+ /**
+ * Enable or disable the error log and customize it through an {@link ErrorLogFactory}.
+ *
+ * Example:
+ *
+ * {@code
+ * HttpServer.create()
+ * .port(8080)
+ * .route(r -> r.get("/hello",
+ * (req, res) -> res.header(CONTENT_TYPE, TEXT_PLAIN)
+ * .sendString(Mono.just("Hello World!"))))
+ * .errorLog(true, ErrorLogFactory.createFilter(
+ * args -> args.cause() instanceof RuntimeException,
+ * args -> ErrorLog.create("host-name={}", args.httpServerInfos().hostName())))
+ * .bindNow()
+ * .onDispose()
+ * .block();
+ * }
+ *
+ *
+ * The {@link ErrorLogFactory} class offers several helper methods to generate such a function,
+ * notably if one wants to {@link ErrorLogFactory#createFilter(Predicate) filter} some exceptions out of the error log.
+ *
+ * Note that this method takes precedence over the {@value reactor.netty.ReactorNetty#ERROR_LOG_ENABLED} system property.
+ *
+ * @param enable enable or disable the error log
+ * @param errorLogFactory the {@link ErrorLogFactory} that creates an {@link ErrorLog} given an {@link ErrorLogArgProvider}
+ * @return a new {@link HttpServer}
+ * @since 1.2.6
+ */
+ public final HttpServer errorLog(boolean enable, ErrorLogFactory errorLogFactory) {
+ Objects.requireNonNull(errorLogFactory, "errorLogFactory");
+ HttpServer dup = duplicate();
+ dup.configuration().errorLog = enable ? errorLogFactory : null;
+ dup.configuration().errorLogEnabled = enable;
+ return dup;
+ }
+
/**
* Specifies a custom request handler for deriving information about the connection.
*
@@ -1250,6 +1325,7 @@ public void onStateChange(Connection connection, State newState) {
}
catch (Throwable t) {
log.error(format(connection.channel(), ""), t);
+ connection.channel().pipeline().fireUserEventTriggered(ErrorLogEvent.create(t));
//"FutureReturnValueIgnored" this is deliberate
connection.channel()
.close();
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 eb48ce095d..8949760048 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
@@ -75,6 +75,9 @@
import reactor.netty.http.server.logging.AccessLog;
import reactor.netty.http.server.logging.AccessLogArgProvider;
import reactor.netty.http.server.logging.AccessLogHandlerFactory;
+import reactor.netty.http.server.logging.error.DefaultErrorLogHandler;
+import reactor.netty.http.server.logging.error.ErrorLog;
+import reactor.netty.http.server.logging.error.ErrorLogArgProvider;
import reactor.netty.resources.LoopResources;
import reactor.netty.tcp.SslProvider;
import reactor.netty.transport.ServerTransportConfig;
@@ -322,6 +325,8 @@ public Function uriTagValue() {
ServerCookieDecoder cookieDecoder;
ServerCookieEncoder cookieEncoder;
HttpRequestDecoderSpec decoder;
+ boolean errorLogEnabled;
+ Function errorLog;
HttpServerFormDecoderProvider formDecoderProvider;
BiFunction forwardedHeaderHandler;
Http2SettingsSpec http2Settings;
@@ -366,6 +371,8 @@ public Function uriTagValue() {
this.cookieDecoder = parent.cookieDecoder;
this.cookieEncoder = parent.cookieEncoder;
this.decoder = parent.decoder;
+ this.errorLogEnabled = parent.errorLogEnabled;
+ this.errorLog = parent.errorLog;
this.formDecoderProvider = parent.formDecoderProvider;
this.forwardedHeaderHandler = parent.forwardedHeaderHandler;
this.http2Settings = parent.http2Settings;
@@ -499,6 +506,8 @@ static void addStreamHandlers(Channel ch,
@Nullable Boolean connectProtocolEnabled,
ServerCookieDecoder decoder,
ServerCookieEncoder encoder,
+ boolean errorLogEnabled,
+ @Nullable Function errorLog,
HttpServerFormDecoderProvider formDecoderProvider,
@Nullable BiFunction forwardedHeaderHandler,
HttpMessageLogFactory httpMessageLogFactory,
@@ -571,6 +580,10 @@ else if (metricsRecorder instanceof ContextAwareHttpServerMetricsRecorder) {
}
}
+ if (errorLogEnabled) {
+ pipeline.addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.ErrorLogHandler, new DefaultErrorLogHandler(errorLog));
+ }
+
if (log.isDebugEnabled()) {
log.debug(format(ch, "Initialized HTTP/2 stream pipeline {}"), pipeline);
}
@@ -616,6 +629,8 @@ static void configureHttp3Pipeline(
@Nullable BiPredicate compressPredicate,
ServerCookieDecoder cookieDecoder,
ServerCookieEncoder cookieEncoder,
+ boolean errorLogEnabled,
+ @Nullable Function errorLog,
HttpServerFormDecoderProvider formDecoderProvider,
@Nullable BiFunction forwardedHeaderHandler,
HttpMessageLogFactory httpMessageLogFactory,
@@ -631,10 +646,10 @@ static void configureHttp3Pipeline(
boolean validate) {
p.remove(NettyPipeline.ReactiveBridge);
- p.addLast(NettyPipeline.HttpCodec, newHttp3ServerConnectionHandler(accessLogEnabled, accessLog, compressionOptions, compressPredicate,
- cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory,
- listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, opsFactory, readTimeout,
- requestTimeout, uriTagValue, validate));
+ p.addLast(NettyPipeline.HttpCodec, newHttp3ServerConnectionHandler(accessLogEnabled, accessLog, compressionOptions,
+ compressPredicate, cookieDecoder, cookieEncoder, errorLogEnabled, errorLog, formDecoderProvider,
+ forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize,
+ opsFactory, readTimeout, requestTimeout, uriTagValue, validate));
if (metricsRecorder != null) {
// Connection metrics are not applicable
@@ -650,6 +665,8 @@ static void configureH2Pipeline(ChannelPipeline p,
ServerCookieDecoder cookieDecoder,
ServerCookieEncoder cookieEncoder,
boolean enableGracefulShutdown,
+ boolean errorLogEnabled,
+ @Nullable Function errorLog,
HttpServerFormDecoderProvider formDecoderProvider,
@Nullable BiFunction forwardedHeaderHandler,
@Nullable Http2SettingsSpec http2SettingsSpec,
@@ -696,7 +713,7 @@ static void configureH2Pipeline(ChannelPipeline p,
.addLast(NettyPipeline.H2MultiplexHandler,
new Http2MultiplexHandler(new H2Codec(accessLogEnabled, accessLog, compressionOptions, compressPredicate,
http2SettingsSpec != null ? http2SettingsSpec.connectProtocolEnabled() : null,
- cookieDecoder, cookieEncoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener,
+ cookieDecoder, cookieEncoder, errorLogEnabled, errorLog, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener,
mapHandle, methodTagValue, metricsRecorder, minCompressionSize, opsFactory, readTimeout, requestTimeout, uriTagValue)));
IdleTimeoutHandler.addIdleTimeoutHandler(p, idleTimeout);
@@ -721,6 +738,8 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p,
ServerCookieEncoder cookieEncoder,
HttpRequestDecoderSpec decoder,
boolean enableGracefulShutdown,
+ boolean errorLogEnabled,
+ @Nullable Function errorLog,
HttpServerFormDecoderProvider formDecoderProvider,
@Nullable BiFunction forwardedHeaderHandler,
@Nullable Http2SettingsSpec http2SettingsSpec,
@@ -747,10 +766,10 @@ static void configureHttp11OrH2CleartextPipeline(ChannelPipeline p,
HttpServerCodec httpServerCodec =
new HttpServerCodec(decoderConfig);
- Http11OrH2CleartextCodec upgrader = new Http11OrH2CleartextCodec(accessLogEnabled, accessLog, compressionOptions, compressPredicate,
- cookieDecoder, cookieEncoder, p.get(NettyPipeline.LoggingHandler) != null, enableGracefulShutdown, formDecoderProvider,
- forwardedHeaderHandler, http2SettingsSpec, httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder,
- minCompressionSize, opsFactory, readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders());
+ Http11OrH2CleartextCodec upgrader = new Http11OrH2CleartextCodec(accessLogEnabled, accessLog, compressionOptions,
+ compressPredicate, cookieDecoder, cookieEncoder, p.get(NettyPipeline.LoggingHandler) != null, enableGracefulShutdown,
+ errorLogEnabled, errorLog, formDecoderProvider, forwardedHeaderHandler, http2SettingsSpec, httpMessageLogFactory, listener, mapHandle,
+ methodTagValue, metricsRecorder, minCompressionSize, opsFactory, readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders());
ChannelHandler http2ServerHandler = new H2CleartextCodec(upgrader, http2SettingsSpec != null ? http2SettingsSpec.maxStreams() : null);
@@ -799,6 +818,10 @@ else if (metricsRecorder instanceof ContextAwareHttpServerMetricsRecorder) {
}
}
}
+
+ if (errorLogEnabled) {
+ p.addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.ErrorLogHandler, new DefaultErrorLogHandler(errorLog));
+ }
}
@SuppressWarnings("deprecation")
@@ -811,6 +834,8 @@ static void configureHttp11Pipeline(ChannelPipeline p,
ServerCookieEncoder cookieEncoder,
boolean channelOpened,
HttpRequestDecoderSpec decoder,
+ boolean errorLogEnabled,
+ @Nullable Function errorLog,
HttpServerFormDecoderProvider formDecoderProvider,
@Nullable BiFunction forwardedHeaderHandler,
HttpMessageLogFactory httpMessageLogFactory,
@@ -874,6 +899,10 @@ else if (metricsRecorder instanceof ContextAwareHttpServerMetricsRecorder) {
}
}
}
+
+ if (errorLogEnabled) {
+ p.addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.ErrorLogHandler, new DefaultErrorLogHandler(errorLog));
+ }
}
static final boolean ACCESS_LOG = Boolean.parseBoolean(System.getProperty(ACCESS_LOG_ENABLED, "false"));
@@ -1029,6 +1058,8 @@ static final class H2Codec extends ChannelInitializer {
final Boolean connectProtocolEnabled;
final ServerCookieDecoder cookieDecoder;
final ServerCookieEncoder cookieEncoder;
+ final boolean errorLogEnabled;
+ final Function errorLog;
final HttpServerFormDecoderProvider formDecoderProvider;
final BiFunction forwardedHeaderHandler;
final HttpMessageLogFactory httpMessageLogFactory;
@@ -1051,6 +1082,8 @@ static final class H2Codec extends ChannelInitializer {
@Nullable Boolean connectProtocolEnabled,
ServerCookieDecoder decoder,
ServerCookieEncoder encoder,
+ boolean errorLogEnabled,
+ @Nullable Function errorLog,
HttpServerFormDecoderProvider formDecoderProvider,
@Nullable BiFunction forwardedHeaderHandler,
HttpMessageLogFactory httpMessageLogFactory,
@@ -1070,6 +1103,8 @@ static final class H2Codec extends ChannelInitializer {
this.connectProtocolEnabled = connectProtocolEnabled;
this.cookieDecoder = decoder;
this.cookieEncoder = encoder;
+ this.errorLogEnabled = errorLogEnabled;
+ this.errorLog = errorLog;
this.formDecoderProvider = formDecoderProvider;
this.forwardedHeaderHandler = forwardedHeaderHandler;
this.httpMessageLogFactory = httpMessageLogFactory;
@@ -1088,8 +1123,8 @@ static final class H2Codec extends ChannelInitializer {
protected void initChannel(Channel ch) {
ch.pipeline().remove(this);
addStreamHandlers(ch, accessLogEnabled, accessLog, compressionOptions, compressPredicate, connectProtocolEnabled, cookieDecoder, cookieEncoder,
- formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, methodTagValue, metricsRecorder,
- minCompressionSize, opsFactory, readTimeout, requestTimeout, uriTagValue);
+ errorLogEnabled, errorLog, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle,
+ methodTagValue, metricsRecorder, minCompressionSize, opsFactory, readTimeout, requestTimeout, uriTagValue);
}
}
@@ -1103,6 +1138,8 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer
final Boolean connectProtocolEnabled;
final ServerCookieDecoder cookieDecoder;
final ServerCookieEncoder cookieEncoder;
+ final boolean errorLogEnabled;
+ final Function errorLog;
final HttpServerFormDecoderProvider formDecoderProvider;
final BiFunction forwardedHeaderHandler;
final Http2FrameCodec http2FrameCodec;
@@ -1128,6 +1165,8 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer
ServerCookieEncoder cookieEncoder,
boolean debug,
boolean enableGracefulShutdown,
+ boolean errorLogEnabled,
+ @Nullable Function errorLog,
HttpServerFormDecoderProvider formDecoderProvider,
@Nullable BiFunction forwardedHeaderHandler,
@Nullable Http2SettingsSpec http2SettingsSpec,
@@ -1149,6 +1188,8 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer
this.connectProtocolEnabled = http2SettingsSpec != null ? http2SettingsSpec.connectProtocolEnabled() : null;
this.cookieDecoder = cookieDecoder;
this.cookieEncoder = cookieEncoder;
+ this.errorLogEnabled = errorLogEnabled;
+ this.errorLog = errorLog;
this.formDecoderProvider = formDecoderProvider;
this.forwardedHeaderHandler = forwardedHeaderHandler;
Http2FrameCodecBuilder http2FrameCodecBuilder =
@@ -1191,9 +1232,9 @@ static final class Http11OrH2CleartextCodec extends ChannelInitializer
@Override
protected void initChannel(Channel ch) {
ch.pipeline().remove(this);
- addStreamHandlers(ch, accessLogEnabled, accessLog, compressionOptions, compressPredicate, connectProtocolEnabled, cookieDecoder, cookieEncoder,
- formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, methodTagValue,
- metricsRecorder, minCompressionSize, opsFactory, readTimeout, requestTimeout, uriTagValue);
+ addStreamHandlers(ch, accessLogEnabled, accessLog, compressionOptions, compressPredicate, connectProtocolEnabled, cookieDecoder,
+ cookieEncoder, errorLogEnabled, errorLog, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory,
+ listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, opsFactory, readTimeout, requestTimeout, uriTagValue);
}
@Override
@@ -1241,6 +1282,8 @@ static final class H2OrHttp11Codec extends ApplicationProtocolNegotiationHandler
final ServerCookieEncoder cookieEncoder;
final HttpRequestDecoderSpec decoder;
final boolean enableGracefulShutdown;
+ final boolean errorLogEnabled;
+ final Function errorLog;
final HttpServerFormDecoderProvider formDecoderProvider;
final BiFunction forwardedHeaderHandler;
final Http2SettingsSpec http2SettingsSpec;
@@ -1273,6 +1316,8 @@ static final class H2OrHttp11Codec extends ApplicationProtocolNegotiationHandler
this.cookieEncoder = initializer.cookieEncoder;
this.decoder = initializer.decoder;
this.enableGracefulShutdown = initializer.enableGracefulShutdown;
+ this.errorLogEnabled = initializer.errorLogEnabled;
+ this.errorLog = initializer.errorLog;
this.formDecoderProvider = initializer.formDecoderProvider;
this.forwardedHeaderHandler = initializer.forwardedHeaderHandler;
this.http2SettingsSpec = initializer.http2SettingsSpec;
@@ -1301,16 +1346,17 @@ protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
configureH2Pipeline(p, accessLogEnabled, accessLog, compressionOptions, compressPredicate, cookieDecoder, cookieEncoder,
- enableGracefulShutdown, formDecoderProvider, forwardedHeaderHandler, http2SettingsSpec, httpMessageLogFactory, idleTimeout,
- listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, opsFactory, readTimeout, requestTimeout,
- uriTagValue, decoder.validateHeaders());
+ enableGracefulShutdown, errorLogEnabled, errorLog, formDecoderProvider, forwardedHeaderHandler, http2SettingsSpec,
+ httpMessageLogFactory, idleTimeout, listener, mapHandle, methodTagValue, metricsRecorder, minCompressionSize, opsFactory,
+ readTimeout, requestTimeout, uriTagValue, decoder.validateHeaders());
return;
}
if (!supportOnlyHttp2 && ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
- configureHttp11Pipeline(p, accessLogEnabled, accessLog, compressionOptions, compressPredicate, cookieDecoder, cookieEncoder, true,
- decoder, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, idleTimeout, listener,
- mapHandle, maxKeepAliveRequests, methodTagValue, metricsRecorder, minCompressionSize, readTimeout, requestTimeout, uriTagValue);
+ configureHttp11Pipeline(p, accessLogEnabled, accessLog, compressionOptions, compressPredicate, cookieDecoder, cookieEncoder,
+ true, decoder, errorLogEnabled, errorLog, formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory,
+ idleTimeout, listener, mapHandle, maxKeepAliveRequests, methodTagValue, metricsRecorder, minCompressionSize, readTimeout,
+ requestTimeout, uriTagValue);
// When the server is configured with HTTP/1.1 and H2 and HTTP/1.1 is negotiated,
// when channelActive event happens, this HttpTrafficHandler is still not in the pipeline,
@@ -1333,6 +1379,8 @@ static final class HttpServerChannelInitializer implements ChannelPipelineConfig
final ServerCookieEncoder cookieEncoder;
final HttpRequestDecoderSpec decoder;
final boolean enableGracefulShutdown;
+ final boolean errorLogEnabled;
+ final Function errorLog;
final HttpServerFormDecoderProvider formDecoderProvider;
final BiFunction forwardedHeaderHandler;
final Http2SettingsSpec http2SettingsSpec;
@@ -1362,6 +1410,8 @@ static final class HttpServerChannelInitializer implements ChannelPipelineConfig
this.cookieEncoder = config.cookieEncoder;
this.decoder = config.decoder;
this.enableGracefulShutdown = config.channelGroup() != null;
+ this.errorLogEnabled = config.errorLogEnabled;
+ this.errorLog = config.errorLog;
this.formDecoderProvider = config.formDecoderProvider;
this.forwardedHeaderHandler = config.forwardedHeaderHandler;
this.http2SettingsSpec = config.http2Settings;
@@ -1417,6 +1467,8 @@ else if ((protocols & h11) == h11) {
cookieEncoder,
false,
decoder,
+ errorLogEnabled,
+ errorLog,
formDecoderProvider,
forwardedHeaderHandler,
httpMessageLogFactory,
@@ -1449,6 +1501,8 @@ else if ((protocols & h2) == h2) {
cookieDecoder,
cookieEncoder,
enableGracefulShutdown,
+ errorLogEnabled,
+ errorLog,
formDecoderProvider,
forwardedHeaderHandler,
http2SettingsSpec,
@@ -1475,6 +1529,8 @@ else if ((protocols & h3) == h3) {
compressPredicate(compressPredicate, minCompressionSize),
cookieDecoder,
cookieEncoder,
+ errorLogEnabled,
+ errorLog,
formDecoderProvider,
forwardedHeaderHandler,
httpMessageLogFactory,
@@ -1502,6 +1558,8 @@ else if ((protocols & h3) == h3) {
cookieEncoder,
decoder,
enableGracefulShutdown,
+ errorLogEnabled,
+ errorLog,
formDecoderProvider,
forwardedHeaderHandler,
http2SettingsSpec,
@@ -1529,6 +1587,8 @@ else if ((protocols & h11) == h11) {
cookieEncoder,
false,
decoder,
+ errorLogEnabled,
+ errorLog,
formDecoderProvider,
forwardedHeaderHandler,
httpMessageLogFactory,
@@ -1553,6 +1613,8 @@ else if ((protocols & h2c) == h2c) {
cookieDecoder,
cookieEncoder,
enableGracefulShutdown,
+ errorLogEnabled,
+ errorLog,
formDecoderProvider,
forwardedHeaderHandler,
http2SettingsSpec,
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerFormDecoderProvider.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerFormDecoderProvider.java
index ddfa32cc58..d101cf9e02 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerFormDecoderProvider.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerFormDecoderProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021-2023 VMware, Inc. or its affiliates, All Rights Reserved.
+ * Copyright (c) 2021-2025 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.
@@ -395,7 +395,7 @@ public List currentHttpData(boolean onlyCompleted) {
public void destroy() {
super.destroy();
InterfaceHttpData partial = currentPartialHttpData();
- if (partial != null) {
+ if (partial != null && partial.refCnt() > 0) {
partial.release();
}
}
@@ -452,7 +452,7 @@ public List currentHttpData(boolean onlyCompleted) {
public void destroy() {
super.destroy();
InterfaceHttpData partial = currentPartialHttpData();
- if (partial != null) {
+ if (partial != null && partial.refCnt() > 0) {
partial.release();
}
}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java
index 826c097300..41ba5bd7f8 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerOperations.java
@@ -95,6 +95,7 @@
import reactor.netty.http.logging.HttpMessageArgProviderFactory;
import reactor.netty.http.logging.HttpMessageLogFactory;
import reactor.netty.http.server.compression.HttpCompressionOptionsSpec;
+import reactor.netty.http.server.logging.error.ErrorLogEvent;
import reactor.netty.http.websocket.WebsocketInbound;
import reactor.netty.http.websocket.WebsocketOutbound;
import reactor.util.Logger;
@@ -1183,6 +1184,7 @@ else if (cause instanceof TooLongHttpHeaderException) {
*/
@Override
protected void onOutboundError(Throwable err) {
+ channel().pipeline().fireUserEventTriggered(ErrorLogEvent.create(err));
if (!channel().isActive()) {
super.onOutboundError(err);
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/DeflateOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/DeflateOption.java
index 8fcb6ea8b1..de7e81f3bf 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/DeflateOption.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/DeflateOption.java
@@ -86,10 +86,11 @@ public interface Builder {
}
private static final class Build implements Builder {
+ static final io.netty.handler.codec.compression.DeflateOptions DEFAULT = StandardCompressionOptions.deflate();
- private int compressionLevel = 6;
- private int memoryLevel = 8;
- private int windowBits = 12;
+ private int compressionLevel = DEFAULT.compressionLevel();
+ private int memoryLevel = DEFAULT.memLevel();
+ private int windowBits = DEFAULT.windowBits();
@Override
public DeflateOption build() {
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/GzipOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/GzipOption.java
index a7f6dfb3dd..7c749dd431 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/GzipOption.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/GzipOption.java
@@ -86,10 +86,11 @@ public interface Builder {
}
private static final class Build implements Builder {
+ static final io.netty.handler.codec.compression.GzipOptions DEFAULT = StandardCompressionOptions.gzip();
- private int compressionLevel = 6;
- private int memoryLevel = 8;
- private int windowBits = 12;
+ private int compressionLevel = DEFAULT.compressionLevel();
+ private int memoryLevel = DEFAULT.memLevel();
+ private int windowBits = DEFAULT.windowBits();
@Override
public GzipOption build() {
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/ZstdOption.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/ZstdOption.java
index 656cc0444c..d1da037dc1 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/ZstdOption.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/compression/ZstdOption.java
@@ -91,10 +91,11 @@ public interface Builder {
}
private static final class Build implements Builder {
+ static final io.netty.handler.codec.compression.ZstdOptions DEFAULT = StandardCompressionOptions.zstd();
- private int blockSize = 1 << 16; // 64KB
- private int compressionLevel = 3;
- private int maxEncodeSize = 1 << (compressionLevel + 7 + 0x0F); // 32MB
+ private int blockSize = DEFAULT.blockSize();
+ private int compressionLevel = DEFAULT.compressionLevel();
+ private int maxEncodeSize = DEFAULT.maxEncodeSize();
@Override
public ZstdOption build() {
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLog.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLog.java
index d2d691efee..c175bf780f 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLog.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLog.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020-2021 VMware, Inc. or its affiliates, All Rights Reserved.
+ * Copyright (c) 2020-2025 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.
@@ -29,14 +29,14 @@
* @author limaoning
* @since 1.0.1
*/
-public final class AccessLog {
+public class AccessLog {
static final Logger LOG = Loggers.getLogger("reactor.netty.http.server.AccessLog");
final String logFormat;
final Object[] args;
- private AccessLog(String logFormat, Object... args) {
+ protected AccessLog(String logFormat, Object... args) {
Objects.requireNonNull(logFormat, "logFormat");
this.logFormat = logFormat;
this.args = args;
@@ -46,7 +46,7 @@ public static AccessLog create(String logFormat, Object... args) {
return new AccessLog(logFormat, args);
}
- void log() {
+ protected void log() {
if (LOG.isInfoEnabled()) {
LOG.info(logFormat, args);
}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProvider.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProvider.java
index 93db057e3e..793bb7ae6e 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProvider.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved.
+ * Copyright (c) 2020-2025 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.
@@ -21,6 +21,7 @@
import java.net.SocketAddress;
import java.time.ZonedDateTime;
+import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.function.BiFunction;
@@ -160,4 +161,26 @@ public interface AccessLogArgProvider {
*/
@Nullable
Map> cookies();
+
+ /**
+ * Returns an iterator over all request headers.
+ *
+ * @return an iterator over all request headers or {@code null} if request is not available
+ * @since 1.2.6
+ */
+ @Nullable
+ default Iterator> requestHeaderIterator() {
+ return null;
+ }
+
+ /**
+ * Returns an iterator over all response headers.
+ *
+ * @return an iterator over all response headers or {@code null} if response is not available
+ * @since 1.2.6
+ */
+ @Nullable
+ default Iterator> responseHeaderIterator() {
+ return null;
+ }
}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH1.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH1.java
index e362a9948f..56e9d7a41e 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH1.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH1.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020-2024 VMware, Inc. or its affiliates, All Rights Reserved.
+ * Copyright (c) 2020-2025 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.
@@ -20,6 +20,8 @@
import reactor.util.annotation.Nullable;
import java.net.SocketAddress;
+import java.util.Iterator;
+import java.util.Map;
import java.util.Objects;
/**
@@ -73,6 +75,18 @@ public CharSequence responseHeader(CharSequence name) {
return response == null ? null : response.headers().get(name);
}
+ @Override
+ @Nullable
+ public Iterator> requestHeaderIterator() {
+ return request == null ? null : request.requestHeaders().iteratorCharSequence();
+ }
+
+ @Override
+ @Nullable
+ public Iterator> responseHeaderIterator() {
+ return response == null ? null : response.headers().iteratorCharSequence();
+ }
+
@Override
void onRequest() {
if (request != null) {
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH2.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH2.java
index 7ca51b7a1a..9ad8bfd279 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH2.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH2.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved.
+ * Copyright (c) 2020-2025 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.
@@ -19,6 +19,8 @@
import reactor.util.annotation.Nullable;
import java.net.SocketAddress;
+import java.util.Iterator;
+import java.util.Map;
import java.util.Objects;
/**
@@ -68,6 +70,18 @@ public CharSequence responseHeader(CharSequence name) {
return responseHeaders == null ? null : responseHeaders.headers().get(name);
}
+ @Override
+ @Nullable
+ public Iterator> requestHeaderIterator() {
+ return requestHeaders == null ? null : requestHeaders.headers().iterator();
+ }
+
+ @Override
+ @Nullable
+ public Iterator> responseHeaderIterator() {
+ return responseHeaders == null ? null : responseHeaders.headers().iterator();
+ }
+
@Override
void onRequest() {
super.onRequest();
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH3.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH3.java
index ac33fcae36..d74cb2ca13 100644
--- a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH3.java
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/AccessLogArgProviderH3.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2024 VMware, Inc. or its affiliates, All Rights Reserved.
+ * Copyright (c) 2024-2025 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.
@@ -19,6 +19,8 @@
import reactor.util.annotation.Nullable;
import java.net.SocketAddress;
+import java.util.Iterator;
+import java.util.Map;
import java.util.Objects;
final class AccessLogArgProviderH3 extends AbstractAccessLogArgProvider {
@@ -63,6 +65,18 @@ public CharSequence responseHeader(CharSequence name) {
return responseHeaders == null ? null : responseHeaders.headers().get(name);
}
+ @Override
+ @Nullable
+ public Iterator> requestHeaderIterator() {
+ return requestHeaders == null ? null : requestHeaders.headers().iterator();
+ }
+
+ @Override
+ @Nullable
+ public Iterator> responseHeaderIterator() {
+ return responseHeaders == null ? null : responseHeaders.headers().iterator();
+ }
+
@Override
void onRequest() {
super.onRequest();
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/AbstractErrorLogArgProvider.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/AbstractErrorLogArgProvider.java
new file mode 100644
index 0000000000..71ff36278b
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/AbstractErrorLogArgProvider.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+import reactor.util.annotation.Nullable;
+
+import java.net.SocketAddress;
+import java.util.function.Supplier;
+
+/**
+ * A provider of the args required for error log.
+ *
+ * @author raccoonback
+ * @author Violeta Georgieva
+ */
+abstract class AbstractErrorLogArgProvider>
+ implements ErrorLogArgProvider, Supplier {
+
+ final SocketAddress remoteAddress;
+
+ AbstractErrorLogArgProvider(@Nullable SocketAddress remoteAddress) {
+ this.remoteAddress = remoteAddress;
+ }
+
+ @Override
+ @Nullable
+ public SocketAddress remoteAddress() {
+ return remoteAddress;
+ }
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/BaseErrorLogHandler.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/BaseErrorLogHandler.java
new file mode 100644
index 0000000000..2f4e1c34f8
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/BaseErrorLogHandler.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+import io.netty.channel.ChannelDuplexHandler;
+import reactor.util.annotation.Nullable;
+
+import java.lang.management.ManagementFactory;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.time.format.DateTimeFormatter;
+import java.util.function.Function;
+
+class BaseErrorLogHandler extends ChannelDuplexHandler {
+
+ static String PID;
+ static {
+ String jvmName = ManagementFactory.getRuntimeMXBean().getName();
+ int index = jvmName.indexOf('@');
+ if (index != -1) {
+ PID = jvmName.substring(0, index);
+ }
+ else {
+ PID = jvmName;
+ }
+ }
+
+ static final String DEFAULT_LOG_FORMAT = "[{}] [pid " + PID + "] [client {}] {}";
+ static final DateTimeFormatter DATE_TIME_FORMATTER =
+ DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssZ");
+ static final String MISSING = "-";
+
+ static final Function DEFAULT_ERROR_LOG =
+ args -> ErrorLog.create(
+ DEFAULT_LOG_FORMAT,
+ args.errorDateTime().format(DATE_TIME_FORMATTER),
+ refinedRemoteAddress(args.remoteAddress()),
+ refinedExceptionMessage(args.cause()));
+
+ final Function errorLog;
+
+ BaseErrorLogHandler(@Nullable Function errorLog) {
+ this.errorLog = errorLog == null ? DEFAULT_ERROR_LOG : errorLog;
+ }
+
+ private static String refinedRemoteAddress(@Nullable SocketAddress remoteAddress) {
+ if (remoteAddress instanceof InetSocketAddress) {
+ return ((InetSocketAddress) remoteAddress).getHostString();
+ }
+
+ return MISSING;
+ }
+
+ private static String refinedExceptionMessage(Throwable throwable) {
+ String error = throwable.getClass().getName();
+ String message = throwable.getLocalizedMessage();
+ return message == null ? error : error + "." + message;
+ }
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLog.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLog.java
new file mode 100644
index 0000000000..24589e625e
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLog.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+import reactor.util.Logger;
+import reactor.util.Loggers;
+
+import java.util.Objects;
+
+/**
+ * Log the http default error information into a Logger named {@code reactor.netty.http.server.ErrorLog} at {@code ERROR} level.
+ *
+ * See {@link ErrorLogFactory} for convenience methods to create an error log factory to be passed to
+ * {@link reactor.netty.http.server.HttpServer#errorLog(boolean, ErrorLogFactory)} during server configuration.
+ *
+ * @author raccoonback
+ * @author Violeta Georgieva
+ */
+final class DefaultErrorLog implements ErrorLog {
+
+ static final Logger LOGGER = Loggers.getLogger("reactor.netty.http.server.ErrorLog");
+
+ final String logFormat;
+ final Object[] args;
+
+ DefaultErrorLog(String logFormat, Object... args) {
+ Objects.requireNonNull(logFormat, "logFormat");
+ this.logFormat = logFormat;
+ this.args = args;
+ }
+
+ @Override
+ public void log() {
+ if (LOGGER.isErrorEnabled()) {
+ LOGGER.error(logFormat, args);
+ }
+ }
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogArgProvider.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogArgProvider.java
new file mode 100644
index 0000000000..e459b82349
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogArgProvider.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+import io.netty.channel.Channel;
+import reactor.netty.ReactorNetty;
+import reactor.netty.channel.ChannelOperations;
+import reactor.netty.http.server.HttpServerInfos;
+import reactor.util.annotation.Nullable;
+
+import java.net.SocketAddress;
+import java.time.ZonedDateTime;
+
+/**
+ * A default implementation of the args required for error log.
+ *
+ * @author raccoonback
+ */
+final class DefaultErrorLogArgProvider extends AbstractErrorLogArgProvider {
+
+ private Throwable cause;
+ private ZonedDateTime errorDateTime;
+ private HttpServerInfos httpServerInfos;
+
+ DefaultErrorLogArgProvider(@Nullable SocketAddress remoteAddress) {
+ super(remoteAddress);
+ }
+
+ @Override
+ public Throwable cause() {
+ return cause;
+ }
+
+ @Override
+ public ZonedDateTime errorDateTime() {
+ return errorDateTime;
+ }
+
+ @Override
+ public DefaultErrorLogArgProvider get() {
+ return this;
+ }
+
+ @Override
+ @Nullable
+ public HttpServerInfos httpServerInfos() {
+ return httpServerInfos;
+ }
+
+ void clear() {
+ cause = null;
+ errorDateTime = null;
+ httpServerInfos = null;
+ }
+
+ void applyConnectionInfo(Channel channel) {
+ ChannelOperations, ?> ops = ChannelOperations.get(channel);
+ if (ops instanceof HttpServerInfos) {
+ this.httpServerInfos = (HttpServerInfos) ops;
+ }
+ }
+
+ void applyThrowable(Throwable cause) {
+ this.cause = cause;
+ this.errorDateTime = ZonedDateTime.now(ReactorNetty.ZONE_ID_SYSTEM);
+ }
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogEvent.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogEvent.java
new file mode 100644
index 0000000000..0d28d23dd8
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogEvent.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+/**
+ * Provide a default implementation of an error logging event for UserEvent delivery.
+ *
+ * @author raccoonback
+ */
+final class DefaultErrorLogEvent implements ErrorLogEvent {
+
+ private final Throwable throwable;
+
+ DefaultErrorLogEvent(Throwable throwable) {
+ this.throwable = throwable;
+ }
+
+ @Override
+ public Throwable cause() {
+ return throwable;
+ }
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogHandler.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogHandler.java
new file mode 100644
index 0000000000..75cc631fe4
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/DefaultErrorLogHandler.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+import io.netty.channel.ChannelHandlerContext;
+import reactor.netty.channel.ChannelOperations;
+import reactor.netty.http.server.HttpServerInfos;
+import reactor.util.annotation.Nullable;
+
+import java.net.SocketAddress;
+import java.util.function.Function;
+
+/**
+ * Handler for logging errors that occur in the HTTP Server.
+ *
+ * @author raccoonback
+ * @since 1.2.6
+ */
+public final class DefaultErrorLogHandler extends BaseErrorLogHandler {
+
+ private DefaultErrorLogArgProvider errorLogArgProvider;
+
+ public DefaultErrorLogHandler(@Nullable Function errorLog) {
+ super(errorLog);
+ }
+
+ @Override
+ public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+ ErrorLog log;
+
+ if (errorLogArgProvider == null) {
+ ChannelOperations, ?> ops = ChannelOperations.get(ctx.channel());
+ SocketAddress remoteAddress = ops instanceof HttpServerInfos ?
+ ((HttpServerInfos) ops).connectionRemoteAddress() :
+ ctx.channel().remoteAddress();
+ errorLogArgProvider = new DefaultErrorLogArgProvider(remoteAddress);
+ }
+ else {
+ errorLogArgProvider.clear();
+ }
+
+ errorLogArgProvider.applyConnectionInfo(ctx.channel());
+ errorLogArgProvider.applyThrowable(cause);
+
+ log = errorLog.apply(errorLogArgProvider);
+ if (log != null) {
+ log.log();
+ }
+
+ ctx.fireExceptionCaught(cause);
+ }
+
+ @Override
+ public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
+ if (evt instanceof DefaultErrorLogEvent) {
+ exceptionCaught(ctx, ((DefaultErrorLogEvent) evt).cause());
+ }
+
+ ctx.fireUserEventTriggered(evt);
+ }
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLog.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLog.java
new file mode 100644
index 0000000000..0ad31b9fd5
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLog.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+/**
+ * Represents a log entry for HTTP server errors.
+ * Implementations of this interface define how the error information is logged.
+ *
+ * @author raccoonback
+ * @author Violeta Georgieva
+ * @since 1.2.6
+ */
+public interface ErrorLog {
+
+ /**
+ * Creates a default {@code ErrorLog} with the given log format and arguments.
+ *
+ * @param logFormat the log format string
+ * @param args the list of arguments
+ * @return a new {@link DefaultErrorLog}
+ * @see DefaultErrorLog
+ */
+ static ErrorLog create(String logFormat, Object... args) {
+ return new DefaultErrorLog(logFormat, args);
+ }
+
+ /**
+ * Logs the error information.
+ */
+ void log();
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogArgProvider.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogArgProvider.java
new file mode 100644
index 0000000000..64e19812de
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogArgProvider.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+import reactor.netty.http.server.ConnectionInformation;
+import reactor.netty.http.server.HttpServerInfos;
+import reactor.util.annotation.Nullable;
+
+import java.net.SocketAddress;
+import java.time.ZonedDateTime;
+import java.util.function.BiFunction;
+
+/**
+ * A provider of the args required for error log.
+ *
+ * @author raccoonback
+ * @since 1.2.6
+ */
+public interface ErrorLogArgProvider {
+
+ /**
+ * Returns the date-time of the moment when the exception occurred.
+ *
+ * @return zoned date-time
+ */
+ ZonedDateTime errorDateTime();
+
+ /**
+ * Returns the address of the remote peer or possibly {@code null} in case of Unix Domain Sockets.
+ *
+ * @return the peer's address
+ */
+ @Nullable
+ SocketAddress remoteAddress();
+
+ /**
+ * Returns information about the HTTP server-side connection information.
+ * Note that the {@link ConnectionInformation#remoteAddress()} will return the forwarded
+ * remote client address if the server is configured in forwarded mode.
+ *
+ * @return HTTP server-side connection information
+ * @see reactor.netty.http.server.HttpServer#forwarded(BiFunction)
+ */
+ @Nullable
+ HttpServerInfos httpServerInfos();
+
+ /**
+ * Returns the exception that occurred.
+ *
+ * @return exception
+ */
+ Throwable cause();
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogEvent.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogEvent.java
new file mode 100644
index 0000000000..85863b700e
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogEvent.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+/**
+ * Define an interface to handle error log events propagated through UserEvent.
+ *
+ * @author raccoonback
+ * @author Violeta Georgieva
+ * @since 1.2.6
+ */
+public interface ErrorLogEvent {
+
+ /**
+ * Creates a default {@code ErrorLogEvent} with the given throwable.
+ *
+ * @param t the throwable that occurred
+ * @return a new {@link DefaultErrorLogEvent}
+ * @see DefaultErrorLogEvent
+ */
+ static ErrorLogEvent create(Throwable t) {
+ return new DefaultErrorLogEvent(t);
+ }
+
+ /**
+ * Returns the throwable that occurred.
+ *
+ * @return the throwable that occurred
+ */
+ Throwable cause();
+}
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogFactory.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogFactory.java
new file mode 100644
index 0000000000..218424406c
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/ErrorLogFactory.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.netty.http.server.logging.error;
+
+import java.util.function.Function;
+import java.util.function.Predicate;
+
+/**
+ * An interface to declare more concisely a {@link Function} that apply an {@link ErrorLog} by an
+ * {@link ErrorLogArgProvider}.
+ *
+ * Can be used in {@link reactor.netty.http.server.HttpServer#errorLog(boolean, ErrorLogFactory) errorLog} method for example.
+ *
+ * @author raccoonback
+ * @since 1.2.6
+ */
+public interface ErrorLogFactory extends Function {
+
+ /**
+ * Helper method to create an error log factory that selectively enables error logs.
+ *
+ * Any exception (represented as an {@link ErrorLogArgProvider}) that doesn't match the
+ * provided {@link Predicate} is excluded from the error log. Other exceptions are logged
+ * using the default format.
+ *
+ * @param predicate the filter that returns {@code true} if the exception should be logged, {@code false} otherwise
+ * @return an {@link ErrorLogFactory} to be used in
+ * {@link reactor.netty.http.server.HttpServer#errorLog(boolean, ErrorLogFactory)}
+ */
+ static ErrorLogFactory createFilter(Predicate predicate) {
+ return input -> predicate.test(input) ? BaseErrorLogHandler.DEFAULT_ERROR_LOG.apply(input) : null;
+ }
+
+ /**
+ * Helper method to create an error log factory that selectively enables error logs and customizes
+ * the format to apply.
+ *
+ * Any exception (represented as an {@link ErrorLogArgProvider}) that doesn't match the
+ * provided {@link Predicate} is excluded from the error log. Other exceptions are logged
+ * using the provided formatting {@link Function}.
+ * Create an {@link ErrorLog} instance by defining both the String format and a vararg of the relevant arguments,
+ * extracted from the {@link ErrorLogArgProvider}.
+ *
+ *
+ * @param predicate the filter that returns {@code true} if the exception should be logged, {@code false} otherwise
+ * @param formatFunction the {@link ErrorLogFactory} that creates {@link ErrorLog} instances, encapsulating the
+ * format and the extraction of relevant arguments
+ * @return an {@link ErrorLogFactory} to be used in
+ * {@link reactor.netty.http.server.HttpServer#errorLog(boolean, ErrorLogFactory)}
+ */
+ static ErrorLogFactory createFilter(Predicate predicate, ErrorLogFactory formatFunction) {
+ return input -> predicate.test(input) ? formatFunction.apply(input) : null;
+ }
+
+}
\ No newline at end of file
diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/package-info.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/package-info.java
new file mode 100644
index 0000000000..4e7ef5fa9f
--- /dev/null
+++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/logging/error/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2025 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.
+ * You may obtain a copy of the License at
+ *
+ * https://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.
+ */
+
+/**
+ * Http error log.
+ */
+@NonNullApi
+package reactor.netty.http.server.logging.error;
+
+import reactor.util.annotation.NonNullApi;
\ No newline at end of file
diff --git a/reactor-netty-http/src/main/resources/META-INF/native-image/io.projectreactor.netty/reactor-netty-http/reflect-config.json b/reactor-netty-http/src/main/resources/META-INF/native-image/io.projectreactor.netty/reactor-netty-http/reflect-config.json
index 6fddd1a5c1..65c4c74cdd 100644
--- a/reactor-netty-http/src/main/resources/META-INF/native-image/io.projectreactor.netty/reactor-netty-http/reflect-config.json
+++ b/reactor-netty-http/src/main/resources/META-INF/native-image/io.projectreactor.netty/reactor-netty-http/reflect-config.json
@@ -327,5 +327,19 @@
},
"name": "reactor.netty.http.server.logging.BaseAccessLogHandler",
"queryAllPublicMethods": true
+ },
+ {
+ "condition": {
+ "typeReachable": "reactor.netty.http.server.logging.error.DefaultErrorLogHandler"
+ },
+ "name": "reactor.netty.http.server.logging.error.DefaultErrorLogHandler",
+ "queryAllPublicMethods": true
+ },
+ {
+ "condition": {
+ "typeReachable": "reactor.netty.http.server.logging.error.BaseErrorLogHandler"
+ },
+ "name": "reactor.netty.http.server.logging.error.BaseErrorLogHandler",
+ "queryAllPublicMethods": true
}
]
\ No newline at end of file
diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java b/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java
index 256c0ea79b..608adb0743 100644
--- a/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java
+++ b/reactor-netty-http/src/test/java/reactor/netty/http/HttpCompressionClientServerTests.java
@@ -755,6 +755,7 @@ void serverCompressionWithCompressionLevelSettings(HttpServer server, HttpClient
byte[] result = new byte[encodedByteBuf.readableBytes()];
encodedByteBuf.getBytes(encodedByteBuf.readerIndex(), result);
+ encodedByteBuf.release();
assertThat(resp).isNotNull();
assertThat(resp).startsWith(result); // Ignore the original data size and crc checksum comparison
diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/client/ConnectionPoolTests.java b/reactor-netty-http/src/test/java/reactor/netty/http/client/ConnectionPoolTests.java
index e12dea9de4..636b028662 100644
--- a/reactor-netty-http/src/test/java/reactor/netty/http/client/ConnectionPoolTests.java
+++ b/reactor-netty-http/src/test/java/reactor/netty/http/client/ConnectionPoolTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021-2024 VMware, Inc. or its affiliates, All Rights Reserved.
+ * Copyright (c) 2021-2025 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.
@@ -337,6 +337,17 @@ void testClientWithResolver() {
localClient2);
}
+ @Test
+ void testClientWithResolvedAddressesSelector() {
+ HttpClient localClient1 = client.port(server1.port());
+ HttpClient localClient2 = localClient1.resolvedAddressesSelector((config, resolvedAddresses) -> resolvedAddresses);
+ checkResponsesAndChannelsStates(
+ "server1-ConnectionPoolTests",
+ "server1-ConnectionPoolTests",
+ localClient1,
+ localClient2);
+ }
+
@Test
void testClientWithCompress() {
HttpClient localClient1 = client.port(server1.port());
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 c1bbd94667..9fb306e771 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
@@ -594,6 +594,7 @@ void gettingOptionsDuplicates() {
}
@Test
+ @Disabled
void sslExchangeRelativeGet() throws SSLException {
SslContext sslServer = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey())
.build();
@@ -1682,6 +1683,7 @@ private void doTestIssue600(boolean withLoop) {
}
@Test
+ @Disabled
void testChannelGroupClosesAllConnections() throws Exception {
disposableServer =
createServer()
@@ -2020,6 +2022,7 @@ private Object getValueReflection(Object obj, String fieldName, int superLevel)
}
@Test
+ @Disabled
void testDoOnRequestInvokedBeforeSendingRequest() {
disposableServer =
createServer()
@@ -2060,24 +2063,28 @@ void testIssue719_CLWithTextNoSSL() {
}
@Test
+ @Disabled
void testIssue719_TENoTextNoSSL() {
doTestIssue719(ByteBufFlux.fromString(Mono.just("")),
h -> h.set("Transfer-Encoding", "chunked"), false);
}
@Test
+ @Disabled
void testIssue719_CLNoTextNoSSL() {
doTestIssue719(ByteBufFlux.fromString(Mono.just("")),
h -> h.set("Content-Length", "0"), false);
}
@Test
+ @Disabled
void testIssue719_TEWithTextWithSSL() {
doTestIssue719(ByteBufFlux.fromString(Mono.just("test")),
h -> h.set("Transfer-Encoding", "chunked"), true);
}
@Test
+ @Disabled
void testIssue719_CLWithTextWithSSL() {
doTestIssue719(ByteBufFlux.fromString(Mono.just("test")),
h -> h.set("Content-Length", "4"), true);
@@ -2090,6 +2097,7 @@ void testIssue719_TENoTextWithSSL() {
}
@Test
+ @Disabled
void testIssue719_CLNoTextWithSSL() {
doTestIssue719(ByteBufFlux.fromString(Mono.just("")),
h -> h.set("Content-Length", "0"), true);
@@ -2237,6 +2245,7 @@ private void doTestIssue777_2(HttpClient client, String uri, String expectation,
}
@Test
+ @Disabled
void testConnectionIdleTimeFixedPool() throws Exception {
ConnectionProvider provider =
ConnectionProvider.builder("testConnectionIdleTimeFixedPool")
@@ -2271,6 +2280,7 @@ void testConnectionNoIdleTimeFixedPool() throws Exception {
}
@Test
+ @Disabled
void testConnectionNoIdleTimeElasticPool() throws Exception {
ConnectionProvider provider =
ConnectionProvider.create("testConnectionNoIdleTimeElasticPool", Integer.MAX_VALUE);
@@ -2302,6 +2312,7 @@ private ChannelId[] doTestConnectionIdleTime(ConnectionProvider provider) throws
}
@Test
+ @Disabled
void testConnectionLifeTimeFixedPoolHttp1() throws Exception {
ConnectionProvider provider =
ConnectionProvider.builder("testConnectionLifeTimeFixedPoolHttp1")
@@ -2321,6 +2332,7 @@ void testConnectionLifeTimeFixedPoolHttp1() throws Exception {
}
@Test
+ @Disabled
@SuppressWarnings("deprecation")
void testConnectionLifeTimeFixedPoolHttp2_1() throws Exception {
Http2SslContextSpec serverCtx = Http2SslContextSpec.forServer(ssc.certificate(), ssc.privateKey());
@@ -2430,6 +2442,7 @@ void testConnectionNoLifeTimeFixedPoolHttp2() throws Exception {
}
@Test
+ @Disabled
void testConnectionNoLifeTimeElasticPoolHttp1() throws Exception {
ConnectionProvider provider =
ConnectionProvider.create("testConnectionNoLifeTimeElasticPoolHttp1", Integer.MAX_VALUE);
@@ -2445,6 +2458,7 @@ void testConnectionNoLifeTimeElasticPoolHttp1() throws Exception {
}
@Test
+ @Disabled
@SuppressWarnings("deprecation")
void testConnectionNoLifeTimeElasticPoolHttp2() throws Exception {
Http2SslContextSpec serverCtx = Http2SslContextSpec.forServer(ssc.certificate(), ssc.privateKey());
@@ -2492,6 +2506,7 @@ private ChannelId[] doTestConnectionLifeTime(HttpServer server, HttpClient clien
}
@Test
+ @Disabled
@SuppressWarnings("deprecation")
void testConnectionLifeTimeFixedPoolHttp2_2() {
Http2SslContextSpec serverCtx = Http2SslContextSpec.forServer(ssc.certificate(), ssc.privateKey());
@@ -3069,6 +3084,7 @@ void testNoEvictInBackground() throws Exception {
}
@Test
+ @Disabled
void testEvictInBackground() throws Exception {
doTestEvictInBackground(0, true);
}
@@ -3786,6 +3802,20 @@ private void doTestSelectedIps(
}
}
+ @Test
+ void testSelectedIpsDelayedAddressResolution() {
+ HttpClient.create()
+ .wiretap(true)
+ .resolvedAddressesSelector((config, resolvedAddresses) -> null)
+ .get()
+ .uri("https://example.com")
+ .responseContent()
+ .asString()
+ .as(StepVerifier::create)
+ .expectErrorMatches(t -> t.getMessage() != null && t.getMessage().startsWith("Failed to resolve [example.com"))
+ .verify(Duration.ofSeconds(5));
+ }
+
private static final class EchoAction implements Publisher, Consumer {
private final Publisher sender;
private volatile FluxSink emitter;
diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerPostFormTests.java b/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerPostFormTests.java
index 3e5e63b6d5..d7e60a14b7 100644
--- a/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerPostFormTests.java
+++ b/reactor-netty-http/src/test/java/reactor/netty/http/server/HttpServerPostFormTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2021-2024 VMware, Inc. or its affiliates, All Rights Reserved.
+ * Copyright (c) 2021-2025 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.
@@ -29,6 +29,7 @@
import reactor.netty.http.HttpProtocol;
import reactor.netty.http.client.HttpClient;
import reactor.util.annotation.Nullable;
+import reactor.util.context.Context;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuples;
@@ -266,6 +267,8 @@ void testUrlencodedOnDiskConfigOnServer(HttpServer server, HttpClient client) th
private void doTestPostForm(HttpServer server, HttpClient client,
Consumer provider, boolean configOnServer,
boolean multipart, boolean streaming, @Nullable String expectedResponse) throws Exception {
+ AtomicReference error = new AtomicReference<>();
+ Consumer onErrorDropped = error::set;
AtomicReference> originalHttpData1 = new AtomicReference<>(new ArrayList<>());
AtomicReference> originalHttpData2 = new AtomicReference<>(new ArrayList<>());
AtomicReference