diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java b/service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java index 54b6a5f3b..ea8b3ec1f 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java @@ -21,8 +21,19 @@ public class IOExceptionMapper implements ExceptionMapper { public Response toResponse(IOException e) { if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) { logger.warn("IOExceptionMapper", e); - } else if (e.getCause().getMessage().startsWith("Idle timeout expired")) { - return Response.status(Response.Status.REQUEST_TIMEOUT).build(); + } else { + // Some TimeoutExceptions are because the connection is idle, but are only distinguishable using the exception + // message + final String message = e.getCause().getMessage(); + final boolean idleTimeout = + message != null && + // org.eclipse.jetty.io.IdleTimeout + (message.startsWith("Idle timeout expired") + // org.eclipse.jetty.http2.HTTP2Session + || (message.startsWith("Idle timeout") && message.endsWith("elapsed"))); + if (idleTimeout) { + return Response.status(Response.Status.REQUEST_TIMEOUT).build(); + } } return Response.status(503).build(); diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapperTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapperTest.java new file mode 100644 index 000000000..c307f30bc --- /dev/null +++ b/service/src/test/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapperTest.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.whispersystems.textsecuregcm.mappers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.IOException; +import java.util.concurrent.TimeoutException; +import java.util.stream.Stream; +import javax.ws.rs.core.Response; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +class IOExceptionMapperTest { + + @ParameterizedTest + @MethodSource + void testExceptionParsing(final IOException exception, final int expectedStatus) { + + try (Response response = new IOExceptionMapper().toResponse(exception)) { + assertEquals(expectedStatus, response.getStatus()); + } + } + + static Stream testExceptionParsing() { + return Stream.of( + Arguments.of(new IOException(), 503), + Arguments.of(new IOException(new TimeoutException("A timeout")), 503), + Arguments.of(new IOException(new TimeoutException()), 503), + Arguments.of(new IOException(new TimeoutException("Idle timeout 30000 ms elapsed")), 408), + Arguments.of(new IOException(new TimeoutException("Idle timeout expired")), 408), + Arguments.of(new IOException(new RuntimeException(new TimeoutException("Idle timeout expired"))), 503), + Arguments.of(new IOException(new TimeoutException("Idle timeout of another kind expired")), 503) + ); + } + +}