diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java index c34f872243a9..e598a08a8afa 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/HttpWebHandlerAdapter.java @@ -362,15 +362,13 @@ private Mono handleUnresolvedError( ServerHttpResponse response = exchange.getResponse(); String logPrefix = exchange.getLogPrefix(); - // Sometimes a remote call error can look like a disconnected client. - // Try to set the response first before the "isDisconnectedClient" check. - - if (response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR)) { - logger.error(logPrefix + "500 Server Error for " + formatRequest(request), ex); + // Check for client disconnect first; DisconnectedClientHelper already excludes remote-call wrappers. + if (disconnectedClientHelper.checkAndLogClientDisconnectedException(ex)) { + observationContext.setConnectionAborted(true); return Mono.empty(); } - else if (disconnectedClientHelper.checkAndLogClientDisconnectedException(ex)) { - observationContext.setConnectionAborted(true); + else if (response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR)) { + logger.error(logPrefix + "500 Server Error for " + formatRequest(request), ex); return Mono.empty(); } else { diff --git a/spring-web/src/test/java/org/springframework/web/server/adapter/HttpWebHandlerAdapterObservabilityTests.java b/spring-web/src/test/java/org/springframework/web/server/adapter/HttpWebHandlerAdapterObservabilityTests.java index aebfee9830bd..f782ab686015 100644 --- a/spring-web/src/test/java/org/springframework/web/server/adapter/HttpWebHandlerAdapterObservabilityTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/adapter/HttpWebHandlerAdapterObservabilityTests.java @@ -17,6 +17,7 @@ package org.springframework.web.server.adapter; +import java.io.IOException; import java.util.List; import java.util.Optional; @@ -25,7 +26,10 @@ import io.micrometer.observation.tck.TestObservationRegistry; import io.micrometer.observation.tck.TestObservationRegistryAssert; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.Mono; +import reactor.netty.channel.AbortedException; import reactor.test.StepVerifier; import org.springframework.http.HttpStatus; @@ -88,6 +92,36 @@ void handlerShouldRecordObservationWhenCancelled() { assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN"); } + @ParameterizedTest + @MethodSource("disconnectExceptions") + void handlerShouldMarkConnectionAbortedForClientDisconnect(Exception disconnectEx) { + // ThrowingExceptionWebHandler captures ServerRequestObservationContext from exchange + // attributes; handleUnresolvedError operates on the same object, so setConnectionAborted(true) + // is visible after block(). + ThrowingExceptionWebHandler throwingHandler = new ThrowingExceptionWebHandler(disconnectEx); + createWebHandler(throwingHandler).handle(this.request, this.response).block(); + assertThat(throwingHandler.observationContext).isPresent(); + assertThat(throwingHandler.observationContext.get().isConnectionAborted()).isTrue(); + } + + static List disconnectExceptions() { + // Verified by DisconnectedClientHelperTests.disconnectedExceptions() and exceptionPhrases() + return List.of( + new AbortedException(""), + new IOException("Broken pipe") + ); + } + + @Test + void handlerShouldNotMarkConnectionAbortedForServerError() { + ThrowingExceptionWebHandler throwingHandler = + new ThrowingExceptionWebHandler(new IllegalStateException("server error")); + createWebHandler(throwingHandler).handle(this.request, this.response).block(); + assertThat(throwingHandler.observationContext).isPresent(); + assertThat(throwingHandler.observationContext.get().isConnectionAborted()).isFalse(); + assertThat(this.response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + } + private HttpWebHandlerAdapter createWebHandler(WebHandler targetHandler) { HttpWebHandlerAdapter handlerAdapter = new HttpWebHandlerAdapter(targetHandler); handlerAdapter.setObservationRegistry(this.observationRegistry);