clockworklabs/SpacetimeDB · critical · RangeError

Tried to read ${n} byte(s) at relative offset ${this.offset}

Error message

Tried to read ${n} byte(s) at relative offset ${this.offset}, but only ${this.remaining} byte(s) remain

What it means

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.

Source

Thrown at crates/bindings-typescript/src/lib/binary_reader.ts:43

    this.offset = 0;
  }

  reset(input: Uint8Array | DataView) {
    this.view =
      input instanceof DataView
        ? input
        : new DataView(input.buffer, input.byteOffset, input.byteLength);
    this.offset = 0;
  }

  get remaining(): number {
    return this.view.byteLength - this.offset;
  }

  /** Ensure we have at least `n` bytes left to read */
  #ensure(n: number): void {
    if (this.offset + n > this.view.byteLength) {
      throw new RangeError(
        `Tried to read ${n} byte(s) at relative offset ${this.offset}, but only ${this.remaining} byte(s) remain`
      );
    }
  }

  readUInt8Array(): Uint8Array {
    const length = this.readU32();
    this.#ensure(length);
    // Return an owned copy, not a view over the reader's buffer. Decoded column
    // values are handed to user code and may be retained, but the runtime reuses
    // this buffer across table scans, so a view would be silently invalidated by
    // the next iter()/filter(). Callers that only consume the bytes transiently
    // (e.g. readString) can use readBytes directly to avoid this copy.
    // https://github.com/clockworklabs/SpacetimeDB/issues/5490
    return this.readBytes(length).slice();
  }

  readBool(): boolean {

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Verify the deployed module's schema matches the TS bindings (redeploy the module, regenerate bindings)
  2. Sanity-check the input length before decoding and treat any failure as a poisoned stream, not a partial row
  3. Do not retain or replay views over reader buffers - readUInt8Array already returns owned copies
  4. Catch RangeError at the message-decode boundary and resubscribe/reconnect to resynchronize

Example fix

// before
const rows = decodeUpdate(new BinaryReader(bytes)); // truncated frame -> RangeError

// after
try {
  const rows = decodeUpdate(new BinaryReader(bytes));
} catch (e) {
  if (e instanceof RangeError) {
    // corrupt/truncated frame: drop it and resubscribe rather than continue
    await resubscribe();
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

function isPlausibleFrame(bytes: Uint8Array, minHeaderBytes = 4): boolean {
  return bytes.byteLength >= minHeaderBytes;
}
// Note: full validation is impossible before decoding; treat this as a cheap sanity gate only.

Try / catch

try {
  const rows = decodeMessage(new BinaryReader(bytes));
} catch (e) {
  if (e instanceof RangeError) {
    // Truncated/corrupt frame or schema desync: do not partially apply rows.
    logger.warn('decode overrun, resubscribing', { byteLength: bytes.byteLength });
    await resubscribe();
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16). Data as JSON: /api/errors/037e4c7d84664850. Report an issue: GitHub.