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
- Confirm the peer frames with the same layout: little-endian ('true' in setUint32/getUint32) uint32 length after the 4-byte magic.
- 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.
- 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.
- 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
- Write custom encoders with the exact header layout: 4-byte magic + little-endian uint32 length.
- Keep both peers' MAX_FRAME_SIZE identical so legitimate frames are never rejected.
- Treat repeated oversized-length headers from one peer as hostile and disconnect.
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
- Message is too large (${payload.byteLength} bytes)
- Relay stream did not start with croc framing
- Sender did not complete the croc PAKE handshake
- Relay returned an invalid port list: ${banner}
- Peer sent an unexpected control handshake
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/63ec1653f4e9ba6d.
Report an issue: GitHub.