signalapp/Signal-Server · error · NoContentException
Empty body not allowed
Error message
Empty body not allowed
What it means
MultiRecipientMessageProvider.readFrom (a JAX-RS message body reader for SealedSenderMultiRecipientMessage) throws NoContentException when the request body is zero bytes. An empty body cannot be a valid sealed sender multi-recipient message, so the provider fails fast with an HTTP 204-style NoContentException instead of a parse error.
Solutions
- Send a non-empty serialized SealedSenderMultiRecipientMessage as the request body
- Check the HTTP client that the payload is attached and Content-Length/Transfer-Encoding is correct
- Inspect intermediaries (proxies, load balancers) that may drop empty or streamed bodies
Example fix
// before
await fetch('/v1/messages/multi', {method: 'PUT', headers: {'Content-Type': 'application/vnd.signal.protocol'}}); // no body
// after
await fetch('/v1/messages/multi', {method: 'PUT', headers: {'Content-Type': 'application/vnd.signal.protocol'}, body: serializedMessageBytes}); Defensive patterns
Strategy: validation
Validate before calling
const body = serializeMultiRecipientMessage(msg); if (!body || body.length === 0) { throw new Error('Refusing to send empty multi-recipient message body'); } Type guard
null
Try / catch
try { await putMultiRecipientMessage(body); } catch (e) { if (e.status === 204 || /Empty body/.test(e.message)) { logError('serialized body was empty — check serializer'); } } Prevention
- Assert the serialized message is non-empty before every send
- Verify Content-Length/Transfer-Encoding is set by your HTTP client
- Test message serialization in unit tests so empty output fails locally
When it happens
Trigger: PUT/POST of a multi-recipient message endpoint with Content-Length: 0 or no body, so entityStream.readNBytes(...) returns an empty array.
Common situations: HTTP clients that send headers but forget the body; proxies/gateways stripping bodies; test requests built with empty payloads; clients that serialize nothing on failure.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- return Response.status(499).build();
- Blank header
- end of range must be after start of range
- timestamps must be day aligned
- start of range too far in the past
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/a785ae2ba28af6a2.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/providers/MultiRecipientMessageProvider.java:52
public static final int MAX_RECIPIENT_COUNT = 5000;
public static final int MAX_MESSAGE_SIZE = Math.toIntExact(32 + DataSizeUnit.KIBIBYTES.toBytes(256));
private static final DistributionSummary RECIPIENT_COUNT_DISTRIBUTION = DistributionSummary
.builder(name(MultiRecipientMessageProvider.class, "recipients"))
.register(Metrics.globalRegistry);
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return MEDIA_TYPE.equals(mediaType.toString()) && SealedSenderMultiRecipientMessage.class.isAssignableFrom(type);
}
@Override
public SealedSenderMultiRecipientMessage readFrom(Class<SealedSenderMultiRecipientMessage> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
byte[] fullMessage = entityStream.readNBytes(MAX_MESSAGE_SIZE + MAX_RECIPIENT_COUNT * 100);
if (fullMessage.length == 0) {
throw new NoContentException("Empty body not allowed");
}
try {
final SealedSenderMultiRecipientMessage message = SealedSenderMultiRecipientMessage.parse(fullMessage);
RECIPIENT_COUNT_DISTRIBUTION.record(message.getRecipients().size());
return message;
} catch (InvalidMessageException | InvalidVersionException e) {
throw new BadRequestException(e);
}
}
}
View on GitHub (pinned to 100ab61c82)