decolua/9router · error

AWS EventStream prelude CRC mismatch

Error message

AWS EventStream prelude CRC mismatch

What it means

EventStream frames carry a CRC32 checksum of the 8-byte prelude (total length + headers length) at offset 8. parseEventFrame() recomputes CRC32 over data[0..8] and compares it to the stored value; a mismatch proves the prelude bytes were corrupted (or the frame is not a genuine EventStream frame). Throwing here prevents parsing headers from garbage lengths.

Source

Thrown at open-sse/executors/kiro.js:1212

 */

function parseEventFrame(data) {
  if (!(data instanceof Uint8Array) || data.byteLength < 16) {
    throw new Error("AWS EventStream frame is shorter than 16 bytes");
  }
  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
  const totalLength = view.getUint32(0, false);
  const headersLength = view.getUint32(4, false);
  if (totalLength !== data.byteLength) {
    throw new Error("AWS EventStream frame length does not match its prelude");
  }
  if (totalLength > EVENTSTREAM_MAX_MESSAGE_BYTES ||
      headersLength > EVENTSTREAM_MAX_HEADERS_BYTES ||
      headersLength > totalLength - 16) {
    throw new Error("AWS EventStream frame bounds are invalid");
  }
  if (view.getUint32(8, false) !== crc32(data.subarray(0, 8))) {
    throw new Error("AWS EventStream prelude CRC mismatch");
  }
  if (view.getUint32(totalLength - 4, false) !== crc32(data.subarray(0, totalLength - 4))) {
    throw new Error("AWS EventStream message CRC mismatch");
  }

  const headers = Object.create(null);
  const names = new Set();
  let offset = 12;
  const headerEnd = offset + headersLength;
  const requireBytes = (count) => {
    if (offset + count > headerEnd) {
      throw new Error("AWS EventStream header exceeds its declared bounds");
    }
  };

  while (offset < headerEnd) {
    requireBytes(1);
    const nameLength = data[offset++];

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Copy chunk bytes into a private buffer (Uint8Array.slice / new Uint8Array(bytes)) instead of holding references to reused stream buffers before parsing.
  2. Retry the request — transient bit corruption on the wire is the usual cause.
  3. Bypass any TLS-inspecting proxy/firewall for the Kiro endpoint and retry.
  4. Re-sync the frame stream: if one frame fails CRC, rescan for the next plausible frame boundary rather than continuing from a desynced offset.
  5. Update 9router / undici / Node — some versions had streaming bugs with binary response bodies.

Example fix

// before: keeping a view into a buffer the reader reuses
const frame = chunk.subarray(off, off + totalLength); // may be overwritten later
parseEventFrame(frame);

// after: copy before enqueueing/parsing
const frame = chunk.slice(off, off + totalLength); // independent copy
parseEventFrame(frame);
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const { headers, payload } = parseEventFrame(frame);
  handleFrame(headers, payload);
} catch (e) {
  if (e.message.includes('prelude CRC mismatch')) {
    log.warn('kiro: prelude corrupted in transit, retrying request');
    await retryWithBackoff();
  } else throw e;
}

Prevention

When it happens

Trigger: crc32(data.subarray(0, 8)) !== view.getUint32(8, false): the prelude bytes were altered in transit, the buffer was mutated/overwritten between reads (e.g. a shared/aliased ArrayBuffer reused by the reader), or non-EventStream bytes are being parsed as a frame.

Common situations: Unstable network path or flaky proxy flipping bytes; a custom fetch/undici dispatcher reusing a pooled buffer without copying before the next read; TLS interception that mangles binary content; desynced parsing where mid-payload bytes are treated as a frame start.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/0f1388a777dc1aed. Report an issue: GitHub.