{"record":{"id":"037e4c7d84664850","repo":"clockworklabs/SpacetimeDB","slug":"tried-to-read-n-byte-s-at-relative-offset-th","errorCode":null,"errorMessage":"Tried to read ${n} byte(s) at relative offset ${this.offset}, but only ${this.remaining} byte(s) remain","messagePattern":"Tried to read (.+?) byte\\(s\\) at relative offset (.+?), but only (.+?) byte\\(s\\) remain","errorType":"exception","errorClass":"RangeError","httpStatus":null,"severity":"critical","filePath":"crates/bindings-typescript/src/lib/binary_reader.ts","lineNumber":43,"sourceCode":"    this.offset = 0;\n  }\n\n  reset(input: Uint8Array | DataView) {\n    this.view =\n      input instanceof DataView\n        ? input\n        : new DataView(input.buffer, input.byteOffset, input.byteLength);\n    this.offset = 0;\n  }\n\n  get remaining(): number {\n    return this.view.byteLength - this.offset;\n  }\n\n  /** Ensure we have at least `n` bytes left to read */\n  #ensure(n: number): void {\n    if (this.offset + n > this.view.byteLength) {\n      throw new RangeError(\n        `Tried to read ${n} byte(s) at relative offset ${this.offset}, but only ${this.remaining} byte(s) remain`\n      );\n    }\n  }\n\n  readUInt8Array(): Uint8Array {\n    const length = this.readU32();\n    this.#ensure(length);\n    // Return an owned copy, not a view over the reader's buffer. Decoded column\n    // values are handed to user code and may be retained, but the runtime reuses\n    // this buffer across table scans, so a view would be silently invalidated by\n    // the next iter()/filter(). Callers that only consume the bytes transiently\n    // (e.g. readString) can use readBytes directly to avoid this copy.\n    // https://github.com/clockworklabs/SpacetimeDB/issues/5490\n    return this.readBytes(length).slice();\n  }\n\n  readBool(): boolean {","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings-typescript/src/lib/binary_reader.ts#L25-L61","documentation":"BinaryReader.#ensure(n) runs before fixed-size reads and after reading U32 length prefixes (e.g. readUInt8Array reads a count then ensures that many bytes remain). If offset + n exceeds the view's byteLength, it throws this RangeError: the byte stream is shorter than the type being decoded says it must be.","triggerScenarios":"Decoding a truncated or wrongly-sliced buffer; a U32 length prefix declaring more bytes than remain; decoding bytes produced by a module whose schema (field types/order) differs from the client's bindings; reusing a reader whose offset is already at the end.","commonSituations":"Version drift between the deployed SpacetimeDB module and the TS bindings; partially received or incorrectly concatenated WebSocket frames; replaying cached buffers that the runtime reuses across table scans.","solutions":["Verify the deployed module's schema matches the TS bindings (redeploy the module, regenerate bindings)","Sanity-check the input length before decoding and treat any failure as a poisoned stream, not a partial row","Do not retain or replay views over reader buffers - readUInt8Array already returns owned copies","Catch RangeError at the message-decode boundary and resubscribe/reconnect to resynchronize"],"exampleFix":"// before\nconst rows = decodeUpdate(new BinaryReader(bytes)); // truncated frame -> RangeError\n\n// after\ntry {\n  const rows = decodeUpdate(new BinaryReader(bytes));\n} catch (e) {\n  if (e instanceof RangeError) {\n    // corrupt/truncated frame: drop it and resubscribe rather than continue\n    await resubscribe();\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"function isPlausibleFrame(bytes: Uint8Array, minHeaderBytes = 4): boolean {\n  return bytes.byteLength >= minHeaderBytes;\n}\n// Note: full validation is impossible before decoding; treat this as a cheap sanity gate only.","typeGuard":null,"tryCatchPattern":"try {\n  const rows = decodeMessage(new BinaryReader(bytes));\n} catch (e) {\n  if (e instanceof RangeError) {\n    // Truncated/corrupt frame or schema desync: do not partially apply rows.\n    logger.warn('decode overrun, resubscribing', { byteLength: bytes.byteLength });\n    await resubscribe();\n  } else {\n    throw e;\n  }\n}","preventionTips":["Treat a decode overrun as a poisoned stream: drop the whole message and resynchronize, never apply partial rows","Pin module and bindings versions together in CI so schema drift is caught before runtime","Assert minimum frame lengths before decoding as a cheap early signal"],"tags":["deserialization","binary-protocol","spacetimedb","range-error","data-corruption"],"backgroundTag":"unexpected-end-of-buffer","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}