schollz/croc · error · Error
Relay stream did not start with croc framing
Error message
Relay stream did not start with croc framing
What it means
FrameDecoder.push() validates that the first 4 buffered bytes of each frame equal the croc magic ('c','r','o','c'). If they differ, the byte stream is not croc framing and the decoder clears its buffer and throws. This is the first integrity gate on data arriving from the relay.
Source
Thrown at web/src/protocol/framing.ts:29
frame.set(MAGIC, 0);
new DataView(frame.buffer).setUint32(4, payload.byteLength, true);
frame.set(payload, 8);
return frame;
}
export class FrameDecoder {
private buffer = new Uint8Array();
push(chunk: Uint8Array) {
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
- Inspect the first bytes of the rejected chunk (hex-dump the stream) to identify what protocol actually answered — usually an HTTP/proxy response indicating the wrong relay address.
- Verify the relay URL and scheme (wss vs wss-less, port) match a croc relay that speaks the framed protocol.
- Ensure you only push bytes received after the relay connection is fully established and no handshake preamble is prepended.
- After any prior framing error, recreate the FrameDecoder (its buffer is cleared) rather than continuing with the same connection.
Example fix
// before const decoder = new FrameDecoder(); ws.onmessage = (e) => pushAll(decoder, e.data); // mixed text/binary stream // after ws.binaryType = "arraybuffer"; const decoder = new FrameDecoder(); ws.onmessage = (e) => pushAll(decoder, new Uint8Array(e.data));
Defensive patterns
Strategy: try-catch
Validate before calling
const MAGIC = new Uint8Array([0x63, 0x72, 0x6f, 0x63]);
function startsWithCrocFraming(chunk: Uint8Array): boolean {
return chunk.length >= 4 && MAGIC.every((b, i) => chunk[i] === b);
} Try / catch
try {
for (const msg of decoder.push(chunk)) handle(msg);
} catch (error) {
if (error instanceof Error && /did not start with croc framing/.test(error.message)) {
await reconnectRelay(); // decoder buffer was cleared; restart the stream
return;
}
throw error;
} Prevention
- Verify the relay URL serves the croc framed protocol before piping its socket into FrameDecoder.
- Set ws.binaryType = 'arraybuffer' and never feed text frames or pre-handshake bytes to the decoder.
- Recreate the FrameDecoder and reconnect after any framing error — the old stream position is unrecoverable.
When it happens
Trigger: Pushing chunks from a WebSocket/relay stream whose initial bytes are not the croc magic: an HTTP error body or proxy greeting read as binary, a wrong relay URL serving a different protocol, TLS/plain mismatch, or a stream that desynced after an earlier framing bug. The check runs for every frame, so garbage at any frame boundary also triggers it.
Common situations: Connecting to a relay URL that returns an HTML error page; a reverse proxy or captive portal injecting non-binary data; accidentally piping a croc TCP stream and an HTTP handshake into the same socket; feeding the decoder a partial frame offset by stray bytes after a previous decode error; feeding it raw (unframed) croc protocol bytes.
Related errors
- Message is too large (${payload.byteLength} bytes)
- Relay frame is too large (${length} bytes)
- Relay returned an invalid port list: ${banner}
- Relay rejected the connection: ${response}
- Relay could not open the room: ${confirmation}
AI-assisted analysis of schollz/croc@e25f1bdc04 (2026-08-15).
Data as JSON: /api/errors/12e7d46941bc3dea.
Report an issue: GitHub.