diff --git a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureExceptions.java b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureExceptions.java index 7bfd740b4..e1a5e8329 100644 --- a/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureExceptions.java +++ b/conjure-java-undertow-runtime/src/main/java/com/palantir/conjure/java/undertow/runtime/ConjureExceptions.java @@ -32,6 +32,7 @@ import com.palantir.logsafe.SafeArg; import com.palantir.logsafe.logger.SafeLogger; import com.palantir.logsafe.logger.SafeLoggerFactory; +import io.undertow.UndertowMessages; import io.undertow.io.UndertowOutputStream; import io.undertow.server.HttpServerExchange; import io.undertow.util.Headers; @@ -40,6 +41,7 @@ import java.io.OutputStream; import java.time.temporal.ChronoUnit; import java.util.Collections; +import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; import org.xnio.IoUtils; @@ -56,6 +58,9 @@ public enum ConjureExceptions implements ExceptionHandler { private static final Serializer serializer = new ConjureBodySerDe(Collections.singletonList(Encodings.json())).serializer(new TypeMarker<>() {}); + private static final String COULD_NOT_READ_CONTENT_LENGTH_DATA_MESSAGE = + UndertowMessages.MESSAGES.couldNotReadContentLengthData().getMessage(); + // Log at most once every second private static final RateLimiter qosLoggingRateLimiter = RateLimiter.create(1); @@ -65,24 +70,22 @@ public void handle(HttpServerExchange exchange, Throwable throwable) { setFailure(exchange, throwable); if (throwable instanceof CheckedServiceException checkedServiceException) { checkedServiceException(exchange, checkedServiceException); - } else if (throwable instanceof ServiceException) { - serviceException(exchange, (ServiceException) throwable); - } else if (throwable instanceof QosException) { - qosException(exchange, (QosException) throwable); - } else if (throwable instanceof RemoteException) { - remoteException(exchange, (RemoteException) throwable); + } else if (throwable instanceof ServiceException serviceException) { + serviceException(exchange, serviceException); + } else if (throwable instanceof QosException qosException) { + qosException(exchange, qosException); + } else if (throwable instanceof RemoteException remoteException) { + remoteException(exchange, remoteException); } else if (throwable instanceof IllegalArgumentException) { illegalArgumentException(exchange, throwable); - } else if (throwable instanceof FrameworkException) { - frameworkException(exchange, (FrameworkException) throwable); - } else if (throwable instanceof DeadlineExpiredException) { - deadlineExpiredException(exchange, (DeadlineExpiredException) throwable); - } else if (throwable instanceof Error) { - error(exchange, (Error) throwable); - } else if (throwable instanceof IOException && !exchange.getConnection().isOpen()) { - log.info( - "I/O exception from a closed connection. The request may have been aborted by the client", - throwable); + } else if (throwable instanceof FrameworkException frameworkException) { + frameworkException(exchange, frameworkException); + } else if (throwable instanceof DeadlineExpiredException deadlineExpiredException) { + deadlineExpiredException(exchange, deadlineExpiredException); + } else if (throwable instanceof Error error) { + error(exchange, error); + } else if (throwable instanceof IOException ioException) { + ioException(exchange, ioException); } else { ServiceException exception = new ServiceException(ErrorType.INTERNAL, throwable); log(exception, throwable); @@ -199,6 +202,31 @@ private static void deadlineExpiredException(HttpServerExchange exchange, Deadli exception, exchange, UndertowDeadlineReasonResponseEncodingAdapter.INSTANCE); } + private static void ioException(HttpServerExchange exchange, IOException ioException) { + if (!exchange.getConnection().isOpen()) { + log.info( + "I/O exception from a closed connection. The request may have been aborted by the client", + ioException); + return; + } + + if (Objects.equals(ioException.getMessage(), COULD_NOT_READ_CONTENT_LENGTH_DATA_MESSAGE)) { + log.info( + "Remote peer closed connection before all data could be read. " + + "The request may have been aborted by the client.", + ioException); + IoUtils.safeClose(exchange.getConnection()); + return; + } + + ServiceException exception = new ServiceException(ErrorType.INTERNAL, ioException); + log(exception, ioException); + writeResponse( + exchange, + Optional.of(ConjureError.fromServiceException(exception)), + exception.getErrorType().httpErrorCode()); + } + private static void error(HttpServerExchange exchange, Error error) { // log errors in order to associate the log line with the correct traceId but // avoid doing work beyond setting a 500 response code, no response body is sent. diff --git a/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/ConjureExceptionHandlerTest.java b/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/ConjureExceptionHandlerTest.java index 2e6dcd218..4c8797311 100644 --- a/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/ConjureExceptionHandlerTest.java +++ b/conjure-java-undertow-runtime/src/test/java/com/palantir/conjure/java/undertow/runtime/ConjureExceptionHandlerTest.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.InstanceOfAssertFactories.LIST; import static org.assertj.core.api.InstanceOfAssertFactories.MAP; @@ -39,12 +40,14 @@ import com.palantir.deadlines.DeadlineExpiredException; import com.palantir.logsafe.SafeArg; import io.undertow.Undertow; +import io.undertow.UndertowMessages; import io.undertow.server.HttpHandler; import io.undertow.server.handlers.BlockingHandler; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; +import java.net.SocketException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.Duration; @@ -361,6 +364,15 @@ public void handlesErrorWithoutRethrowing() { .doesNotThrowAnyException(); } + @Test + public void handlesPeerRemoteExceptionsWithoutRethrowing() throws IOException { + exception = UndertowMessages.MESSAGES.couldNotReadContentLengthData(); + HttpURLConnection connection = execute(); + assertThatThrownBy(() -> connection.getResponseCode()) + .isInstanceOf(SocketException.class) + .hasMessageContaining("Unexpected end of file from server"); + } + private static String getErrorBody(HttpURLConnection connection) { try (InputStream response = connection.getErrorStream()) { return new String(response.readAllBytes(), StandardCharsets.UTF_8);