diff --git a/.github/workflows/check_netty_snapshots.yml b/.github/workflows/check_netty_snapshots.yml index ed9a9079d4..ab3a77f734 100644 --- a/.github/workflows/check_netty_snapshots.yml +++ b/.github/workflows/check_netty_snapshots.yml @@ -34,6 +34,6 @@ jobs: distribution: 'graalvm' java-version: '17.0.12' - name: Build with Gradle - run: ./gradlew clean check -x :reactor-netty-core:java17Test --no-daemon -PforceTransport=${{ matrix.transport }} -PforceNettyVersion='4.1.120.Final-SNAPSHOT' + run: ./gradlew clean check -x :reactor-netty-core:java17Test --no-daemon -PforceTransport=${{ matrix.transport }} -PforceNettyVersion='4.1.122.Final-SNAPSHOT' - name: GraalVM smoke tests - run: ./gradlew :reactor-netty-graalvm-smoke-tests:nativeTest --no-daemon -PforceTransport=${{ matrix.transport }} -PforceNettyVersion='4.1.120.Final-SNAPSHOT' + run: ./gradlew :reactor-netty-graalvm-smoke-tests:nativeTest --no-daemon -PforceTransport=${{ matrix.transport }} -PforceNettyVersion='4.1.122.Final-SNAPSHOT' diff --git a/.github/workflows/check_reactor_core_3.6_snapshots.yml b/.github/workflows/check_reactor_core_3.6_snapshots.yml index 3f7a22f964..c66c243a22 100644 --- a/.github/workflows/check_reactor_core_3.6_snapshots.yml +++ b/.github/workflows/check_reactor_core_3.6_snapshots.yml @@ -19,4 +19,4 @@ jobs: distribution: 'temurin' java-version: '8' - name: Build with Gradle - run: ./gradlew clean check --no-daemon -PforceTransport=nio -PreactorCoreVersion='3.6.15-SNAPSHOT' -PforceContextPropagationVersion='1.1.0' + run: ./gradlew clean check --no-daemon -PforceTransport=nio -PreactorCoreVersion='3.6.16-SNAPSHOT' -PforceContextPropagationVersion='1.1.0' diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 433129d081..f5cd995e8e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -92,7 +92,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Set up Ruby for asciidoctor-pdf - uses: ruby/setup-ruby@e5ac7b085f6e63d49c8973eb0c6e04d876b881f1 # v1 + uses: ruby/setup-ruby@e34163cd15f4bb403dcd72d98e295997e6a55798 # v1 with: ruby-version: 3.3.0 - name: Install asciidoctor-pdf / rouge @@ -135,7 +135,7 @@ jobs: distribution: 'temurin' java-version: '8' - name: download antora docs/build - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: ${{ env.DOCS_BUILD_ARTIFACT }} path: docs/build @@ -166,7 +166,7 @@ jobs: distribution: 'temurin' java-version: '8' - name: download antora docs/build - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: ${{ env.DOCS_BUILD_ARTIFACT }} path: docs/build @@ -199,7 +199,7 @@ jobs: distribution: 'temurin' java-version: '8' - name: download antora docs/build - uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: ${{ env.DOCS_BUILD_ARTIFACT }} path: docs/build diff --git a/README.md b/README.md index 0faf57e104..8c45a064bf 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,10 @@ With `Gradle` from [repo.spring.io](https://repo.spring.io) or `Maven Central` r } dependencies { - //compile "io.projectreactor.netty:reactor-netty-core:1.2.6-SNAPSHOT" - compile "io.projectreactor.netty:reactor-netty-core:1.2.5" - //compile "io.projectreactor.netty:reactor-netty-http:1.2.6-SNAPSHOT" - compile "io.projectreactor.netty:reactor-netty-http:1.2.5" + //compile "io.projectreactor.netty:reactor-netty-core:1.2.7-SNAPSHOT" + compile "io.projectreactor.netty:reactor-netty-core:1.2.6" + //compile "io.projectreactor.netty:reactor-netty-http:1.2.7-SNAPSHOT" + compile "io.projectreactor.netty:reactor-netty-http:1.2.6" } ``` diff --git a/build.gradle b/build.gradle index d3917dd507..2baee3b55d 100644 --- a/build.gradle +++ b/build.gradle @@ -41,7 +41,7 @@ plugins { id 'biz.aQute.bnd.builder' version '6.4.0' apply false id 'org.graalvm.buildtools.native' version '0.9.25' apply false id 'io.spring.antora.generate-antora-yml' version '0.0.1' apply false - id 'net.ltgt.errorprone' version '4.1.0' apply false + id 'net.ltgt.errorprone' version '4.2.0' apply false id 'me.champeau.mrjar' version '0.1.1' apply false } @@ -109,7 +109,7 @@ ext { logbackVersion = '1.2.13' // Netty - nettyDefaultVersion = '4.1.119.Final' + nettyDefaultVersion = '4.1.121.Final' if (!project.hasProperty("forceNettyVersion")) { nettyVersion = nettyDefaultVersion } @@ -118,19 +118,19 @@ ext { println "Netty version defined from command line: ${forceNettyVersion}" } nettyIoUringVersion = '0.0.26.Final' - nettyQuicVersion = '0.0.71.Final' + nettyQuicVersion = '0.0.72.Final' nettyHttp3Version = '0.0.29.Final' // Testing brotli4jVersion = '1.18.0' - zstdJniVersion = '1.5.7-2' - jacksonDatabindVersion = '2.18.3' + zstdJniVersion = '1.5.7-3' + jacksonDatabindVersion = '2.19.0' testAddonVersion = reactorCoreVersion assertJVersion = '3.27.3' awaitilityVersion = '4.3.0' hoverflyJavaVersion = '0.19.1' tomcatVersion = '9.0.104' - boringSslVersion = '2.0.70.Final' + boringSslVersion = '2.0.71.Final' junitVersion = '5.12.2' junitPlatformLauncherVersion = '1.12.2' mockitoVersion = '4.11.0' diff --git a/docs/modules/ROOT/pages/http-server.adoc b/docs/modules/ROOT/pages/http-server.adoc index c27fd9a040..d1d67df61d 100644 --- a/docs/modules/ROOT/pages/http-server.adoc +++ b/docs/modules/ROOT/pages/http-server.adoc @@ -412,10 +412,13 @@ The following example uses a domain name containing a wildcard: include::{examples-dir}/sni/Application.java[lines=18..47] ---- -[[http-access-log]] -== HTTP Access Log +[[http-log]] +== HTTP Log -You can enable the `HTTP` access log either programmatically or by configuration. By default, it is disabled. +You can enable the `HTTP` access or error log either programmatically or by configuration. By default, it is disabled. + +[[access-log]] +=== Access Log You can use `-Dreactor.netty.http.server.accessLogEnabled=true` to enable the `HTTP` access log by configuration. @@ -472,6 +475,63 @@ include::{examples-dir}/accessLog/CustomFormatAndFilterAccessLogApplication.java <1> Specifies the filter predicate to use <2> Specifies the custom format to apply +[[error-log]] +=== Error Log + +You can use `-Dreactor.netty.http.server.errorLogEnabled=true` to enable the `HTTP` error log by configuration. + +You can use the following configuration (for Logback or similar logging frameworks) to have a separate +`HTTP` error log file: + +[source,xml] +---- + + error_log.log + + %msg%n + + + + + + + + + +---- + +The following example enables it programmatically: + +{examples-link}/errorLog/Application.java +---- +include::{examples-dir}/errorLog/Application.java[lines=18..32] +---- + +Calling this method takes precedence over the system property configuration. + +By default, the logging format is `[+{datetime}+] [pid +{PID}+] [client {remote address}] {exception message}`, but you can +specify a custom one as a parameter, as in the following example: + +{examples-link}/errorLog/CustomLogErrorFormatApplication.java +---- +include::{examples-dir}/errorLog/CustomLogErrorFormatApplication.java[lines=18..33] +---- + +You can also filter `HTTP` error logs by using the `ErrorLogFactory#createFilter` method, as in the following example: + +{examples-link}/errorLog/FilterLogErrorApplication.java +---- +include::{examples-dir}/errorLog/FilterLogErrorApplication.java[lines=18..33] +---- + +Note that this method can take a custom format parameter too, as in this example: + +{examples-link}/errorLog/CustomFormatAndFilterErrorLogApplication.java.java +---- +include::{examples-dir}/errorLog/CustomFormatAndFilterErrorLogApplication.java[lines=18..35] +---- +<1> Specifies the filter predicate to use +<2> Specifies the custom format to apply [[HTTP2]] == HTTP/2 diff --git a/docs/package.json b/docs/package.json index bf033ec78a..2bf435f1d7 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,7 +3,7 @@ "antora": "3.2.0-alpha.8", "@antora/atlas-extension": "1.0.0-alpha.2", "@antora/collector-extension": "1.0.1", - "@antora/pdf-extension": "1.0.0-alpha.11", + "@antora/pdf-extension": "1.0.0-alpha.12", "@asciidoctor/tabs": "1.0.0-beta.6", "@springio/antora-extensions": "1.14.4", "@springio/asciidoctor-extensions": "1.0.0-alpha.17" diff --git a/gradle.properties b/gradle.properties index 39e2332a0c..3f98c90c56 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,7 +1,7 @@ reactorPoolVersion=1.1.2 -version=1.2.5 -reactorNettyQuicVersion=0.2.5 -reactorCoreVersion=3.7.5 +version=1.2.6 +reactorNettyQuicVersion=0.2.6 +reactorCoreVersion=3.7.6 reactorAddonsVersion=3.5.2 -compatibleVersion=1.2.4 -bomVersion=2024.0.5 +compatibleVersion=1.2.5 +bomVersion=2024.0.6 diff --git a/gradle/javadoc.gradle b/gradle/javadoc.gradle index 333e11b304..462eb7c588 100644 --- a/gradle/javadoc.gradle +++ b/gradle/javadoc.gradle @@ -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. @@ -25,18 +25,19 @@ project.tasks.withType(Javadoc) { title = "${project.name} ${version}" compileTestJava.options.compilerArgs += "-parameters" - tasks.withType(Javadoc) { - options.addStringOption('Xdoclint:none', '-quiet') - } - options.addStringOption('charSet', 'UTF-8') + failOnError = true - options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED - options.author = true - options.header = "${project.name}" - options.overview = "$rootDir/docs/api/overview.html" - options.stylesheetFile = file("$rootDir/docs/api/stylesheet.css") - options.links(rootProject.ext.javadocLinks) + options { + addStringOption('Xdoclint:none', '-quiet') + addStringOption('charSet', 'UTF-8') + memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + author = true + header = "${project.name}" + overview = "$rootDir/docs/api/overview.html" + stylesheetFile = file("$rootDir/docs/api/stylesheet.css") + links(rootProject.ext.javadocLinks) + } maxMemory = "1024m" destinationDir = new File(buildDir, "docs/javadoc") diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975c74..1b33c55baa 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b1c8..ca025c83a7 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index faf93008b7..23d15a9367 100755 --- a/gradlew +++ b/gradlew @@ -114,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9b42019c79..5eed7ee845 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/reactor-netty-core/src/main/java/reactor/netty/NettyPipeline.java b/reactor-netty-core/src/main/java/reactor/netty/NettyPipeline.java index 15b89f85c0..449fcb84de 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/NettyPipeline.java +++ b/reactor-netty-core/src/main/java/reactor/netty/NettyPipeline.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-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. @@ -93,6 +93,7 @@ public interface NettyPipeline { String ChunkedWriter = LEFT + "chunkedWriter"; String CompressionHandler = LEFT + "compressionHandler"; String ConnectMetricsHandler = LEFT + "connectMetricsHandler"; + String ErrorLogHandler = LEFT + "errorLogHandler"; String H2CUpgradeHandler = LEFT + "h2cUpgradeHandler"; String H2Flush = LEFT + "h2Flush"; String H2MultiplexHandler = LEFT + "h2MultiplexHandler"; diff --git a/reactor-netty-core/src/main/java/reactor/netty/ReactorNetty.java b/reactor-netty-core/src/main/java/reactor/netty/ReactorNetty.java index c88d92c565..d0aa139e2b 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/ReactorNetty.java +++ b/reactor-netty-core/src/main/java/reactor/netty/ReactorNetty.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-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. @@ -194,6 +194,12 @@ public final class ReactorNetty { */ public static final String ACCESS_LOG_ENABLED = "reactor.netty.http.server.accessLogEnabled"; + /** + * Specifies whether the Http Server error log will be enabled. + * By default, it is disabled. + */ + public static final String ERROR_LOG_ENABLED = "reactor.netty.http.server.errorLogEnabled"; + /** * Specifies the zone id used by the access log. */ diff --git a/reactor-netty-core/src/main/java/reactor/netty/transport/ClientTransportConfig.java b/reactor-netty-core/src/main/java/reactor/netty/transport/ClientTransportConfig.java index f7a8f6df9e..5cfa834286 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/transport/ClientTransportConfig.java +++ b/reactor-netty-core/src/main/java/reactor/netty/transport/ClientTransportConfig.java @@ -57,6 +57,7 @@ public int channelHash() { int result = super.channelHash(); result = 31 * result + Objects.hashCode(proxyProvider); result = 31 * result + Objects.hashCode(resolver); + result = 31 * result + Objects.hashCode(resolvedAddressesSelector); return result; } diff --git a/reactor-netty-core/src/main/java/reactor/netty/transport/TransportConnector.java b/reactor-netty-core/src/main/java/reactor/netty/transport/TransportConnector.java index d1dffc7ea6..d9ae825dea 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/transport/TransportConnector.java +++ b/reactor-netty-core/src/main/java/reactor/netty/transport/TransportConnector.java @@ -376,7 +376,12 @@ static Mono doResolveAndConnect(Channel channel, TransportConfig config monoChannelPromise.tryFailure(future.cause()); } else { - doConnect(selectedAddresses(config, remoteAddress, future.getNow()), bindAddress, monoChannelPromise, 0); + try { + doConnect(selectedAddresses(config, remoteAddress, future.getNow()), bindAddress, monoChannelPromise, 0); + } + catch (Throwable t) { + monoChannelPromise.tryFailure(t); + } } }); return monoChannelPromise; diff --git a/reactor-netty-core/src/test/java/reactor/netty/tcp/TcpClientTests.java b/reactor-netty-core/src/test/java/reactor/netty/tcp/TcpClientTests.java index 1933bfc7c7..71267a1a2d 100644 --- a/reactor-netty-core/src/test/java/reactor/netty/tcp/TcpClientTests.java +++ b/reactor-netty-core/src/test/java/reactor/netty/tcp/TcpClientTests.java @@ -59,6 +59,7 @@ import io.netty.util.NetUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -796,11 +797,13 @@ public void close() throws IOException { @Test + @Disabled void testIssue600_1() { doTestIssue600(true); } @Test + @Disabled void testIssue600_2() { doTestIssue600(false); } @@ -927,6 +930,7 @@ private void connect(TcpClient client, boolean reconnect, CountDownLatch latch) } @Test + @Disabled void testIssue585_1() throws Exception { DisposableServer server = TcpServer.create() @@ -987,6 +991,7 @@ void testIssue585_1() throws Exception { } @Test + @Disabled void testIssue585_2() throws Exception { DisposableServer server = TcpServer.create() @@ -1201,6 +1206,7 @@ void testBootstrapUnsupported() { @Test @SuppressWarnings("deprecation") + @Disabled void testBootstrap() { DisposableServer server = TcpServer.create() diff --git a/reactor-netty-core/src/test/java/reactor/netty/tcp/TcpServerTests.java b/reactor-netty-core/src/test/java/reactor/netty/tcp/TcpServerTests.java index 152d052381..e90135685e 100644 --- a/reactor-netty-core/src/test/java/reactor/netty/tcp/TcpServerTests.java +++ b/reactor-netty-core/src/test/java/reactor/netty/tcp/TcpServerTests.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2011-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. @@ -66,6 +66,7 @@ import io.netty.util.concurrent.EventExecutor; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.params.ParameterizedTest; @@ -793,6 +794,7 @@ void testEchoWithLineBasedFrameDecoder() throws Exception { } @Test + @Disabled void testChannelGroupClosesAllConnections() throws Exception { ChannelGroup group = new DefaultChannelGroup(executor); @@ -828,6 +830,7 @@ void testChannelGroupClosesAllConnections() throws Exception { } @Test + @Disabled void testIssue688() throws Exception { CountDownLatch connected = new CountDownLatch(1); CountDownLatch configured = new CountDownLatch(1); @@ -872,6 +875,7 @@ else if (newState == ConnectionObserver.State.DISCONNECTING) { } @Test + @Disabled @SuppressWarnings("FutureReturnValueIgnored") void testHalfClosedConnection() throws Exception { DisposableServer server = @@ -920,6 +924,7 @@ void testHalfClosedConnection() throws Exception { } @Test + @Disabled void testGracefulShutdown() throws Exception { CountDownLatch latch1 = new CountDownLatch(2); CountDownLatch latch2 = new CountDownLatch(2); @@ -1127,6 +1132,7 @@ void testDisposeTimeoutLongOverflow() { } @Test + @Disabled @SuppressWarnings("deprecation") void testSniSupport() throws Exception { SelfSignedCertificate defaultCert = new SelfSignedCertificate("default"); @@ -1240,6 +1246,7 @@ void testTcpServerCancelled() throws InterruptedException { } @ParameterizedTest + @Disabled @ValueSource(booleans = {true, false}) void testIssue3406(boolean singleInvocation) { DisposableServer server = null; diff --git a/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/Application.java b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/Application.java new file mode 100644 index 0000000000..5239bf55c0 --- /dev/null +++ b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/Application.java @@ -0,0 +1,32 @@ +/* + * 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.examples.documentation.http.server.errorLog; + +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; + +public class Application { + + public static void main(String[] args) { + DisposableServer server = + HttpServer.create() + .errorLog(true) + .bindNow(); + + server.onDispose() + .block(); + } +} \ No newline at end of file diff --git a/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/CustomFormatAndFilterErrorLogApplication.java b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/CustomFormatAndFilterErrorLogApplication.java new file mode 100644 index 0000000000..fe58d0d933 --- /dev/null +++ b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/CustomFormatAndFilterErrorLogApplication.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.examples.documentation.http.server.errorLog; + +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.logging.error.ErrorLog; +import reactor.netty.http.server.logging.error.ErrorLogFactory; + +public class CustomFormatAndFilterErrorLogApplication { + + public static void main(String[] args) { + DisposableServer server = + HttpServer.create() + .errorLog(true, ErrorLogFactory.createFilter(p -> p.cause() instanceof RuntimeException, //<1> + x -> ErrorLog.create("method={}, uri={}", x.httpServerInfos().method(), x.httpServerInfos().uri()))) //<2> + .bindNow(); + + server.onDispose() + .block(); + } +} \ No newline at end of file diff --git a/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/CustomLogErrorFormatApplication.java b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/CustomLogErrorFormatApplication.java new file mode 100644 index 0000000000..051971a432 --- /dev/null +++ b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/CustomLogErrorFormatApplication.java @@ -0,0 +1,33 @@ +/* + * 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.examples.documentation.http.server.errorLog; + +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.logging.error.ErrorLog; + +public class CustomLogErrorFormatApplication { + + public static void main(String[] args) { + DisposableServer server = + HttpServer.create() + .errorLog(true, x -> ErrorLog.create("method={}, uri={}", x.httpServerInfos().method(), x.httpServerInfos().uri())) + .bindNow(); + + server.onDispose() + .block(); + } +} \ No newline at end of file diff --git a/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/FilterLogErrorApplication.java b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/FilterLogErrorApplication.java new file mode 100644 index 0000000000..4511c8a92a --- /dev/null +++ b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/errorLog/FilterLogErrorApplication.java @@ -0,0 +1,33 @@ +/* + * 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.examples.documentation.http.server.errorLog; + +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.logging.error.ErrorLogFactory; + +public class FilterLogErrorApplication { + + public static void main(String[] args) { + DisposableServer server = + HttpServer.create() + .errorLog(true, ErrorLogFactory.createFilter(p -> p.cause() instanceof RuntimeException)) + .bindNow(); + + server.onDispose() + .block(); + } +} \ No newline at end of file diff --git a/reactor-netty-graalvm-smoke-tests/build.gradle b/reactor-netty-graalvm-smoke-tests/build.gradle index d0cd669fe9..8f14cdb6e7 100644 --- a/reactor-netty-graalvm-smoke-tests/build.gradle +++ b/reactor-netty-graalvm-smoke-tests/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2023-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. @@ -63,7 +63,7 @@ graalvmNative { } metadataRepository { enabled = true - version = "0.3.14" + version = "0.3.19" } } diff --git a/reactor-netty-http/build.gradle b/reactor-netty-http/build.gradle index 31b4e26bb0..823b83d582 100644 --- a/reactor-netty-http/build.gradle +++ b/reactor-netty-http/build.gradle @@ -204,6 +204,7 @@ dependencies { http3TestImplementation(testFixtures(project(':reactor-netty-core'))) { exclude module: "reactor-netty-core" } + http3TestImplementation "org.mockito:mockito-core:$mockitoVersion" http3TestCompileOnly "com.google.code.findbugs:jsr305:$jsr305Version" http3TestImplementation "io.projectreactor:reactor-test:$testAddonVersion" http3TestImplementation "org.assertj:assertj-core:$assertJVersion" @@ -283,7 +284,6 @@ task japicmp(type: JapicmpTask) { compatibilityChangeExcludes = [ "METHOD_NEW_DEFAULT" ] methodExcludes = [ - 'reactor.netty.http.Http2SettingsSpec$Builder#connectProtocolEnabled(boolean)' ] } diff --git a/reactor-netty-http/src/http3Test/java/reactor/netty/http/server/logging/AccessLogArgProviderH3Tests.java b/reactor-netty-http/src/http3Test/java/reactor/netty/http/server/logging/AccessLogArgProviderH3Tests.java new file mode 100644 index 0000000000..f6ad3d1f29 --- /dev/null +++ b/reactor-netty-http/src/http3Test/java/reactor/netty/http/server/logging/AccessLogArgProviderH3Tests.java @@ -0,0 +1,161 @@ +/* + * 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; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.incubator.codec.http3.DefaultHttp3Headers; +import io.netty.incubator.codec.http3.DefaultHttp3HeadersFrame; +import io.netty.incubator.codec.http3.Http3Headers; +import io.netty.incubator.codec.http3.Http3HeadersFrame; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +class AccessLogArgProviderH3Tests { + static final CharSequence HEADER_TEST_NAME = "test"; + static final String HEADER_TEST_VALUE = "test"; + static final String URI = "/hello"; + + private static final Http3HeadersFrame requestHeaders; + private static final Http3HeadersFrame responseHeaders; + + static { + Http3Headers requestHttpHeaders = new DefaultHttp3Headers(); + requestHttpHeaders.add(HEADER_TEST_NAME, HEADER_TEST_VALUE); + requestHttpHeaders.method(HttpMethod.GET.name()); + requestHttpHeaders.path(URI); + requestHeaders = new DefaultHttp3HeadersFrame(requestHttpHeaders); + + Http3Headers responseHttpHeaders = new DefaultHttp3Headers(); + responseHttpHeaders.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_JSON); + responseHttpHeaders.status(HttpResponseStatus.OK.codeAsText()); + responseHeaders = new DefaultHttp3HeadersFrame(responseHttpHeaders); + } + + private AccessLogArgProviderH3 accessLogArgProvider; + + @BeforeEach + void beforeEach() { + accessLogArgProvider = new AccessLogArgProviderH3(new InetSocketAddress("127.0.0.1", 8080)); + } + + @Test + void requestHeaders() { + assertThatNullPointerException().isThrownBy(() -> accessLogArgProvider.requestHeaders(null)); + accessLogArgProvider.requestHeaders(requestHeaders); + assertThat(accessLogArgProvider.requestHeaders).isEqualTo(requestHeaders); + } + + @Test + void method() { + assertThat(accessLogArgProvider.method()).isNull(); + accessLogArgProvider.requestHeaders(requestHeaders); + assertThat(accessLogArgProvider.method()).isEqualTo(HttpMethod.GET.name()); + } + + @Test + void uri() { + assertThat(accessLogArgProvider.uri()).isNull(); + accessLogArgProvider.requestHeaders(requestHeaders); + assertThat(accessLogArgProvider.uri()).isEqualTo(URI); + } + + @Test + void protocol() { + assertThat(accessLogArgProvider.protocol()).isNull(); + accessLogArgProvider.requestHeaders(requestHeaders); + assertThat(accessLogArgProvider.protocol()).isEqualTo(AccessLogArgProviderH3.H3_PROTOCOL_NAME); + } + + @Test + void requestHeader() { + assertThatNullPointerException().isThrownBy(() -> accessLogArgProvider.requestHeader(null)); + assertThat(accessLogArgProvider.requestHeader(HEADER_TEST_NAME)).isNull(); + accessLogArgProvider.requestHeaders(requestHeaders); + assertThat(accessLogArgProvider.requestHeader(HEADER_TEST_NAME)) + .isEqualTo(HEADER_TEST_VALUE); + } + + @Test + void clear() { + assertThat(accessLogArgProvider.requestHeaders).isNull(); + assertThat(accessLogArgProvider.responseHeaders).isNull(); + accessLogArgProvider.requestHeaders(requestHeaders); + accessLogArgProvider.responseHeaders(responseHeaders); + assertThat(accessLogArgProvider.requestHeaders).isEqualTo(requestHeaders); + assertThat(accessLogArgProvider.responseHeaders).isEqualTo(responseHeaders); + accessLogArgProvider.clear(); + assertThat(accessLogArgProvider.requestHeaders).isNull(); + assertThat(accessLogArgProvider.responseHeaders).isNull(); + } + + @Test + void get() { + assertThat(accessLogArgProvider.get()).isEqualTo(accessLogArgProvider); + } + + @Test + void status() { + assertThat(accessLogArgProvider.status()).isNull(); + accessLogArgProvider.responseHeaders(responseHeaders); + assertThat(accessLogArgProvider.status()).isEqualTo(HttpResponseStatus.OK.codeAsText()); + } + + @Test + void responseHeader() { + assertThat(accessLogArgProvider.responseHeader(HttpHeaderNames.CONTENT_TYPE)).isNull(); + accessLogArgProvider.responseHeaders(responseHeaders); + assertThat(accessLogArgProvider.responseHeader(HttpHeaderNames.CONTENT_TYPE)) + .isEqualTo(HttpHeaderValues.APPLICATION_JSON); + } + + @Test + @SuppressWarnings({"CollectionUndefinedEquality", "DataFlowIssue"}) + void requestHeaderIterator() { + assertThat(accessLogArgProvider.requestHeaderIterator()).isNull(); + accessLogArgProvider.requestHeaders(requestHeaders); + assertThat(accessLogArgProvider.requestHeaderIterator()).isNotNull(); + Map requestHeaders = new HashMap<>(); + accessLogArgProvider.requestHeaderIterator().forEachRemaining(e -> requestHeaders.put(e.getKey(), e.getValue())); + assertThat(requestHeaders.size()).isEqualTo(3); + assertThat(requestHeaders.get(HEADER_TEST_NAME)).isEqualTo(HEADER_TEST_VALUE); + assertThat(requestHeaders.get(Http3Headers.PseudoHeaderName.METHOD.value())).isEqualTo(HttpMethod.GET.name()); + assertThat(requestHeaders.get(Http3Headers.PseudoHeaderName.PATH.value())).isEqualTo(URI); + } + + @Test + @SuppressWarnings({"CollectionUndefinedEquality", "DataFlowIssue"}) + void responseHeaderIterator() { + assertThat(accessLogArgProvider.responseHeaderIterator()).isNull(); + accessLogArgProvider.responseHeaders(responseHeaders); + assertThat(accessLogArgProvider.responseHeaderIterator()).isNotNull(); + Map responseHeaders = new HashMap<>(); + accessLogArgProvider.responseHeaderIterator().forEachRemaining(e -> responseHeaders.put(e.getKey(), e.getValue())); + assertThat(responseHeaders.size()).isEqualTo(2); + assertThat(responseHeaders.get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo(HttpHeaderValues.APPLICATION_JSON); + assertThat(responseHeaders.get(Http3Headers.PseudoHeaderName.STATUS.value())).isEqualTo(HttpResponseStatus.OK.codeAsText()); + } + +} diff --git a/reactor-netty-http/src/http3Test/java/reactor/netty/http/server/logging/error/ErrorLogTest.java b/reactor-netty-http/src/http3Test/java/reactor/netty/http/server/logging/error/ErrorLogTest.java new file mode 100644 index 0000000000..213886bda2 --- /dev/null +++ b/reactor-netty-http/src/http3Test/java/reactor/netty/http/server/logging/error/ErrorLogTest.java @@ -0,0 +1,226 @@ +/* + * 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.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.quic.InsecureQuicTokenHandler; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; +import reactor.netty.DisposableServer; +import reactor.netty.LogTracker; +import reactor.netty.http.Http3SslContextSpec; +import reactor.netty.http.HttpProtocol; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.server.HttpServer; + +import java.security.cert.CertificateException; +import java.time.Duration; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import static org.assertj.core.api.Assertions.assertThat; + +class ErrorLogTest { + + static final String CUSTOM_FORMAT = "method={}, uri={}"; + + static SelfSignedCertificate ssc; + + DisposableServer disposableServer; + + @BeforeAll + static void createSelfSignedCertificate() throws CertificateException { + ssc = new SelfSignedCertificate(); + } + + @AfterEach + void tearDown() { + if (disposableServer != null) { + disposableServer.disposeNow(); + } + } + + @Test + void errorLogDefaultFormat() throws Exception { + testErrorLogDefaultFormat( + server -> server.handle((req, res) -> { + res.withConnection(conn -> conn.channel().pipeline().fireExceptionCaught(new RuntimeException())); + return res.send(); + })); + } + + @Test + void errorLogDefaultFormatWhenReactivePipelineThrowsException() throws Exception { + testErrorLogDefaultFormat(server -> server.handle((req, res) -> Mono.error(new RuntimeException()))); + } + + @Test + void errorLogDefaultFormatWhenUnhandledThrowsException() throws Exception { + testErrorLogDefaultFormat( + server -> server.handle((req, res) -> { + throw new RuntimeException(); + })); + } + + @Test + void errorLogDefaultFormatWhenReactivePipelineThrowsExceptionInRoute() throws Exception { + testErrorLogDefaultFormat(server -> server.route(r -> r.get("/example/test", (req, res) -> Mono.error(new RuntimeException())))); + } + + @Test + void errorLogDefaultFormatWhenUnhandledThrowsExceptionInRoute() throws Exception { + testErrorLogDefaultFormat( + server -> server.route(r -> r.get("/example/test", (req, res) -> { + throw new RuntimeException(); + }))); + } + + void testErrorLogDefaultFormat(Function serverCustomizer) throws Exception { + try (LogTracker logTracker = new LogTracker("reactor.netty.http.server.ErrorLog", "java.lang.RuntimeException")) { + disposableServer = serverCustomizer.apply(createServer()).errorLog(true).bindNow(); + + getHttpClientResponse(createClient(disposableServer.port()), "/example/test"); + + assertThat(logTracker.latch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(logTracker.actualMessages).hasSize(1); + logTracker.actualMessages.forEach(e -> { + assertThat(e.getMessage()).isEqualTo(BaseErrorLogHandler.DEFAULT_LOG_FORMAT); + assertThat(e.getFormattedMessage()) + .matches("\\[(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}:\\d{2})\\+\\d{4}] \\[pid (\\d+)] \\[client ([0-9a-fA-F:]+(?:%[a-zA-Z0-9]+)?|\\d+\\.\\d+\\.\\d+\\.\\d+)(?::\\d+)?] java.lang.RuntimeException"); + }); + } + } + + @Test + void errorLogCustomFormat() throws Exception { + String msg = "method=GET, uri=/example/test"; + try (LogTracker logTracker = new LogTracker("reactor.netty.http.server.ErrorLog", msg)) { + disposableServer = + createServer() + .handle((req, resp) -> { + resp.withConnection(conn -> conn.channel().pipeline().fireExceptionCaught(new RuntimeException())); + return resp.send(); + }) + .errorLog(true, args -> ErrorLog.create(CUSTOM_FORMAT, args.httpServerInfos().method(), args.httpServerInfos().uri())) + .bindNow(); + + getHttpClientResponse(createClient(disposableServer.port()), "/example/test"); + + assertThat(logTracker.latch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(logTracker.actualMessages).hasSize(1); + logTracker.actualMessages.forEach(e -> { + assertThat(e.getMessage()).isEqualTo(CUSTOM_FORMAT); + assertThat(e.getFormattedMessage()).isEqualTo(msg); + }); + } + } + + @Test + void secondCallToErrorLogOverridesPreviousOne() throws Exception { + try (LogTracker logTracker = new LogTracker("reactor.netty.http.server.ErrorLog")) { + disposableServer = + createServer() + .handle((req, resp) -> { + resp.withConnection(conn -> conn.channel().pipeline().fireExceptionCaught(new RuntimeException())); + return resp.send(); + }) + .errorLog(true, args -> ErrorLog.create(CUSTOM_FORMAT, args.httpServerInfos().method(), args.httpServerInfos().uri())) + .errorLog(false) + .bindNow(); + + getHttpClientResponse(createClient(disposableServer.port()), "/example/test"); + + assertThat(logTracker.latch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(logTracker.actualMessages).hasSize(0); + } + } + + @Test + void errorLogFilteringAndFormatting() throws Exception { + String msg = "method=GET, uri=/filtered/test"; + try (LogTracker logTracker = new LogTracker("reactor.netty.http.server.ErrorLog", msg)) { + disposableServer = + createServer() + .handle((req, resp) -> { + resp.withConnection(conn -> conn.channel().pipeline().fireExceptionCaught(new RuntimeException())); + return resp.send(); + }) + .errorLog(true, ErrorLogFactory.createFilter( + p -> p.httpServerInfos().uri().startsWith("/filtered"), + args -> ErrorLog.create(CUSTOM_FORMAT, args.httpServerInfos().method(), args.httpServerInfos().uri()))) + .bindNow(); + + HttpClient httpClient = createClient(disposableServer.port()); + getHttpClientResponse(httpClient, "/example/test"); + getHttpClientResponse(httpClient, "/filtered/test"); + + assertThat(logTracker.latch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(logTracker.actualMessages).hasSize(1); + logTracker.actualMessages.forEach(e -> { + assertThat(e.getMessage()).isEqualTo(CUSTOM_FORMAT); + assertThat(e.getFormattedMessage()).isEqualTo(msg); + }); + } + } + + private static void getHttpClientResponse(HttpClient client, String uri) { + try { + client.get() + .uri(uri) + .response() + .block(Duration.ofSeconds(30)); + } + catch (Exception e) { + // ignore + } + } + + static HttpServer createServer() { + Http3SslContextSpec serverCtx = Http3SslContextSpec.forServer(ssc.key(), null, ssc.cert()); + return HttpServer.create() + .port(0) + .wiretap(true) + .protocol(HttpProtocol.HTTP3) + .secure(spec -> spec.sslContext(serverCtx)) + .http3Settings(spec -> spec.idleTimeout(Duration.ofSeconds(5)) + .maxData(10000000) + .maxStreamDataBidirectionalLocal(1000000) + .maxStreamDataBidirectionalRemote(1000000) + .maxStreamsBidirectional(100) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE)); + } + + static HttpClient createClient(int port) { + Http3SslContextSpec clientCtx = + Http3SslContextSpec.forClient() + .configure(builder -> builder.trustManager(InsecureTrustManagerFactory.INSTANCE)); + return HttpClient.create() + .port(port) + .wiretap(true) + .protocol(HttpProtocol.HTTP3) + .secure(spec -> spec.sslContext(clientCtx)) + .http3Settings(spec -> spec.idleTimeout(Duration.ofSeconds(5)) + .maxData(10000000) + .maxStreamDataBidirectionalLocal(1000000)); + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java index 3baab80283..52539e4ad8 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientConnect.java @@ -216,7 +216,7 @@ public void subscribe(CoreSubscriber actual) { boolean configCopied = false; HttpClientConfig _config = config; - //append secure handler if needed + //append a secure handler if needed if (handler.toURI.isSecure()) { if (_config.sslProvider == null) { configCopied = true; @@ -370,7 +370,7 @@ else if (handler.shouldRetry && AbortedException.isConnectionReset(error)) { // In some cases the channel close event may be delayed and thus the connection to be // returned to the pool and later the eviction functionality to remove it from the pool. // In some rare cases the connection might be acquired immediately, before the channel close - // event and the eviction functionality be able to remove it from the pool, this may lead to I/O + // event and the eviction functionality is able to remove it from the pool; this may lead to I/O // errors. // Mark the connection as non-persistent here so that it is never returned to the pool and leave // the channel close event to invalidate it. @@ -388,7 +388,7 @@ else if (handler.shouldRetry && AbortedException.isConnectionReset(error)) { // In some cases the channel close event may be delayed and thus the connection to be // returned to the pool and later the eviction functionality to remove it from the pool. // In some rare cases the connection might be acquired immediately, before the channel close - // event and the eviction functionality be able to remove it from the pool, this may lead to I/O + // event and the eviction functionality is able to remove it from the pool; this may lead to I/O // errors. // Mark the connection as non-persistent here so that it is never returned to the pool and leave // the channel close event to invalidate it. @@ -469,7 +469,6 @@ static final class HttpClientHandler extends SocketAddress final HttpHeaders defaultHeaders; final BiFunction> handler; - final boolean compress; final UriEndpointFactory uriEndpointFactory; final WebsocketClientSpec websocketClientSpec; final BiPredicate @@ -492,7 +491,6 @@ static final class HttpClientHandler extends SocketAddress HttpClientHandler(HttpClientConfig configuration) { this.method = configuration.method; - this.compress = configuration.acceptGzip; this.followRedirectPredicate = configuration.followRedirectPredicate; this.redirectRequestBiConsumer = configuration.redirectRequestBiConsumer; this.redirectRequestConsumer = configuration.redirectRequestConsumer; @@ -597,7 +595,7 @@ Publisher requestWithBody(HttpClientOperations ch) { } } Mono result = - Mono.fromRunnable(() -> ch.withWebsocketSupport(websocketClientSpec, compress)); + Mono.fromRunnable(() -> ch.withWebsocketSupport(websocketClientSpec)); if (handler != null) { result = result.thenEmpty(Mono.fromRunnable(() -> Flux.concat(handler.apply(ch, ch)))); } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java index 7a437a53f7..8a99428bfb 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/client/HttpClientOperations.java @@ -928,7 +928,7 @@ final void setNettyResponse(HttpResponse nettyResponse) { } @SuppressWarnings("ReferenceEquality") - final void withWebsocketSupport(WebsocketClientSpec websocketClientSpec, boolean compress) { + final void withWebsocketSupport(WebsocketClientSpec websocketClientSpec) { URI url = websocketUri(); //prevent further header to be sent for handshaking if (markSentHeaders()) { diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/AbstractHttpServerMetricsHandler.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/AbstractHttpServerMetricsHandler.java index 883df317b2..f2140e592c 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/AbstractHttpServerMetricsHandler.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/AbstractHttpServerMetricsHandler.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. @@ -164,8 +164,6 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) try { if (msg instanceof HttpResponse) { if (((HttpResponse) msg).status().code() == HttpResponseStatus.CONTINUE.code()) { - //"FutureReturnValueIgnored" this is deliberate - ctx.write(msg, promise); return; } @@ -195,9 +193,11 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) if (isHttp11 && LAST_FLUSH_WHEN_NO_READ) { copy = createMetricsArgProvider(); ChannelOperations channelOps = ChannelOperations.get(ctx.channel()); + HttpServerOperations ops = null; if (channelOps instanceof HttpServerOperations) { - recordInactiveConnectionOrStream(ctx.channel(), (HttpServerOperations) channelOps); + ops = (HttpServerOperations) channelOps; } + recordInactiveConnectionOrStream(ctx.channel(), ops); } else { copy = null; @@ -220,9 +220,11 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) if (copy == null) { ChannelOperations channelOps = ChannelOperations.get(ctx.channel()); + HttpServerOperations ops = null; if (channelOps instanceof HttpServerOperations) { - recordInactiveConnectionOrStream(ctx.channel(), (HttpServerOperations) channelOps); + ops = (HttpServerOperations) channelOps; } + recordInactiveConnectionOrStream(ctx.channel(), ops); } }); } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java index 6dc0b9ac14..128cee1e34 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java @@ -35,6 +35,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.util.Logger; import reactor.util.Loggers; import reactor.util.annotation.Nullable; @@ -56,6 +59,8 @@ final class Http3Codec extends ChannelInitializer { final BiPredicate compressPredicate; final ServerCookieDecoder cookieDecoder; final ServerCookieEncoder cookieEncoder; + final boolean errorLogEnabled; + final Function errorLog; final HttpServerFormDecoderProvider formDecoderProvider; final BiFunction forwardedHeaderHandler; final HttpMessageLogFactory httpMessageLogFactory; @@ -78,6 +83,8 @@ final class Http3Codec extends ChannelInitializer { @Nullable BiPredicate compressPredicate, ServerCookieDecoder decoder, ServerCookieEncoder encoder, + boolean errorLogEnabled, + @Nullable Function errorLog, HttpServerFormDecoderProvider formDecoderProvider, @Nullable BiFunction forwardedHeaderHandler, HttpMessageLogFactory httpMessageLogFactory, @@ -97,6 +104,8 @@ final class Http3Codec extends ChannelInitializer { this.compressPredicate = compressPredicate; this.cookieDecoder = decoder; this.cookieEncoder = encoder; + this.errorLogEnabled = errorLogEnabled; + this.errorLog = errorLog; this.formDecoderProvider = formDecoderProvider; this.forwardedHeaderHandler = forwardedHeaderHandler; this.httpMessageLogFactory = httpMessageLogFactory; @@ -149,6 +158,10 @@ else if (metricsRecorder instanceof ContextAwareHttpServerMetricsRecorder) { } } + if (errorLogEnabled) { + p.addBefore(NettyPipeline.ReactiveBridge, NettyPipeline.ErrorLogHandler, new DefaultErrorLogHandler(errorLog)); + } + channel.pipeline().remove(this); if (log.isDebugEnabled()) { @@ -163,6 +176,8 @@ static ChannelHandler newHttp3ServerConnectionHandler( @Nullable BiPredicate compressPredicate, ServerCookieDecoder decoder, ServerCookieEncoder encoder, + boolean errorLogEnabled, + @Nullable Function errorLog, HttpServerFormDecoderProvider formDecoderProvider, @Nullable BiFunction forwardedHeaderHandler, HttpMessageLogFactory httpMessageLogFactory, @@ -177,8 +192,8 @@ static ChannelHandler newHttp3ServerConnectionHandler( @Nullable Function 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> copiedHttpData = new AtomicReference<>(new HashMap<>()); @@ -296,7 +299,8 @@ private void doTestPostForm(HttpServer server, HttpClient client, data.isCompleted() + "] "); }) .onErrorResume(t -> Mono.just(t.getCause().getMessage())) - .log())); + .log() + .contextWrite(Context.of("reactor.onErrorDropped.local", onErrorDropped)))); disposableServer = server.bindNow(); @@ -350,6 +354,8 @@ private void doTestPostForm(HttpServer server, HttpClient client, } } } + + assertThat(error.get()).isNull(); } private void testContent(CompositeByteBuf file, byte[] expectedBytes) { diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogArgProviderH1Tests.java b/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogArgProviderH1Tests.java index 22f86f2602..5a8f5abb48 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogArgProviderH1Tests.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogArgProviderH1Tests.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. @@ -34,6 +34,8 @@ import java.net.InetSocketAddress; import java.time.ZonedDateTime; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; @@ -156,4 +158,28 @@ void responseHeader() { .isEqualTo(HttpHeaderValues.APPLICATION_JSON.toString()); } + @Test + @SuppressWarnings({"CollectionUndefinedEquality", "DataFlowIssue"}) + void requestHeaderIterator() { + assertThat(accessLogArgProvider.requestHeaderIterator()).isNull(); + accessLogArgProvider.request(request); + assertThat(accessLogArgProvider.requestHeaderIterator()).isNotNull(); + Map requestHeaders = new HashMap<>(); + accessLogArgProvider.requestHeaderIterator().forEachRemaining(e -> requestHeaders.put(e.getKey(), e.getValue())); + assertThat(requestHeaders.size()).isEqualTo(1); + assertThat(requestHeaders.get(HEADER_CONNECTION_NAME)).isEqualTo(HEADER_CONNECTION_VALUE); + } + + @Test + @SuppressWarnings({"CollectionUndefinedEquality", "DataFlowIssue"}) + void responseHeaderIterator() { + assertThat(accessLogArgProvider.responseHeaderIterator()).isNull(); + accessLogArgProvider.response(response); + assertThat(accessLogArgProvider.responseHeaderIterator()).isNotNull(); + Map responseHeaders = new HashMap<>(); + accessLogArgProvider.responseHeaderIterator().forEachRemaining(e -> responseHeaders.put(e.getKey(), e.getValue())); + assertThat(responseHeaders.size()).isEqualTo(1); + assertThat(responseHeaders.get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo(HttpHeaderValues.APPLICATION_JSON); + } + } diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogArgProviderH2Tests.java b/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogArgProviderH2Tests.java index 515342c92a..a7a7000356 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogArgProviderH2Tests.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogArgProviderH2Tests.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. @@ -27,6 +27,8 @@ import org.junit.jupiter.api.Test; import java.net.InetSocketAddress; +import java.util.HashMap; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; @@ -135,4 +137,31 @@ void responseHeader() { .isEqualTo(HttpHeaderValues.APPLICATION_JSON); } + @Test + @SuppressWarnings({"CollectionUndefinedEquality", "DataFlowIssue"}) + void requestHeaderIterator() { + assertThat(accessLogArgProvider.requestHeaderIterator()).isNull(); + accessLogArgProvider.requestHeaders(requestHeaders); + assertThat(accessLogArgProvider.requestHeaderIterator()).isNotNull(); + Map requestHeaders = new HashMap<>(); + accessLogArgProvider.requestHeaderIterator().forEachRemaining(e -> requestHeaders.put(e.getKey(), e.getValue())); + assertThat(requestHeaders.size()).isEqualTo(3); + assertThat(requestHeaders.get(HEADER_TEST_NAME)).isEqualTo(HEADER_TEST_VALUE); + assertThat(requestHeaders.get(Http2Headers.PseudoHeaderName.METHOD.value())).isEqualTo(HttpMethod.GET.name()); + assertThat(requestHeaders.get(Http2Headers.PseudoHeaderName.PATH.value())).isEqualTo(URI); + } + + @Test + @SuppressWarnings({"CollectionUndefinedEquality", "DataFlowIssue"}) + void responseHeaderIterator() { + assertThat(accessLogArgProvider.responseHeaderIterator()).isNull(); + accessLogArgProvider.responseHeaders(responseHeaders); + assertThat(accessLogArgProvider.responseHeaderIterator()).isNotNull(); + Map responseHeaders = new HashMap<>(); + accessLogArgProvider.responseHeaderIterator().forEachRemaining(e -> responseHeaders.put(e.getKey(), e.getValue())); + assertThat(responseHeaders.size()).isEqualTo(2); + assertThat(responseHeaders.get(HttpHeaderNames.CONTENT_TYPE)).isEqualTo(HttpHeaderValues.APPLICATION_JSON); + assertThat(responseHeaders.get(Http2Headers.PseudoHeaderName.STATUS.value())).isEqualTo(HttpResponseStatus.OK.codeAsText()); + } + } diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogTest.java b/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogTest.java index ed18b83e30..5f56bfd3a7 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogTest.java +++ b/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/AccessLogTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2022 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. @@ -30,6 +30,7 @@ import reactor.core.publisher.Mono; import reactor.netty.BaseHttpTest; import reactor.netty.NettyPipeline; +import reactor.netty.http.server.HttpServer; import reactor.util.annotation.Nullable; import reactor.util.function.Tuple2; @@ -38,6 +39,7 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import static org.assertj.core.api.Assertions.assertThat; import static reactor.netty.http.server.logging.AccessLog.LOG; @@ -61,6 +63,7 @@ class AccessLogTest extends BaseHttpTest { private Appender mockedAppender; private ArgumentCaptor loggingEventArgumentCaptor; + private HttpServer server; @BeforeEach @SuppressWarnings("unchecked") @@ -69,6 +72,15 @@ void setUp() { loggingEventArgumentCaptor = ArgumentCaptor.forClass(LoggingEvent.class); Mockito.when(mockedAppender.getName()).thenReturn("MOCK"); ROOT.addAppender(mockedAppender); + + server = createServer() + .handle((req, resp) -> { + resp.withConnection(conn -> { + ChannelHandler handler = conn.channel().pipeline().get(NettyPipeline.AccessLogHandler); + resp.header(ACCESS_LOG_HANDLER, handler != null ? FOUND : NOT_FOUND); + }); + return resp.send(); + }); } @AfterEach @@ -78,16 +90,7 @@ void tearDown() { @Test void accessLogDefaultFormat() { - disposableServer = createServer() - .handle((req, resp) -> { - resp.withConnection(conn -> { - ChannelHandler handler = conn.channel().pipeline().get(NettyPipeline.AccessLogHandler); - resp.header(ACCESS_LOG_HANDLER, handler != null ? FOUND : NOT_FOUND); - }); - return resp.send(); - }) - .accessLog(true) - .bindNow(); + disposableServer = server.accessLog(true).bindNow(); Tuple2 response = getHttpClientResponse(URI_1); @@ -96,16 +99,7 @@ void accessLogDefaultFormat() { @Test void accessLogCustomFormat() { - disposableServer = createServer() - .handle((req, resp) -> { - resp.withConnection(conn -> { - ChannelHandler handler = conn.channel().pipeline().get(NettyPipeline.AccessLogHandler); - resp.header(ACCESS_LOG_HANDLER, handler != null ? FOUND : NOT_FOUND); - }); - return resp.send(); - }) - .accessLog(true, CUSTOM_ACCESS_LOG) - .bindNow(); + disposableServer = server.accessLog(true, CUSTOM_ACCESS_LOG).bindNow(); Tuple2 response = getHttpClientResponse(URI_1); @@ -114,17 +108,10 @@ void accessLogCustomFormat() { @Test void secondCallToAccessLogOverridesPreviousOne() { - disposableServer = createServer() - .handle((req, resp) -> { - resp.withConnection(conn -> { - ChannelHandler handler = conn.channel().pipeline().get(NettyPipeline.AccessLogHandler); - resp.header(ACCESS_LOG_HANDLER, handler != null ? FOUND : NOT_FOUND); - }); - return resp.send(); - }) - .accessLog(true, CUSTOM_ACCESS_LOG) - .accessLog(false) - .bindNow(); + disposableServer = + server.accessLog(true, CUSTOM_ACCESS_LOG) + .accessLog(false) + .bindNow(); Tuple2 response = getHttpClientResponse(URI_1); @@ -133,16 +120,9 @@ void secondCallToAccessLogOverridesPreviousOne() { @Test void accessLogFiltering() { - disposableServer = createServer() - .handle((req, resp) -> { - resp.withConnection(conn -> { - ChannelHandler handler = conn.channel().pipeline().get(NettyPipeline.AccessLogHandler); - resp.header(ACCESS_LOG_HANDLER, handler != null ? FOUND : NOT_FOUND); - }); - return resp.send(); - }) - .accessLog(true, AccessLogFactory.createFilter(p -> !String.valueOf(p.uri()).startsWith("/filtered/"))) - .bindNow(); + disposableServer = + server.accessLog(true, AccessLogFactory.createFilter(p -> !String.valueOf(p.uri()).startsWith("/filtered/"))) + .bindNow(); Tuple2 response = getHttpClientResponse(URI_1); @@ -154,17 +134,9 @@ void accessLogFiltering() { @Test void accessLogFilteringAndFormatting() { - disposableServer = createServer() - .handle((req, resp) -> { - resp.withConnection(conn -> { - ChannelHandler handler = conn.channel().pipeline().get(NettyPipeline.AccessLogHandler); - resp.header(ACCESS_LOG_HANDLER, handler != null ? FOUND : NOT_FOUND); - }); - return resp.send(); - }) - .accessLog(true, AccessLogFactory.createFilter(p -> !String.valueOf(p.uri()).startsWith("/filtered/"), - CUSTOM_ACCESS_LOG)) - .bindNow(); + disposableServer = + server.accessLog(true, AccessLogFactory.createFilter(p -> !String.valueOf(p.uri()).startsWith("/filtered/"), CUSTOM_ACCESS_LOG)) + .bindNow(); Tuple2 response = getHttpClientResponse(URI_1); @@ -173,6 +145,25 @@ void accessLogFilteringAndFormatting() { assertAccessLogging(response, true, true, CUSTOM_FORMAT); } + @Test + @SuppressWarnings("unchecked") + void accessLogCustomImplementation() { + Consumer argConsumer = (Consumer) Mockito.mock(Consumer.class); + ArgumentCaptor accessLogArgProviderArgumentCaptor = ArgumentCaptor.forClass(AccessLogArgProvider.class); + disposableServer = + server.accessLog(true, argProvider -> new CustomAccessLog(argProvider, argConsumer)) + .bindNow(); + + Tuple2 response = getHttpClientResponse(URI_1); + assertThat(response).isNotNull(); + assertThat(response.getT2()).isEqualTo(FOUND); + Mockito.verify(argConsumer, Mockito.times(1)).accept(accessLogArgProviderArgumentCaptor.capture()); + AccessLogArgProvider capturedArgs = accessLogArgProviderArgumentCaptor.getValue(); + assertThat(capturedArgs.protocol()).isEqualTo("HTTP/1.1"); + assertThat(capturedArgs.method()).isEqualTo("GET"); + assertThat(capturedArgs.uri()).isEqualTo(URI_1); + } + void assertAccessLogging( @Nullable Tuple2 response, boolean enable, boolean filteringEnabled, @@ -191,7 +182,6 @@ void assertAccessLogging( assertThat(relevantLog.getFormattedMessage()).doesNotContain("filtered"); } } - else { assertThat(relevantLog.getMessage()).isEqualTo(loggerFormat); assertThat(relevantLog.getFormattedMessage()).isEqualTo(EXPECTED_FORMATTED_MESSAGE_2); @@ -220,7 +210,7 @@ private Tuple2 getHttpClientResponse(String uri) { .block(Duration.ofSeconds(30)); } - private void sleep(long ms) { + private static void sleep(long ms) { try { TimeUnit.MILLISECONDS.sleep(ms); } @@ -241,4 +231,22 @@ static String cookieToString(@Nullable Map> cookies) { } return ""; } + + private static class CustomAccessLog extends AccessLog { + + private final AccessLogArgProvider argProvider; + private final Consumer argConsumer; + + public CustomAccessLog(AccessLogArgProvider argProvider, Consumer argConsumer) { + super(""); + this.argProvider = argProvider; + this.argConsumer = argConsumer; + } + + @Override + public void log() { + argConsumer.accept(argProvider); + } + + } } diff --git a/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/error/ErrorLogTest.java b/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/error/ErrorLogTest.java new file mode 100644 index 0000000000..1a2ca14c36 --- /dev/null +++ b/reactor-netty-http/src/test/java/reactor/netty/http/server/logging/error/ErrorLogTest.java @@ -0,0 +1,260 @@ +/* + * 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.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.BaseHttpTest; +import reactor.netty.LogTracker; +import reactor.netty.http.Http11SslContextSpec; +import reactor.netty.http.Http2SslContextSpec; +import reactor.netty.http.HttpProtocol; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.HttpClientConfig; +import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.HttpServerConfig; +import reactor.util.function.Tuple2; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * This test class verifies {@link DefaultErrorLogHandler}. + * + * @author raccoonback + */ +class ErrorLogTest extends BaseHttpTest { + + static final String CUSTOM_FORMAT = "method={}, uri={}"; + + @ParameterizedTest + @MethodSource("httpProtocolsCompatibleCombinations") + void errorLogDefaultFormat(HttpServer server, HttpClient client) throws Exception { + testErrorLogDefaultFormat( + server.handle((req, res) -> { + res.withConnection(conn -> conn.channel().pipeline().fireExceptionCaught(new RuntimeException())); + return res.send(); + }), + client); + } + + @ParameterizedTest + @MethodSource("httpProtocolsCompatibleCombinations") + void errorLogDefaultFormatWhenReactivePipelineThrowsException(HttpServer server, HttpClient client) throws Exception { + testErrorLogDefaultFormat( + server.handle((req, res) -> Mono.error(new RuntimeException())), + client); + } + + @ParameterizedTest + @MethodSource("httpProtocolsCompatibleCombinations") + void errorLogDefaultFormatWhenUnhandledThrowsException(HttpServer server, HttpClient client) throws Exception { + testErrorLogDefaultFormat( + server.handle((req, res) -> { + throw new RuntimeException(); + }), + client); + } + + @ParameterizedTest + @MethodSource("httpProtocolsCompatibleCombinations") + void errorLogDefaultFormatWhenReactivePipelineThrowsExceptionInRoute(HttpServer server, HttpClient client) throws Exception { + testErrorLogDefaultFormat( + server.route(r -> r.get("/example/test", (req, res) -> Mono.error(new RuntimeException()))), + client); + } + + @ParameterizedTest + @MethodSource("httpProtocolsCompatibleCombinations") + void errorLogDefaultFormatWhenUnhandledThrowsExceptionInRoute(HttpServer server, HttpClient client) throws Exception { + testErrorLogDefaultFormat( + server.route(r -> r.get("/example/test", (req, res) -> { + throw new RuntimeException(); + })), + client); + } + + void testErrorLogDefaultFormat(HttpServer server, HttpClient client) throws Exception { + try (LogTracker logTracker = new LogTracker("reactor.netty.http.server.ErrorLog", "java.lang.RuntimeException")) { + disposableServer = server.errorLog(true).bindNow(); + + getHttpClientResponse(client.port(disposableServer.port()), "/example/test"); + + assertThat(logTracker.latch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(logTracker.actualMessages).hasSize(1); + logTracker.actualMessages.forEach(e -> { + assertThat(e.getMessage()).isEqualTo(BaseErrorLogHandler.DEFAULT_LOG_FORMAT); + assertThat(e.getFormattedMessage()) + .matches("\\[(\\d{4}-\\d{2}-\\d{2}) (\\d{2}:\\d{2}:\\d{2})\\+\\d{4}] \\[pid (\\d+)] \\[client ([0-9a-fA-F:]+(?:%[a-zA-Z0-9]+)?|\\d+\\.\\d+\\.\\d+\\.\\d+)(?::\\d+)?] java.lang.RuntimeException"); + }); + } + } + + @ParameterizedTest + @MethodSource("httpProtocolsCompatibleCombinations") + void errorLogCustomFormat(HttpServer server, HttpClient client) throws Exception { + String msg = "method=GET, uri=/example/test"; + try (LogTracker logTracker = new LogTracker("reactor.netty.http.server.ErrorLog", msg)) { + disposableServer = + server.handle((req, resp) -> { + resp.withConnection(conn -> conn.channel().pipeline().fireExceptionCaught(new RuntimeException())); + return resp.send(); + }) + .errorLog(true, args -> ErrorLog.create(CUSTOM_FORMAT, args.httpServerInfos().method(), args.httpServerInfos().uri())) + .bindNow(); + + getHttpClientResponse(client.port(disposableServer.port()), "/example/test"); + + assertThat(logTracker.latch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(logTracker.actualMessages).hasSize(1); + logTracker.actualMessages.forEach(e -> { + assertThat(e.getMessage()).isEqualTo(CUSTOM_FORMAT); + assertThat(e.getFormattedMessage()).isEqualTo(msg); + }); + } + } + + @ParameterizedTest + @MethodSource("httpProtocolsCompatibleCombinations") + void secondCallToErrorLogOverridesPreviousOne(HttpServer server, HttpClient client) throws Exception { + try (LogTracker logTracker = new LogTracker("reactor.netty.http.server.ErrorLog")) { + disposableServer = + server.handle((req, resp) -> { + resp.withConnection(conn -> conn.channel().pipeline().fireExceptionCaught(new RuntimeException())); + return resp.send(); + }) + .errorLog(true, args -> ErrorLog.create(CUSTOM_FORMAT, args.httpServerInfos().method(), args.httpServerInfos().uri())) + .errorLog(false) + .bindNow(); + + getHttpClientResponse(client.port(disposableServer.port()), "/example/test"); + + assertThat(logTracker.latch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(logTracker.actualMessages).hasSize(0); + } + } + + @ParameterizedTest + @MethodSource("httpProtocolsCompatibleCombinations") + void errorLogFilteringAndFormatting(HttpServer server, HttpClient client) throws Exception { + String msg = "method=GET, uri=/filtered/test"; + try (LogTracker logTracker = new LogTracker("reactor.netty.http.server.ErrorLog", msg)) { + disposableServer = + server.handle((req, resp) -> { + resp.withConnection(conn -> conn.channel().pipeline().fireExceptionCaught(new RuntimeException())); + return resp.send(); + }) + .errorLog(true, ErrorLogFactory.createFilter( + p -> p.httpServerInfos().uri().startsWith("/filtered"), + args -> ErrorLog.create(CUSTOM_FORMAT, args.httpServerInfos().method(), args.httpServerInfos().uri()))) + .bindNow(); + + HttpClient httpClient = client.port(disposableServer.port()); + getHttpClientResponse(httpClient, "/example/test"); + getHttpClientResponse(httpClient, "/filtered/test"); + + assertThat(logTracker.latch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(logTracker.actualMessages).hasSize(1); + logTracker.actualMessages.forEach(e -> { + assertThat(e.getMessage()).isEqualTo(CUSTOM_FORMAT); + assertThat(e.getFormattedMessage()).isEqualTo(msg); + }); + } + } + + private static void getHttpClientResponse(HttpClient client, String uri) { + try { + client.get() + .uri(uri) + .response() + .block(Duration.ofSeconds(30)); + } + catch (Exception e) { + // ignore + } + } + + @SuppressWarnings("deprecation") + static Object[][] httpProtocolsCompatibleCombinations() throws Exception { + SelfSignedCertificate cert = new SelfSignedCertificate(); + Http11SslContextSpec serverCtxHttp11 = Http11SslContextSpec.forServer(cert.certificate(), cert.privateKey()); + Http11SslContextSpec clientCtxHttp11 = + Http11SslContextSpec.forClient() + .configure(builder -> builder.trustManager(InsecureTrustManagerFactory.INSTANCE)); + Http2SslContextSpec serverCtxHttp2 = Http2SslContextSpec.forServer(cert.certificate(), cert.privateKey()); + Http2SslContextSpec clientCtxHttp2 = + Http2SslContextSpec.forClient() + .configure(builder -> builder.trustManager(InsecureTrustManagerFactory.INSTANCE)); + + HttpServer _server = createServer(); + + HttpServer[] servers = new HttpServer[]{ + _server, // by default protocol is HTTP/1.1 + _server.protocol(HttpProtocol.H2C), + _server.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C), + _server.secure(spec -> spec.sslContext(serverCtxHttp11)), // by default protocol is HTTP/1.1 + _server.secure(spec -> spec.sslContext(serverCtxHttp2)).protocol(HttpProtocol.H2), + _server.secure(spec -> spec.sslContext(serverCtxHttp2)).protocol(HttpProtocol.HTTP11, HttpProtocol.H2) + }; + + HttpClient _client = HttpClient.create(); + _client = _client.wiretap(true); + + HttpClient[] clients = new HttpClient[]{ + _client, // by default protocol is HTTP/1.1 + _client.protocol(HttpProtocol.H2C), + _client.protocol(HttpProtocol.HTTP11, HttpProtocol.H2C), + _client.secure(spec -> spec.sslContext(clientCtxHttp11)), // by default protocol is HTTP/1.1 + _client.secure(spec -> spec.sslContext(clientCtxHttp2)).protocol(HttpProtocol.H2), + _client.secure(spec -> spec.sslContext(clientCtxHttp2)).protocol(HttpProtocol.HTTP11, HttpProtocol.H2) + }; + + Flux f1 = Flux.fromArray(servers).concatMap(o -> Flux.just(o).repeat(clients.length - 1)); + Flux f2 = Flux.fromArray(clients).repeat(servers.length - 1); + + return Flux.zip(f1, f2) + .filter(tuple2 -> { + HttpServerConfig serverConfig = tuple2.getT1().configuration(); + HttpClientConfig clientConfig = tuple2.getT2().configuration(); + List serverProtocols = Arrays.asList(serverConfig.protocols()); + List clientProtocols = Arrays.asList(clientConfig.protocols()); + if (serverConfig.isSecure() != clientConfig.isSecure()) { + return false; + } + else if (serverProtocols.size() == 1 && serverProtocols.get(0) == HttpProtocol.H2C && + clientProtocols.size() == 2) { + return false; + } + return serverProtocols.containsAll(clientProtocols) || clientProtocols.containsAll(serverProtocols); + }) + .map(Tuple2::toArray) + .collectList() + .block(Duration.ofSeconds(30)) + .toArray(new Object[0][2]); + } +} diff --git a/reactor-netty-http/src/test/java/reactor/netty/resources/PooledConnectionProviderDefaultMetricsTest.java b/reactor-netty-http/src/test/java/reactor/netty/resources/PooledConnectionProviderDefaultMetricsTest.java index b63dff62a7..145444a8c1 100644 --- a/reactor-netty-http/src/test/java/reactor/netty/resources/PooledConnectionProviderDefaultMetricsTest.java +++ b/reactor-netty-http/src/test/java/reactor/netty/resources/PooledConnectionProviderDefaultMetricsTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2024 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2019-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. @@ -16,6 +16,7 @@ package reactor.netty.resources; import io.micrometer.core.instrument.Gauge; +import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @@ -418,7 +419,9 @@ public void testConnectionProviderDisableAllBuiltInMetrics() throws Exception { AtomicInteger count = new AtomicInteger(); REGISTRY.forEachMeter(meter -> { - if (meter.getId().getName().startsWith("reactor.netty.connection")) { + Meter.Id meterId = meter.getId(); + if (meterId.getName().startsWith("reactor.netty.connection") && + "testConnectionProviderDisableAllBuiltInMetrics".equals(meterId.getTag("name"))) { count.incrementAndGet(); } }); diff --git a/reactor-netty-incubator-quic/README.md b/reactor-netty-incubator-quic/README.md index 1cee9bec31..2f2a36a4bc 100644 --- a/reactor-netty-incubator-quic/README.md +++ b/reactor-netty-incubator-quic/README.md @@ -13,8 +13,8 @@ With `Gradle` from [repo.spring.io](https://repo.spring.io) or `Maven Central` r } dependencies { - //compile "io.projectreactor.netty.incubator:reactor-netty-incubator-quic:0.2.6-SNAPSHOT" - compile "io.projectreactor.netty.incubator:reactor-netty-incubator-quic:0.2.5" + //compile "io.projectreactor.netty.incubator:reactor-netty-incubator-quic:0.2.7-SNAPSHOT" + compile "io.projectreactor.netty.incubator:reactor-netty-incubator-quic:0.2.6" } ```