signalapp/Signal-Server · error

return Response.status(503).build();

Error message

return Response.status(503).build();

What it means

IOExceptionMapper maps generic java.io.IOExceptions to HTTP 503 Service Unavailable as a catch-all. Before that, it inspects the exception message: client-abort / broken-pipe style messages return 400, and Jetty 'Idle timeout ... elapsed' messages return 408 Request Timeout. The final 503 means an I/O failure occurred while handling the request and no more specific mapping applied.

Solutions

  1. Check server logs for the underlying IOException root cause (storage, network, dependency)
  2. Verify connectivity/health of external dependencies (attachment storage, database)
  3. If client disconnects are being misclassified as 503, extend the mapper's message patterns for that IO message
  4. Add retries with backoff on the client side; 503 here is effectively transient

Example fix

// before
code = response.code(); // 503 treated as permanent failure
// after
if (response.code() == 503) {
  retryWithExponentialBackoff(request, maxAttempts = 3);
}
Defensive patterns

Strategy: retry

Try / catch

if (response.code() == 503) {
  retryWithExponentialBackoff(request, 3); // transient IO failure server-side
} else if (response.code() == 408 || response.code() == 400) {
  reconnectAndResend(); // client abort/idle timeout
}

Prevention

When it happens

Trigger: Any request handler throws an IOException not matching the early-EOF/idle-timeout patterns — e.g. failures reading request bodies, storage/attachment I/O errors, or downstream stream failures.

Common situations: Backend storage (S3/attachment service) connectivity problems; client disconnecting mid-upload causing IO variants not matched by the mapper's patterns; transient network faults between service and dependencies.

Related errors


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

Appendix: source

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

  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)