{"record":{"id":"0c3ba17674adac0f","repo":"signalapp/Signal-Server","slug":"ioexceptionmapper","errorCode":null,"errorMessage":"IOExceptionMapper","messagePattern":"IOExceptionMapper","errorType":"http","errorClass":"IOException","httpStatus":503,"severity":"warning","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java","lineNumber":22,"sourceCode":" */\npackage org.whispersystems.textsecuregcm.mappers;\n\nimport jakarta.ws.rs.core.Response;\nimport jakarta.ws.rs.ext.ExceptionMapper;\nimport jakarta.ws.rs.ext.Provider;\nimport java.io.IOException;\nimport org.slf4j.Logger;\nimport org.slf4j.LoggerFactory;\n\n@Provider\npublic class IOExceptionMapper implements ExceptionMapper<IOException> {\n\n  private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class);\n\n  @Override\n  public Response toResponse(IOException e) {\n    if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) {\n      logger.warn(\"IOExceptionMapper\", e);\n    } else {\n      // Some TimeoutExceptions are because the connection is idle, but are only distinguishable using the exception\n      // message\n      final String message = e.getCause().getMessage();\n      final boolean idleTimeout =\n          message != null &&\n              // org.eclipse.jetty.io.IdleTimeout\n              (message.startsWith(\"Idle timeout expired\")\n                  // org.eclipse.jetty.http2.HTTP2Session\n                  || (message.startsWith(\"Idle timeout\") && message.endsWith(\"elapsed\")));\n      if (idleTimeout) {\n        return Response.status(Response.Status.REQUEST_TIMEOUT).build();\n      }\n    }\n\n    return Response.status(503).build();\n  }\n}","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java#L4-L40","documentation":"IOExceptionMapper converts uncaught IOExceptions from resource methods into HTTP 500 responses and logs them at WARN. If the cause is a java.util.concurrent.TimeoutException, it inspects the message to distinguish idle-connection timeouts from real timeouts: idle timeouts are logged at DEBUG, everything else at WARN with the stack trace.","triggerScenarios":"Any resource method lets an IOException escape without a more specific mapper — most commonly Jetty client idle timeouts during long-poll or upstream HTTP calls, or genuine I/O failures reading/writing request/response streams.","commonSituations":"Clients disconnecting mid-request, upstream services exceeding idle timeout settings, misconfigured Jetty HttpClient idle timeouts, disk/network I/O errors in resource code.","solutions":["Read the logged stack trace ('IOExceptionMapper') for the root cause of the I/O failure.","If caused by idle timeouts, tune the Jetty client idle timeout and distinguish via the exception message as the mapper does.","Handle expected I/O errors inside resource methods and map them to appropriate 4xx statuses instead of letting them reach this mapper.","For client disconnects, treat them as benign and avoid alerting on the DEBUG-classified idle timeout case."],"exampleFix":"// before: long-poll call exceeding idle timeout, surfacing as 500\nresponse = jettyClient.newRequest(url).send();\n\n// after: align timeouts\nresponse = jettyClient.newRequest(url)\n    .timeout(30, TimeUnit.SECONDS)\n    .send();","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  return resource.call();\n} catch (IOException e) {\n  if (ExceptionUtils.getRootCause(e) instanceof TimeoutException t && isIdleTimeout(t.getMessage())) {\n    logger.debug(\"idle timeout, treating as client disconnect\");\n    return Response.status(499).build();\n  }\n  logger.warn(\"I/O failure in resource\", e);\n  return Response.status(500).build();\n}","preventionTips":["Set Jetty HttpClient idle timeouts longer than the slowest legitimate upstream call.","Distinguish idle-timeout IOExceptions by message and log them at DEBUG, as the mapper does.","Don't page on this WARN unless the root cause is not a timeout."],"tags":["io","http","timeout","exception-mapper"],"backgroundTag":"http-error-response","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}