{"record":{"id":"9400138c718f62dd","repo":"zeroclaw-labs/zeroclaw","slug":"matrix-whoami-response-did-not-include-device-id","errorCode":null,"errorMessage":"matrix: whoami response did not include device_id; configure channels.matrix.device-id for access-token login","messagePattern":"matrix: whoami response did not include device_id; configure channels\\.matrix\\.device-id for access-token login","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/matrix.rs","lineNumber":1648,"sourceCode":"        if let Some(ref configured) = configured_user_id\n            && configured != &whoami.user_id\n        {\n            bail!(\n                \"matrix: configured channels.matrix.user-id ({configured}) does not match Matrix whoami user_id ({})\",\n                whoami.user_id\n            );\n        }\n\n        if let (Some(configured), Some(actual)) = (&configured_device_id, &whoami.device_id)\n            && configured != actual\n        {\n            bail!(\n                \"matrix: configured channels.matrix.device-id ({configured}) does not match Matrix whoami device_id ({actual})\"\n            );\n        }\n\n        if configured_device_id.is_none() && whoami.device_id.is_none() {\n            bail!(\n                \"matrix: whoami response did not include device_id; configure channels.matrix.device-id for access-token login\"\n            );\n        }\n\n        Ok(AccessTokenIdentity {\n            user_id: configured_user_id.unwrap_or(whoami.user_id),\n            device_id: configured_device_id.or(whoami.device_id),\n        })\n    }\n\n    async fn fetch_access_token_whoami(config: &MatrixConfig) -> Result<WhoamiResponse> {\n        let access_token = config\n            .access_token\n            .as_deref()\n            .context(\"matrix: whoami requires access_token\")?;\n        let url = matrix_client_api_url(&config.homeserver, WHOAMI_ENDPOINT)?;\n        let response = reqwest::Client::builder()\n            .timeout(WHOAMI_TIMEOUT)","sourceCodeStart":1630,"sourceCodeEnd":1666,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/matrix.rs#L1630-L1666","documentation":"Access-token login needs to know which device the token represents (verification keys and E2EE depend on it). The channel reads device_id from the whoami response, but some homeservers omit it. If whoami provides no device_id and the operator did not configure channels.matrix.device-id, login cannot determine the device and aborts, asking for an explicit device-id.","triggerScenarios":"Access-token login against a homeserver whose whoami response omits device_id, with channels.matrix.device-id unset in config.","commonSituations":"Homeserver versions or forks that leave device_id out of whoami; tokens minted through unusual flows (appservice, legacy admin) with no bound device; older Synapse-compatible servers.","solutions":["Set channels.matrix.device-id to the device the token was created with - visible in the client's active sessions/devices list.","If unknown, do a one-time user-id+password login so a fresh device and session are persisted, then keep that setup or copy the new device id into config.","Prefer minting tokens via a standard client login so they carry a device id."],"exampleFix":"# before\n[channels.matrix]\naccess-token = \"syt_...\"\n\n# after\n[channels.matrix]\naccess-token = \"syt_...\"\ndevice-id = \"ABCD123EFG\"","handlingStrategy":"validation","validationCode":"async fn whoami_device_known(cfg: &MatrixConfig) -> anyhow::Result<bool> {\n    let url = format!(\n        \"{}/_matrix/client/v3/account/whoami\",\n        cfg.homeserver.trim_end_matches('/')\n    );\n    let who: serde_json::Value = reqwest::Client::new()\n        .get(url)\n        .bearer_auth(cfg.access_token.as_deref().unwrap_or_default())\n        .send().await?\n        .error_for_status()?\n        .json().await?;\n    let server_knows = who[\"device_id\"].as_str().is_some_and(|d| !d.trim().is_empty());\n    let configured = cfg.device_id.as_deref().is_some_and(|d| !d.trim().is_empty());\n    Ok(server_knows || configured)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pin channels.matrix.device-id when using token auth - it removes the dependency on whoami's optional field.","Smoke-test token auth against your specific homeserver before rollout; response shapes vary.","Capture the (token, device-id) pair from the login session that minted it."],"tags":["matrix","auth","whoami","device-id","access-token","homeserver"],"backgroundTag":"incomplete-whoami-response","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}