aeron-io/aeron · error · ArchiveException

expected schemaId=

Error message

expected schemaId=

What it means

RecordingSignalAdapter.onFragment wraps the incoming SBE buffer as a MessageHeader and verifies the schemaId matches the archive protocol's MessageHeaderDecoder.SCHEMA_ID (1055). A mismatch means the fragment was not produced by the Aeron Archive control-protocol SBE schema, so decoding would be unsafe. The library throws ArchiveException to abort processing of the corrupted/foreign message.

Solutions

  1. Ensure only Aeron Archive control-protocol messages are published to the channel this adapter subscribes to
  2. Use matching aeron-archive and aeron-client jar versions on sender and receiver so the SBE schema id is identical
  3. Verify the fragment offset passed to the adapter comes directly from the Image/FragmentHandler (no manual offset arithmetic)
  4. Regenerate codecs from the same aeron-archive aeron-archive-codecs version used by the archive

Example fix

// before: subscribing to a shared app channel
Aeron.addSubscription("aeron:udp?endpoint=app.com", appStreamId, recordingSignalAdapter::onFragment);
// after: dedicated archive control channel/stream
Aeron.addSubscription("aeron:udp?endpoint=localhost:8010", archiveControlStreamId, recordingSignalAdapter::onFragment);
Defensive patterns

Strategy: try-catch

Validate before calling

// verify the stream only carries archive control SBE messages and codecs match:
if (messageHeaderDecoder.schemaId() != MessageHeaderDecoder.SCHEMA_ID) { /* route to dead-letter/log */ }

Type guard

boolean isArchiveSbeBuffer(DirectBuffer buffer, int offset) {
    MessageHeaderDecoder h = new MessageHeaderDecoder();
    h.wrap(buffer, offset);
    return h.schemaId() == MessageHeaderDecoder.SCHEMA_ID;
}

Try / catch

try { adapter.onFragment(buffer, offset, length, header); }
catch (ArchiveException e) { if (e.getMessage().startsWith("expected schemaId")) { log.error("foreign message on control stream", e); } }

Prevention

When it happens

Trigger: A fragment delivered to the control-response subscription whose first bytes are not an Archive SBE message header — e.g. the subscription is pointed at a publication sending raw/non-SBE payloads, a different SBE schema was used to encode, or a buffer offset bug causes the header to be read at the wrong position.

Common situations: Mixing Aeron versions where the archive schema id changed; a custom app publishing plain JSON/binary onto the channel that the RecordingSignalAdapter subscribes to; misconfigured replay/subscription URI receiving the wrong stream.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of aeron-io/aeron@6d60124e15 (2026-09-12). Data as JSON: /api/errors/1ca161c16e067c53. Report an issue: GitHub.

Appendix: source

Thrown at aeron-archive/src/main/java/io/aeron/archive/client/RecordingSignalAdapter.java:112

    public boolean isDone()
    {
        return isDone;
    }

    private ControlledFragmentHandler.Action onFragment(
        final DirectBuffer buffer, final int offset, final int length, final Header header)
    {
        if (isDone)
        {
            return ABORT;
        }

        messageHeaderDecoder.wrap(buffer, offset);

        final int schemaId = messageHeaderDecoder.schemaId();
        if (schemaId != MessageHeaderDecoder.SCHEMA_ID)
        {
            throw new ArchiveException("expected schemaId=" + MessageHeaderDecoder.SCHEMA_ID + ", actual=" + schemaId);
        }

        switch (messageHeaderDecoder.templateId())
        {
            case ControlResponseDecoder.TEMPLATE_ID:
                controlResponseDecoder.wrap(
                    buffer,
                    offset + MessageHeaderDecoder.ENCODED_LENGTH,
                    messageHeaderDecoder.blockLength(),
                    messageHeaderDecoder.version());

                if (controlResponseDecoder.controlSessionId() == controlSessionId)
                {
                    controlEventListener.onResponse(
                        controlSessionId,
                        controlResponseDecoder.correlationId(),
                        controlResponseDecoder.relevantId(),
                        controlResponseDecoder.code(),

View on GitHub (pinned to 6d60124e15)