{"record":{"id":"12e7d46941bc3dea","repo":"schollz/croc","slug":"relay-stream-did-not-start-with-croc-framing","errorCode":null,"errorMessage":"Relay stream did not start with croc framing","messagePattern":"Relay stream did not start with croc framing","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"web/src/protocol/framing.ts","lineNumber":29,"sourceCode":"  frame.set(MAGIC, 0);\n  new DataView(frame.buffer).setUint32(4, payload.byteLength, true);\n  frame.set(payload, 8);\n  return frame;\n}\n\nexport class FrameDecoder {\n  private buffer = new Uint8Array();\n\n  push(chunk: Uint8Array) {\n    this.buffer =\n      this.buffer.byteLength === 0 ? chunk.slice() : concatBytes(this.buffer, chunk);\n    const messages: Uint8Array[] = [];\n\n    while (this.buffer.byteLength >= 8) {\n      for (let index = 0; index < MAGIC.byteLength; index += 1) {\n        if (this.buffer[index] !== MAGIC[index]) {\n          this.buffer = new Uint8Array();\n          throw new Error(\"Relay stream did not start with croc framing\");\n        }\n      }\n      const length = new DataView(\n        this.buffer.buffer,\n        this.buffer.byteOffset,\n        this.buffer.byteLength,\n      ).getUint32(4, true);\n      if (length > MAX_FRAME_SIZE) {\n        this.buffer = new Uint8Array();\n        throw new Error(`Relay frame is too large (${length} bytes)`);\n      }\n      if (this.buffer.byteLength < length + 8) break;\n      messages.push(this.buffer.slice(8, length + 8));\n      this.buffer = this.buffer.slice(length + 8);\n    }\n\n    return messages;\n  }","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/framing.ts#L11-L47","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst decoder = new FrameDecoder();\nws.onmessage = (e) => pushAll(decoder, e.data); // mixed text/binary stream\n// after\nws.binaryType = \"arraybuffer\";\nconst decoder = new FrameDecoder();\nws.onmessage = (e) => pushAll(decoder, new Uint8Array(e.data));","handlingStrategy":"try-catch","validationCode":"const MAGIC = new Uint8Array([0x63, 0x72, 0x6f, 0x63]);\nfunction startsWithCrocFraming(chunk: Uint8Array): boolean {\n  return chunk.length >= 4 && MAGIC.every((b, i) => chunk[i] === b);\n}","typeGuard":null,"tryCatchPattern":"try {\n  for (const msg of decoder.push(chunk)) handle(msg);\n} catch (error) {\n  if (error instanceof Error && /did not start with croc framing/.test(error.message)) {\n    await reconnectRelay(); // decoder buffer was cleared; restart the stream\n    return;\n  }\n  throw error;\n}","preventionTips":["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."],"tags":["protocol","framing","relay","stream-corruption"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}