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
- 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.
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
- 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.
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
- return Response.status(503).build();
- registration service unavailable
- 503 Service Unavailable
- Empty body not allowed
- Response body was below minimum
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)