EpicGames/lore · error

PresignTokenPayload is always serializable

Error message

PresignTokenPayload is always serializable

What it means

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).

Solutions

  1. Inspect the most recent changes to PresignTokenPayload and revert/remove any field whose Serialize impl can fail
  2. Ensure all payload fields are plain data (strings, numbers, u8 version) with derived Serialize
  3. Run the round_trip_succeeds test to confirm sign/verify still work after changes
  4. If a fallible field is truly needed, switch sign to return Result<String, serde_json::Error> instead of expect

Example fix

// before
pub struct PresignTokenPayload { exp: u64, sub: String, weird: serde_json::Value /* may hold non-string-key maps */ }
// after
#[derive(Serialize, Deserialize)]
pub struct PresignTokenPayload { version: u8, expires_at: u64, subject: String }
Defensive patterns

Strategy: try-catch

Type guard

fn is_plain_payload(p: &PresignTokenPayload) -> bool {
    // derived Serialize on a struct of plain fields guarantees success; check no exotic fields added
    serde_json::to_string(p).is_ok()
}

Try / catch

match serde_json::to_string(payload) {
    Ok(json) => { /* proceed with hmac sign */ },
    Err(e) => log::error!("PresignTokenPayload failed to serialize after refactor: {e}"),
}

Prevention

When it happens

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

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

Understand the failure class

Background: "JSON serialization failed", "not JSON serializable", "Failed to serialize": why JSON marshaling errors happen and how to fix them — this error's family across 46 libraries.

Related errors


AI-assisted analysis of EpicGames/lore@074eb0b0d1 (2026-09-13). Data as JSON: /api/errors/01df2fb787ceb8ba. Report an issue: GitHub.

Appendix: source

Thrown at lore-server/src/http/presign_token.rs:44

#[derive(Debug, Error, PartialEq)]
pub enum PresignTokenError {
    #[error("invalid token format")]
    InvalidFormat,
    #[error("invalid token signature")]
    InvalidSignature,
    #[error("unknown token version: {0}")]
    UnknownVersion(u8),
    #[error("token was signed by a different key")]
    KeyIdMismatch,
    #[error("token has expired")]
    Expired,
}

pub const CURRENT_TOKEN_VERSION: u8 = 1;

/// Signs `payload` and returns `<base64url(json)>.<base64url(signature)>`.
pub fn sign(payload: &PresignTokenPayload, key: &hmac::Key) -> String {
    let json = serde_json::to_string(payload).expect("PresignTokenPayload is always serializable");
    let encoded_payload = URL_SAFE_NO_PAD.encode(json.as_bytes());
    let signature = hmac::sign(key, encoded_payload.as_bytes());
    let encoded_sig = URL_SAFE_NO_PAD.encode(signature.as_ref());
    format!("{encoded_payload}.{encoded_sig}")
}

/// Verifies a token and returns the payload if valid.
///
/// Checks (in order): format, signature, version, `key_id`, expiry.
pub fn verify(
    token: &str,
    key: &hmac::Key,
    key_id: &str,
    now_unix: u64,
) -> Result<PresignTokenPayload, PresignTokenError> {
    let (encoded_payload, encoded_sig) = token
        .split_once('.')
        .ok_or(PresignTokenError::InvalidFormat)?;

View on GitHub (pinned to 074eb0b0d1)