From 0f31dd0a708d69915dcd9bd7c4dbfdb52832452e Mon Sep 17 00:00:00 2001 From: Jiawei Wnag Date: Sun, 5 Oct 2025 01:02:24 +0800 Subject: [PATCH 1/2] fix the open issue #11794 --- .../client/exceptions/ReadTimeoutException.java | 13 ++++++++++--- .../http/client/netty/DefaultHttpClient.java | 4 ++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/http-client-core/src/main/java/io/micronaut/http/client/exceptions/ReadTimeoutException.java b/http-client-core/src/main/java/io/micronaut/http/client/exceptions/ReadTimeoutException.java index 09af788ac4e..5916e680212 100644 --- a/http-client-core/src/main/java/io/micronaut/http/client/exceptions/ReadTimeoutException.java +++ b/http-client-core/src/main/java/io/micronaut/http/client/exceptions/ReadTimeoutException.java @@ -15,6 +15,8 @@ */ package io.micronaut.http.client.exceptions; +import io.micronaut.core.annotation.Nullable; + /** * An exception thrown when a read timeout occurs. * @@ -23,9 +25,14 @@ */ public final class ReadTimeoutException extends HttpClientException { - public static final ReadTimeoutException TIMEOUT_EXCEPTION = new ReadTimeoutException(); + public static final ReadTimeoutException TIMEOUT_EXCEPTION = new ReadTimeoutException("Read Timeout"); + + public ReadTimeoutException(String message) { + super(message); + } - private ReadTimeoutException() { - super("Read Timeout", null, true); + public ReadTimeoutException(String message, @Nullable String serviceId) { + super(message); + setServiceId(serviceId); } } diff --git a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java index 3b38b033ea5..153de1717c4 100644 --- a/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java +++ b/http-client/src/main/java/io/micronaut/http/client/netty/DefaultHttpClient.java @@ -918,7 +918,7 @@ private Mono> exchange(io.micronaut.http.HttpRequest { if (throwable instanceof TimeoutException) { - return ExecutionFlow.error(ReadTimeoutException.TIMEOUT_EXCEPTION); + return ExecutionFlow.error(new ReadTimeoutException("Read Timeout", informationalServiceId)); } return ExecutionFlow.error(throwable); }); @@ -2050,7 +2050,7 @@ private E decorate(E exc) { } else if (cause instanceof io.micronaut.http.exceptions.BufferLengthExceededException blee) { result = decorate(new ContentLengthExceededException(blee.getAdvertisedLength(), blee.getReceivedLength())); } else if (cause instanceof io.netty.handler.timeout.ReadTimeoutException) { - result = ReadTimeoutException.TIMEOUT_EXCEPTION; + result = new ReadTimeoutException("Read Timeout", informationalServiceId); } else if (cause instanceof HttpClientException hce) { result = decorate(hce); } else { From af37e73647adb48c8c34c61c73e8ce8058c764ae Mon Sep 17 00:00:00 2001 From: Jiawei Wnag Date: Tue, 7 Oct 2025 13:05:28 +0800 Subject: [PATCH 2/2] upload a reproducer --- reproduce_issue.groovy | 83 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 reproduce_issue.groovy diff --git a/reproduce_issue.groovy b/reproduce_issue.groovy new file mode 100644 index 00000000000..f72789e99ba --- /dev/null +++ b/reproduce_issue.groovy @@ -0,0 +1,83 @@ +import groovy.transform.Field + +@Field +String testSource = """ +package io.micronaut.reproduce + +import io.micronaut.http.client.exceptions.ReadTimeoutException +import spock.lang.Specification +import io.micronaut.http.client.exceptions.HttpClientException + +/** + * Reproduces the issue where ReadTimeoutException's serviceId remains null + * due to HttpClientException's `serviceIdLocked` flag being set during + * ReadTimeoutException's construction. + * + * If this test passes, the issue is reproduced (serviceId is null after attempting to set, + * and an IllegalStateException is thrown upon attempt). + * If this test fails, the issue is resolved (serviceId can be set). + */ +class ReproduceIssueSpec extends Specification { + + void "ReadTimeoutException serviceId is null and cannot be set"() { + setup: + // Use the static final instance as there is no public constructor for ReadTimeoutException + HttpClientException exception = ReadTimeoutException.TIMEOUT_EXCEPTION + + when: "An attempt is made to set the serviceId on the shared ReadTimeoutException instance" + exception.setServiceId("test-service-id") + + then: "An IllegalStateException should be thrown, indicating that the serviceId cannot be set" + thrown(IllegalStateException) + + and: "The serviceId on the exception should remain null after the failed attempt" + // Note: The `thrown` block intercepts the exception, so subsequent `and:` blocks + // are still executed. If the exception was thrown, the serviceId would indeed remain null. + exception.getServiceId() == null + } +} +""" + +def testDir = new File("test-suite/src/test/groovy/io/micronaut/reproduce") +def exitCode = 1 // Default to error + +try { + testDir.mkdirs() + def testFile = new File(testDir, "ReproduceIssueSpec.groovy") + testFile.write(testSource) + + println "Executing test to reproduce the issue..." + // Target the 'test-suite' project where the test file is located + def command = "./gradlew :test-suite:test --tests io.micronaut.reproduce.ReproduceIssueSpec" + def process = command.execute() + process.waitForProcessOutput(System.out, System.err) + def gradleExitValue = process.exitValue() + + // Gradle test exit codes: + // 0: All tests passed + // >0: Tests failed or other Gradle error + + if (gradleExitValue == 0) { + // If the Spock test "ReadTimeoutException serviceId is null and cannot be set" passes, + // it means the IllegalStateException was thrown AND getServiceId() was null, + // which confirms the issue IS reproduced. + println "Test passed: ReadTimeoutException's serviceId remained null and could not be set. Issue reproduced (exit code 129)." + exitCode = 129 + } else { + // If the Spock test failed, it means either the IllegalStateException was NOT thrown + // or getServiceId() was NOT null, meaning the issue is NOT reproduced/fixed. + println "Test failed: ReadTimeoutException's serviceId was settable or the expected exception was not thrown. Issue NOT reproduced (exit code 0)." + exitCode = 0 + } + +} catch (Exception e) { + e.printStackTrace() + exitCode = 1 // Script error +} finally { + // Clean up the created test file and directory + if (testDir.exists()) { + testDir.deleteDir() + } +} + +System.exit(exitCode) \ No newline at end of file