diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyServerCustomizer.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyServerCustomizer.java index a377e5a0a2f..d8646c0ccdd 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyServerCustomizer.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/NettyServerCustomizer.java @@ -115,11 +115,18 @@ enum ChannelRole { CONNECTION, /** * The channel is a channel representing an individual HTTP2 stream. + *

+ * Note: As of 4.5.0, there is no separate channel for each request anymore for performance + * reasons. You can revert to the old behavior using the + * {@code micronaut.server.netty.legacy-multiplex-handlers=true} configuration property. */ - // todo: deprecate REQUEST_STREAM, /** * The channel is a channel representing an individual HTTP2 stream, created for a push promise. + *

+ * Note: As of 4.5.0, there is no separate channel for each request anymore for performance + * reasons. You can revert to the old behavior using the + * {@code micronaut.server.netty.legacy-multiplex-handlers=true} configuration property. */ PUSH_PROMISE_STREAM, } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java index e41a2992e71..f372ac6306c 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/configuration/NettyHttpServerConfiguration.java @@ -196,7 +196,7 @@ public class NettyHttpServerConfiguration extends HttpServerConfiguration { private List listeners = null; private boolean eagerParsing = DEFAULT_EAGER_PARSING; private int jsonBufferMaxComponents = DEFAULT_JSON_BUFFER_MAX_COMPONENTS; - private boolean legacyMultiplexHandlers = true; // TODO + private boolean legacyMultiplexHandlers = false; /** * Default empty constructor. diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/Http2ServerHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/Http2ServerHandler.java index 99e9e25c166..c1ecf1dce28 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/Http2ServerHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/Http2ServerHandler.java @@ -61,6 +61,7 @@ public final class Http2ServerHandler extends MultiplexedServerHandler implement private Http2ConnectionHandler connectionHandler; private Http2Connection.PropertyKey streamKey; private boolean reading = false; + private boolean upgradedFromHttp1 = false; static { for (Http2Error value : Http2Error.values()) { @@ -192,10 +193,13 @@ public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int stream */ public static final class ConnectionHandler extends Http2ConnectionHandler { private final Http2ServerHandler handler; + @Nullable + private final Http2AccessLogManager accessLogManager; - private ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings, boolean decoupleCloseAndGoAway, boolean flushPreface, Http2ServerHandler handler) { + private ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder, Http2Settings initialSettings, boolean decoupleCloseAndGoAway, boolean flushPreface, Http2ServerHandler handler, Http2AccessLogManager accessLogManager) { super(decoder, encoder, initialSettings, decoupleCloseAndGoAway, flushPreface); this.handler = handler; + this.accessLogManager = accessLogManager; } @Override @@ -238,7 +242,11 @@ protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { if (evt instanceof HttpServerUpgradeHandler.UpgradeEvent upgrade) { + handler.upgradedFromHttp1 = true; FullHttpRequest fhr = upgrade.upgradeRequest(); + if (accessLogManager != null) { + accessLogManager.logHeaders(ctx, 1, fhr); + } io.netty.handler.codec.http2.Http2Stream cs = connection().stream(1); handleFakeRequest(cs, fhr); } @@ -320,7 +328,7 @@ protected ConnectionHandler build(Http2ConnectionDecoder decoder, Http2Connectio if (accessLogManager != null) { encoder = new Http2AccessLogConnectionEncoder(encoder, accessLogManager); } - ConnectionHandler ch = new ConnectionHandler(decoder, encoder, initialSettings, decoupleCloseAndGoAway(), flushPreface(), frameListener); + ConnectionHandler ch = new ConnectionHandler(decoder, encoder, initialSettings, decoupleCloseAndGoAway(), flushPreface(), frameListener, accessLogManager); frameListener.init(ch); return ch; } @@ -336,7 +344,7 @@ private final class Http2Stream extends MultiplexedStream { @Override void notifyDataConsumed(int n) { - if (stream.id() == 1) { + if (stream.id() == 1 && upgradedFromHttp1) { // ignore for upgrade stream return; } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/MultiplexedServerHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/MultiplexedServerHandler.java index f4fc659c934..7c925c8dc08 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/MultiplexedServerHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/MultiplexedServerHandler.java @@ -197,7 +197,9 @@ final void devolveToStreaming() { streamer = new InputStreamer(); if (bufferedContent != null) { for (ByteBuf buf : bufferedContent) { - streamer.sink.tryEmitNext(new DefaultHttpContent(buf)); + if (streamer.sink.tryEmitNext(new DefaultHttpContent(buf)) != Sinks.EmitResult.OK) { + buf.release(); + } } bufferedContent = null; } diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/Http2AccessLogFrameListener.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/Http2AccessLogFrameListener.java index 3a5fe84038a..aa58b10e0da 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/Http2AccessLogFrameListener.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/Http2AccessLogFrameListener.java @@ -16,9 +16,7 @@ package io.micronaut.http.server.netty.handler.accesslog; import io.micronaut.core.annotation.Internal; -import io.micronaut.http.server.netty.handler.accesslog.element.AccessLog; import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.codec.http2.Http2FrameListener; @@ -42,23 +40,8 @@ public Http2AccessLogFrameListener(Http2FrameListener listener, Http2AccessLogMa } private void logHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers) throws Http2Exception { - if (!manager.logger.isInfoEnabled()) { - return; - } HttpRequest request = HttpConversionUtil.toHttpRequest(streamId, headers, false); - if (manager.uriInclusion != null && !manager.uriInclusion.test(request.uri())) { - return; - } - - AccessLog accessLog; - if (manager.logForReuse != null) { - accessLog = manager.logForReuse; - manager.logForReuse = null; - } else { - accessLog = manager.formatParser.newAccessLogger(); - } - manager.connection.stream(streamId).setProperty(manager.accessLogKey, accessLog); - accessLog.onRequestHeaders((SocketChannel) ctx.channel(), request.method().name(), request.headers(), request.uri(), HttpAccessLogHandler.H2_PROTOCOL_NAME); + manager.logHeaders(ctx, streamId, request); } @Override diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/Http2AccessLogManager.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/Http2AccessLogManager.java index a882014513e..6a9fbeca502 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/Http2AccessLogManager.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/handler/accesslog/Http2AccessLogManager.java @@ -18,6 +18,9 @@ import io.micronaut.core.annotation.Internal; import io.micronaut.http.server.netty.handler.accesslog.element.AccessLog; import io.micronaut.http.server.netty.handler.accesslog.element.AccessLogFormatParser; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http2.Http2Connection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -49,6 +52,25 @@ public Http2AccessLogManager(Factory factory, Http2Connection connection) { this.uriInclusion = factory.uriInclusion; } + public void logHeaders(ChannelHandlerContext ctx, int streamId, HttpRequest request) { + if (!logger.isInfoEnabled()) { + return; + } + if (uriInclusion != null && !uriInclusion.test(request.uri())) { + return; + } + + AccessLog accessLog; + if (logForReuse != null) { + accessLog = logForReuse; + logForReuse = null; + } else { + accessLog = formatParser.newAccessLogger(); + } + connection.stream(streamId).setProperty(accessLogKey, accessLog); + accessLog.onRequestHeaders((SocketChannel) ctx.channel(), request.method().name(), request.headers(), request.uri(), HttpAccessLogHandler.H2_PROTOCOL_NAME); + } + public record Factory(Logger logger, String spec, Predicate uriInclusion) { } } diff --git a/src/main/docs/guide/httpServer/serverConfiguration/nettyServerPipeline.adoc b/src/main/docs/guide/httpServer/serverConfiguration/nettyServerPipeline.adoc index 8e4a43ab6ac..dd482e1ce23 100644 --- a/src/main/docs/guide/httpServer/serverConfiguration/nettyServerPipeline.adoc +++ b/src/main/docs/guide/httpServer/serverConfiguration/nettyServerPipeline.adoc @@ -12,4 +12,4 @@ snippet::io.micronaut.docs.netty.LogbookNettyServerCustomizer[tags="imports,clas <4> When a new channel is created, a new, specialized customizer is created for that channel <5> When the server signals that the stream pipeline has been fully constructed, the logbook handler is registered -WARNING: Logbook has a https://github.com/zalando/logbook/issues/1216[major bug] that limits its usefulness with netty. +WARNING: As of version 4.5.0, the Micronaut netty HTTP server does not use per-request channels for HTTP/2 anymore. This makes many pipeline modifications (including logbook) difficult or impossible to implement. To revert to the old behavior, use the `micronaut.server.netty.legacy-multiplex-handlers=true` property. diff --git a/test-suite/src/test/groovy/io/micronaut/http/HttpAccessLogger.groovy b/test-suite/src/test/groovy/io/micronaut/http/HttpAccessLogger.groovy index 2ba0eae399d..91c10c58aa0 100644 --- a/test-suite/src/test/groovy/io/micronaut/http/HttpAccessLogger.groovy +++ b/test-suite/src/test/groovy/io/micronaut/http/HttpAccessLogger.groovy @@ -29,7 +29,7 @@ import io.micronaut.test.extensions.spock.annotation.MicronautTest import jakarta.inject.Inject import org.slf4j.LoggerFactory import reactor.core.publisher.Flux -import spock.lang.IgnoreIf +import spock.lang.PendingFeatureIf import spock.lang.Specification /** @@ -108,8 +108,9 @@ class HttpAccessLoggerSpec extends Specification { } - @IgnoreIf({ instance instanceof Http2AccessLoggerSpec }) // micronaut-session uses channel attributes to get the request, which is broken - void "test simple session - access logger"() { + @PendingFeatureIf({ instance instanceof Http2AccessLoggerSpec }) + // micronaut-session uses channel attributes to get the request, which is broken + void "test simple session - access logger"() { when: appender.events.clear() Flux> flowable = Flux.from(client.exchange(