schollz/croc · error · Error

Relay frame is too large (${length} bytes)

Error message

Relay frame is too large (${length} bytes)

What it means

After verifying the croc magic, FrameDecoder.push() reads the little-endian uint32 length prefix and rejects any frame whose declared length exceeds MAX_FRAME_SIZE (64 MiB), clearing its buffer. The cap prevents a corrupt or hostile length field from causing a multi-gigabyte allocation.

Source

Thrown at web/src/protocol/framing.ts:39

    this.buffer =
      this.buffer.byteLength === 0 ? chunk.slice() : concatBytes(this.buffer, chunk);
    const messages: Uint8Array[] = [];

    while (this.buffer.byteLength >= 8) {
      for (let index = 0; index < MAGIC.byteLength; index += 1) {
        if (this.buffer[index] !== MAGIC[index]) {
          this.buffer = new Uint8Array();
          throw new Error("Relay stream did not start with croc framing");
        }
      }
      const length = new DataView(
        this.buffer.buffer,
        this.buffer.byteOffset,
        this.buffer.byteLength,
      ).getUint32(4, true);
      if (length > MAX_FRAME_SIZE) {
        this.buffer = new Uint8Array();
        throw new Error(`Relay frame is too large (${length} bytes)`);
      }
      if (this.buffer.byteLength < length + 8) break;
      messages.push(this.buffer.slice(8, length + 8));
      this.buffer = this.buffer.slice(length + 8);
    }

    return messages;
  }
}

View on GitHub (pinned to e25f1bdc04)

Solutions

  1. Confirm the peer frames with the same layout: little-endian ('true' in setUint32/getUint32) uint32 length after the 4-byte magic.
  2. Verify the peer's own frame cap does not exceed 64 MiB; if it legitimately sends bigger frames, raise MAX_FRAME_SIZE on both ends together.
  3. Hex-dump the 8-byte header of the failing frame to check for desync (e.g. magic appearing later in the stream), which indicates an upstream truncation bug.
  4. Treat a repeat occurrence from the same peer as hostile and drop the connection.

Example fix

// before (custom encoder using big-endian)
view.setUint32(4, payload.byteLength); // defaults to big-endian
// after
view.setUint32(4, payload.byteLength, true); // little-endian, matches FrameDecoder
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const messages = decoder.push(chunk);
} catch (error) {
  if (error instanceof Error && /frame is too large/.test(error.message)) {
    // corrupt or hostile length prefix: drop the connection, do not retry the stream
    socket.close();
    throw new Error("relay stream corrupted (oversized frame header)");
  }
  throw error;
}

Prevention

When it happens

Trigger: A frame header whose 4 length bytes decode to > 67108864 — caused by stream desync (bytes shifted so data is read as length), a peer that really does frame a >64 MiB message, memory corruption, or a malicious relay/peer probing the client.

Common situations: Peer built with a larger frame limit than this client; byte-level desync after a truncated frame or a chunk lost mid-stream; fuzzing the relay stream; endianness mistakes in a custom encoder (big-endian length for a small frame decodes to a huge number, e.g. 8 → 134217728).

Related errors


AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15). Data as JSON: /api/errors/63ec1653f4e9ba6d. Report an issue: GitHub.