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
- Pass the decoded claims object serialized as JSON, e.g. {"sub":"user-1","iss":"https://issuer"}.
- In production code use ctx.jwt()/has_jwt() rather than constructing claims manually.
- 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
- In tests, pass decoded claims JSON via serde_json::json!(...).to_string(), never a raw JWT.
- In production, rely on ctx.jwt() from the host rather than constructing claims.
- Check has_jwt() before touching claims.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Missing 'sub' claim
- Token 'sub' claim is not a string
- Issuer too long: {:?}
- Subject too long: {:?}
- Issuer empty
AI-assisted analysis of clockworklabs/SpacetimeDB@524b4487d9 (2026-08-16).
Data as JSON: /api/errors/dd31490af168fbca.
Report an issue: GitHub.