clockworklabs/SpacetimeDB · error · Error

Invalid JSON: failed to parse string

Error message

Invalid JSON: failed to parse string

What it means

`parseJsonObject` wraps `JSON.parse` and rethrows a bare string parse failure as `Invalid JSON: failed to parse string` (a sibling check enforces a top-level object). Its main internal caller is `JwtClaimsImpl`, which parses the raw JWT payload of the caller's token when you access `ctx.auth.jwt` inside a reducer/procedure. So this error usually means the connection presented a token whose payload segment is not valid JSON, or you passed a non-JSON string to `parseJsonObject`/claim parsing yourself.

Solutions

  1. Reproduce the token payload outside the module: base64url-decode the middle segment of the token and run `JSON.parse` on it to see the exact parse problem.
  2. Fix the token issuer so the JWT payload segment is base64url-encoded UTF-8 JSON (e.g. `base64url(JSON.stringify(claims))`).
  3. If you call `parseJsonObject` yourself, validate/trim the input first and wrap the call in try/catch to treat malformed input as an application error.
  4. In tests, build tokens with a standard JWT library instead of concatenating strings by hand.

Example fix

// before
const claims = ctx.auth.jwt; // throws: Invalid JSON: failed to parse string

// after — guard before touching claims, treat bad token as unauthenticated
let claims: JwtClaims | null = null;
try {
  claims = ctx.auth.jwt;
} catch {
  ctx.log.error('sender presented a malformed token');
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Validate a token's payload is JSON before relying on ctx.auth.jwt.
function tokenPayloadParses(token: string): boolean {
  const parts = token.split('.');
  if (parts.length !== 3) return false;
  try {
    const json = atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'));
    return JSON.parse(json) !== null && typeof JSON.parse(json) === 'object';
  } catch {
    return false;
  }
}

Try / catch

try { const claims = ctx.auth.jwt; /* ... */ } catch (e) { if (e instanceof Error && e.message.includes('Invalid JSON')) { /* treat sender as unauthenticated */ } else throw e; }

Prevention

When it happens

Trigger: Accessing `ctx.auth.jwt` (claims) when the sender connected with a custom/malformed token whose payload is not valid JSON; calling `parseJsonObject` on a truncated string, empty string, or on a JSON array/scalar is fine for this specific message only if the string itself fails to parse (arrays instead hit the sibling 'Expected a JSON object' error); passing a base64-encoded (undecoded) payload string.

Common situations: Generating a custom auth token with a signing library but forgetting to base64url-encode/decode the JSON payload; hand-crafting tokens in tests; a token-issuing service that emits `undefined` payload or URL-safe base64 with padding stripped incorrectly; a gateway rewriting headers.

Understand the failure class

Related errors


AI-assisted analysis of clockworklabs/SpacetimeDB@eddf9f5014 (2026-08-20). Data as JSON: /api/errors/b71ac94b3408d0ad. Report an issue: GitHub.

Appendix: source

Thrown at crates/bindings-typescript/src/server/runtime.ts:111

function responseIntoWire(response: SyncResponse): [HttpResponse, Uint8Array] {
  return [
    {
      headers: serializeHeaders(response.headers),
      version: response.version,
      code: response.status,
    },
    response.bytes(),
  ];
}

export function parseJsonObject(json: string): JsonObject {
  let value: unknown;

  try {
    value = JSON.parse(json);
  } catch {
    throw new Error('Invalid JSON: failed to parse string');
  }

  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
    throw new Error('Expected a JSON object at the top level');
  }

  // The runtime check above guarantees this cast is safe
  return value as JsonObject;
}

class JwtClaimsImpl implements JwtClaims {
  readonly fullPayload: JsonObject;
  private readonly _identity: Identity;
  /**
   * Creates a new JwtClaims instance.
   * @param rawPayload The JWT payload as a raw JSON string.
   * @param identity The identity for this JWT. We are only taking this because we don't have a blake3 implementation (which we need to compute it).
   */

View on GitHub (pinned to eddf9f5014)