From 9f6a6d7f5bcd5b3af20c880db23382da101b9028 Mon Sep 17 00:00:00 2001 From: Chris Eager Date: Tue, 5 Mar 2024 17:51:14 -0600 Subject: [PATCH] Include HTTP/2 stream idle timeouts in IOExceptionMapper --- .../mappers/IOExceptionMapper.java | 15 ++++++- .../mappers/IOExceptionMapperTest.java | 41 +++++++++++++++++++ 2 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 service/src/test/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapperTest.java 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) + ); + } + +}