{"record":{"id":"01df2fb787ceb8ba","repo":"EpicGames/lore","slug":"presigntokenpayload-is-always-serializable","errorCode":null,"errorMessage":"PresignTokenPayload is always serializable","messagePattern":"PresignTokenPayload is always serializable","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"lore-server/src/http/presign_token.rs","lineNumber":44,"sourceCode":"#[derive(Debug, Error, PartialEq)]\npub enum PresignTokenError {\n    #[error(\"invalid token format\")]\n    InvalidFormat,\n    #[error(\"invalid token signature\")]\n    InvalidSignature,\n    #[error(\"unknown token version: {0}\")]\n    UnknownVersion(u8),\n    #[error(\"token was signed by a different key\")]\n    KeyIdMismatch,\n    #[error(\"token has expired\")]\n    Expired,\n}\n\npub const CURRENT_TOKEN_VERSION: u8 = 1;\n\n/// Signs `payload` and returns `<base64url(json)>.<base64url(signature)>`.\npub fn sign(payload: &PresignTokenPayload, key: &hmac::Key) -> String {\n    let json = serde_json::to_string(payload).expect(\"PresignTokenPayload is always serializable\");\n    let encoded_payload = URL_SAFE_NO_PAD.encode(json.as_bytes());\n    let signature = hmac::sign(key, encoded_payload.as_bytes());\n    let encoded_sig = URL_SAFE_NO_PAD.encode(signature.as_ref());\n    format!(\"{encoded_payload}.{encoded_sig}\")\n}\n\n/// Verifies a token and returns the payload if valid.\n///\n/// Checks (in order): format, signature, version, `key_id`, expiry.\npub fn verify(\n    token: &str,\n    key: &hmac::Key,\n    key_id: &str,\n    now_unix: u64,\n) -> Result<PresignTokenPayload, PresignTokenError> {\n    let (encoded_payload, encoded_sig) = token\n        .split_once('.')\n        .ok_or(PresignTokenError::InvalidFormat)?;","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/EpicGames/lore/blob/074eb0b0d1194c997d7cf28b55519e3e197b3e23/lore-server/src/http/presign_token.rs#L26-L62","documentation":"sign serializes a PresignTokenPayload with serde_json::to_string and panics via .expect because the payload struct is a plain serializable data type, so serialization is an invariant. The panic indicates the payload somehow failed to serialize — practically impossible unless the type definition changed (e.g. gained a non-serializable field or a custom Serialize impl that errors).","triggerScenarios":"Only reachable if PresignTokenPayload's Serialize impl returns Err — e.g. someone added a field with a serializer that fails, a map with non-string keys, or serde_json was built without required features. Normal use of sign with the current struct cannot panic here.","commonSituations":"Appears after refactors where the payload type gained a non-serializable member, or in tests constructing exotic payloads via generic helpers; essentially a compile-time-guaranteed invariant being violated by a code change.","solutions":["Inspect the most recent changes to PresignTokenPayload and revert/remove any field whose Serialize impl can fail","Ensure all payload fields are plain data (strings, numbers, u8 version) with derived Serialize","Run the round_trip_succeeds test to confirm sign/verify still work after changes","If a fallible field is truly needed, switch sign to return Result<String, serde_json::Error> instead of expect"],"exampleFix":"// before\npub struct PresignTokenPayload { exp: u64, sub: String, weird: serde_json::Value /* may hold non-string-key maps */ }\n// after\n#[derive(Serialize, Deserialize)]\npub struct PresignTokenPayload { version: u8, expires_at: u64, subject: String }","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"fn is_plain_payload(p: &PresignTokenPayload) -> bool {\n    // derived Serialize on a struct of plain fields guarantees success; check no exotic fields added\n    serde_json::to_string(p).is_ok()\n}","tryCatchPattern":"match serde_json::to_string(payload) {\n    Ok(json) => { /* proceed with hmac sign */ },\n    Err(e) => log::error!(\"PresignTokenPayload failed to serialize after refactor: {e}\"),\n}","preventionTips":["Keep PresignTokenPayload limited to derived-Serialize plain fields","Add a serialization round-trip unit test (round_trip_succeeds) to CI","Avoid adding serde_json::Value or map fields with non-string keys","If fallibility ever becomes real, convert sign to return Result instead of expect"],"tags":["rust","serde","json","panic","hmac"],"backgroundTag":"json-serialization-failed","analyzedSha":"074eb0b0d1194c997d7cf28b55519e3e197b3e23","analyzedAt":"2026-09-13T09:00:57.509Z","contentChangedAt":"2026-09-13T09:00:57.509Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}