signalapp/Signal-Server · warning · IOException

IOExceptionMapper

Error message

IOExceptionMapper

What it means

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.

Solutions

  1. Read the logged stack trace ('IOExceptionMapper') for the root cause of the I/O failure.
  2. If caused by idle timeouts, tune the Jetty client idle timeout and distinguish via the exception message as the mapper does.
  3. Handle expected I/O errors inside resource methods and map them to appropriate 4xx statuses instead of letting them reach this mapper.
  4. For client disconnects, treat them as benign and avoid alerting on the DEBUG-classified idle timeout case.

Example fix

// before: long-poll call exceeding idle timeout, surfacing as 500
response = jettyClient.newRequest(url).send();

// after: align timeouts
response = jettyClient.newRequest(url)
    .timeout(30, TimeUnit.SECONDS)
    .send();
Defensive patterns

Strategy: try-catch

Try / catch

try {
  return resource.call();
} catch (IOException e) {
  if (ExceptionUtils.getRootCause(e) instanceof TimeoutException t && isIdleTimeout(t.getMessage())) {
    logger.debug("idle timeout, treating as client disconnect");
    return Response.status(499).build();
  }
  logger.warn("I/O failure in resource", e);
  return Response.status(500).build();
}

Prevention

When it happens

Trigger: 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.

Common situations: Clients disconnecting mid-request, upstream services exceeding idle timeout settings, misconfigured Jetty HttpClient idle timeouts, disk/network I/O errors in resource code.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/0c3ba17674adac0f. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/mappers/IOExceptionMapper.java:22

 */
package org.whispersystems.textsecuregcm.mappers;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Provider
public class IOExceptionMapper implements ExceptionMapper<IOException> {

  private final Logger logger = LoggerFactory.getLogger(IOExceptionMapper.class);

  @Override
  public Response toResponse(IOException e) {
    if (!(e.getCause() instanceof java.util.concurrent.TimeoutException)) {
      logger.warn("IOExceptionMapper", e);
    } 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();
  }
}

View on GitHub (pinned to 100ab61c82)