{"record":{"id":"0216f5609ef623af","repo":"Hmbown/CodeWhale","slug":"stable-session-header","errorCode":null,"errorMessage":"stable session header","messagePattern":"stable session header","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/client.rs","lineNumber":8078,"sourceCode":"                        .is_err(),\n                    \"a request cannot silently change an existing client's protocol\"\n                );\n            } else {\n                client\n                    .create_message(minimal_zen_request(model))\n                    .await\n                    .unwrap();\n            }\n            let requests = server.received_requests().await.unwrap();\n            assert_eq!(requests.len(), 1);\n            let request = &requests[0];\n            let header = |name: &str| {\n                request\n                    .headers\n                    .get(name)\n                    .and_then(|value| value.to_str().ok())\n            };\n            let observed_session = header(\"x-opencode-session\").expect(\"stable session header\");\n            assert!(!observed_session.is_empty());\n            if let Some(previous) = &session {\n                assert_eq!(observed_session, previous);\n            }\n            session = Some(observed_session.to_string());\n            assert!(header(\"user-agent\").is_some_and(|value| value.contains(\"Codewhale\") || value.contains(\"codewhale\")));\n            for forbidden in [\"openai-beta\", \"originator\", \"chatgpt-account-id\"] {\n                assert!(\n                    header(forbidden).is_none(),\n                    \"gateway requests cannot carry {forbidden}\"\n                );\n            }\n            let body: Value = serde_json::from_slice(&request.body).unwrap();\n            assert_eq!(body[\"model\"], model);\n            if wire == WireFormat::AnthropicMessages {\n                assert_eq!(header(\"x-api-key\"), Some(\"go-test-key\"));\n                assert_eq!(header(\"anthropic-version\"), Some(\"2023-06-01\"));\n                assert!(header(\"authorization\").is_none());","sourceCodeStart":8060,"sourceCodeEnd":8096,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/client.rs#L8060-L8096","documentation":"This is a test-side `.expect(\"stable session header\")` panic in the Codewhale TUI client test suite. It fires when the request captured by the wiremock server has no `x-opencode-session` header, or the header value is not valid ASCII/UTF-8 (`to_str()` fails). The test asserts that the opencode gateway client attaches a stable, non-empty session header on every outbound request, so a panic here means header injection regressed or the request was sent by a code path that skips session headers.","triggerScenarios":"Running `cargo test -p codewhale-tui` and reaching `opencode_go_dispatches_all_three_wires_with_gateway_auth_and_stable_session` when `prepare_outbound_request` fails to set `x-opencode-session` (missing session id, header only set on one wire format, or header name typo), or the header value contains non-visible-ASCII characters so `HeaderValue::to_str()` returns None.","commonSituations":"Refactoring the outbound request builder so session propagation is dropped for the Responses/ChatCompletions/AnthropicMessages wires; changing the header casing/name; introducing a binary or non-ASCII session token that cannot round-trip through `to_str()`.","solutions":["Inspect the captured request (`request.headers`) and confirm `x-opencode-session` is set by `prepare_outbound_request` for all three wire formats.","Verify the client carries a session id before the first request and reuses it across turns (stability assertion at the same lines).","Check that the session header value is ASCII-printable so `to_str()` succeeds; reject non-ASCII at session-id creation.","Fix in crates/tui/src/client.rs request-preparation code, not in the test, unless the header contract itself changed."],"exampleFix":"// before\nlet observed_session = header(\"x-opencode-session\").expect(\"stable session header\");\n// after (in the client, the actual fix)\nheaders.insert(\n    \"x-opencode-session\",\n    HeaderValue::from_str(&self.session_id).expect(\"session id is ASCII\"),\n);","handlingStrategy":"validation","validationCode":"// Rust (test-side guard before parsing)\nlet raw = request.headers.get(\"x-opencode-session\");\nassert!(raw.is_some(), \"x-opencode-session header must be present\");\nassert!(raw.unwrap().to_str().is_ok(), \"session header must be visible ASCII\");","typeGuard":"fn session_header<'a>(req: &'a RequestRecord) -> Option<&'a str> {\n    req.headers.get(\"x-opencode-session\").and_then(|v| v.to_str().ok()).filter(|s| !s.is_empty())\n}","tryCatchPattern":null,"preventionTips":["Set the session header in a single shared request-preparation function used by all wire formats.","Validate session ids are ASCII at creation time.","Add an assertion on every new wire-format test that the session header survives.","Never construct HeaderValue from unvalidated external session tokens."],"tags":["test-assertion","http-headers","rust","expect-panic"],"backgroundTag":"unexpected-response-shape","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}