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

  1. Send a non-empty serialized SealedSenderMultiRecipientMessage as the request body
  2. Check the HTTP client that the payload is attached and Content-Length/Transfer-Encoding is correct
  3. 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

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


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)