{"record":{"id":"c007ebd0a5cbb44a","repo":"decolua/9router","slug":"aws-eventstream-frame-length-does-not-match-its-pr","errorCode":null,"errorMessage":"AWS EventStream frame length does not match its prelude","messagePattern":"AWS EventStream frame length does not match its prelude","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/executors/kiro.js","lineNumber":1204,"sourceCode":"      log?.error?.(\"TOKEN\", `Kiro refresh error: ${error.message}`);\n      return null;\n    }\n  }\n}\n\n/**\n * Parse AWS EventStream frame\n */\n\nfunction parseEventFrame(data) {\n  if (!(data instanceof Uint8Array) || data.byteLength < 16) {\n    throw new Error(\"AWS EventStream frame is shorter than 16 bytes\");\n  }\n  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n  const totalLength = view.getUint32(0, false);\n  const headersLength = view.getUint32(4, false);\n  if (totalLength !== data.byteLength) {\n    throw new Error(\"AWS EventStream frame length does not match its prelude\");\n  }\n  if (totalLength > EVENTSTREAM_MAX_MESSAGE_BYTES ||\n      headersLength > EVENTSTREAM_MAX_HEADERS_BYTES ||\n      headersLength > totalLength - 16) {\n    throw new Error(\"AWS EventStream frame bounds are invalid\");\n  }\n  if (view.getUint32(8, false) !== crc32(data.subarray(0, 8))) {\n    throw new Error(\"AWS EventStream prelude CRC mismatch\");\n  }\n  if (view.getUint32(totalLength - 4, false) !== crc32(data.subarray(0, totalLength - 4))) {\n    throw new Error(\"AWS EventStream message CRC mismatch\");\n  }\n\n  const headers = Object.create(null);\n  const names = new Set();\n  let offset = 12;\n  const headerEnd = offset + headersLength;\n  const requireBytes = (count) => {","sourceCodeStart":1186,"sourceCodeEnd":1222,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/executors/kiro.js#L1186-L1222","documentation":"AWS EventStream frames encode their own total byte length in the first 4 bytes (big-endian). parseEventFrame() throws this when the declared totalLength does not equal the actual byteLength of the buffer — the frame boundary computed from the prelude disagrees with the data received. This guards against mis-assembled frames and corrupted or forged framing bytes.","triggerScenarios":"A frame buffer is passed to parseEventFrame whose byte count differs from view.getUint32(0, false): the caller sliced/concated the stream buffer at the wrong offset, dropped or duplicated bytes, or the frame's length header itself was corrupted in transit.","commonSituations":"Manual chunk reassembly with an off-by-one/off-by-CRC (16-byte) error; a frame split across TCP/HTTP2 segments and re-joined incorrectly; a buggy custom proxy or HTTP interceptor rewriting the body; corrupted padding when a partial final frame is padded to a boundary.","solutions":["Fix the stream assembler: split the buffer exactly on the totalLength read from each prelude, consuming frames sequentially with no gaps or overlaps.","Retry the request — corruption usually originates from the upstream connection, not your code.","Bypass any MITM/transforming proxy for the Kiro endpoint to rule out body rewriting.","Log the expected vs actual byteLength and the prelude totalLength to identify whether your slicing logic or the network corrupted the frame.","Update 9router if the Kiro upstream changed framing (e.g. new padding or compression)."],"exampleFix":"// before: advancing by data.length instead of the frame's declared length\nconst frame = buf.subarray(0, 1024);\nparseEventFrame(frame);\n\n// after: slice on the prelude-declared totalLength\nconst totalLength = new DataView(buf.buffer, buf.byteOffset).getUint32(0, false);\nif (buf.byteLength < totalLength) return; // wait for more bytes\nparseEventFrame(buf.subarray(0, totalLength));\nbuf = buf.subarray(totalLength);","handlingStrategy":"validation","validationCode":"function frameLengthMatches(data) {\n  if (!(data instanceof Uint8Array) || data.byteLength < 16) return false;\n  const view = new DataView(data.buffer, data.byteOffset, data.byteLength);\n  return view.getUint32(0, false) === data.byteLength;\n}\nif (frameLengthMatches(buf)) parseEventFrame(buf);","typeGuard":"function isWellFramedBuffer(v) {\n  return v instanceof Uint8Array && v.byteLength >= 16 &&\n    new DataView(v.buffer, v.byteOffset, v.byteLength).getUint32(0, false) === v.byteLength;\n}","tryCatchPattern":"try {\n  parseEventFrame(frame);\n} catch (e) {\n  if (e.message.includes('does not match its prelude')) {\n    log.error('kiro: framing desync, expected', frame.byteLength, 'declared', declaredLength);\n    resetStreamAndRetry();\n  } else throw e;\n}","preventionTips":["Advance the parse offset by exactly the prelude totalLength after each frame — never by chunk length.","Wait for the full frame (totalLength bytes buffered) before calling parseEventFrame.","Log declared vs actual length on failure to distinguish assembler bugs from network corruption.","Keep a single well-tested frame-splitting routine instead of ad-hoc slicing at call sites."],"tags":["eventstream","binary-protocol","framing","kiro","corrupt-frame"],"backgroundTag":"eventstream-frame-length-mismatch","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}