clockworklabs/SpacetimeDB · critical

Failed to parse JWT payload

Error message

Failed to parse JWT payload

What it means

JwtClaims wraps the JWT payload string supplied by the host via ctx.jwt() - the verified claims document as JSON. get_parsed lazily parses it with serde_json, and this .expect panics if it is not valid JSON. In production the host always provides well-formed claims, so this mostly trips in unit tests using AuthCtx::from_jwt_payload with a hand-written string (e.g. a raw compact JWT or a base64 fragment).

Source

Thrown at crates/bindings/src/lib.rs:1930

    ///
    /// [JWT]: https://en.wikipedia.org/wiki/JSON_Web_Token
    pub fn jwt(&self) -> Option<&JwtClaims> {
        self.jwt.as_ref().deref().as_ref()
    }
}

impl JwtClaims {
    fn new(jwt: String) -> Self {
        Self {
            payload: jwt,
            parsed: OnceCell::new(),
            audience: OnceCell::new(),
        }
    }

    fn get_parsed(&self) -> &serde_json::Value {
        self.parsed
            .get_or_init(|| serde_json::from_str(&self.payload).expect("Failed to parse JWT payload"))
    }

    /// Returns the tokens subject, from the sub claim.
    pub fn subject(&self) -> &str {
        self.get_parsed()
            .get("sub")
            .expect("Missing 'sub' claim")
            .as_str()
            .expect("Token 'sub' claim is not a string")
    }

    /// Returns the issuer for these credentials, from the iss claim.
    pub fn issuer(&self) -> &str {
        self.get_parsed().get("iss").unwrap().as_str().unwrap()
    }

    fn extract_audience(&self) -> Vec<String> {
        let Some(aud) = self.get_parsed().get("aud") else {

View on GitHub (pinned to 524b4487d9)

Solutions

  1. Pass the decoded claims object serialized as JSON, e.g. {"sub":"user-1","iss":"https://issuer"}.
  2. In production code use ctx.jwt()/has_jwt() rather than constructing claims manually.
  3. Validate that test fixtures parse as JSON before wiring them into AuthCtx.

Example fix

// before: raw compact JWT - the payload string is not JSON
let ctx = AuthCtx::from_jwt_payload(String::from("eyJhbGciOiJIUzI1NiJ9..."));

// after: decoded claims as JSON
let claims = serde_json::json!({"sub": "user-1", "iss": "https://issuer.example"}).to_string();
let ctx = AuthCtx::from_jwt_payload(claims);
Defensive patterns

Strategy: validation

Validate before calling

fn is_valid_claims_json(s: &str) -> bool {
    serde_json::from_str::<serde_json::Value>(s).is_ok()
}

// Use in test setup so bad fixtures fail loudly before reaching AuthCtx:
assert!(is_valid_claims_json(&jwt_payload), "fixture must be decoded claims JSON");

Try / catch

let outcome = std::panic::catch_unwind(std::AssertUnwindSafe(|| ctx.jwt().unwrap().subject().to_string()));
if outcome.is_err() {
    // Payload was not JSON: fix the fixture/issuer - the claims string handed to
    // JwtClaims must be the decoded JSON claims document.
}

Prevention

When it happens

Trigger: Unit tests calling AuthCtx::from_jwt_payload with a full header.payload.signature token or a base64-encoded blob instead of decoded claims JSON; a mocked host returning a non-JSON string; version skew in the claims format.

Common situations: Developers pasting an entire JWT into test fixtures; mocks returning the wrong string field; CI tests that never exercise jwt() locally.

Understand the failure class

Related errors


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