Hmbown/CodeWhale · error
stable session header
Error message
stable session header
What it means
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.
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.
Example fix
// before
let observed_session = header("x-opencode-session").expect("stable session header");
// after (in the client, the actual fix)
headers.insert(
"x-opencode-session",
HeaderValue::from_str(&self.session_id).expect("session id is ASCII"),
); Defensive patterns
Strategy: validation
Validate before calling
// Rust (test-side guard before parsing)
let raw = request.headers.get("x-opencode-session");
assert!(raw.is_some(), "x-opencode-session header must be present");
assert!(raw.unwrap().to_str().is_ok(), "session header must be visible ASCII"); Type guard
fn session_header<'a>(req: &'a RequestRecord) -> Option<&'a str> {
req.headers.get("x-opencode-session").and_then(|v| v.to_str().ok()).filter(|s| !s.is_empty())
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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()`.
Related errors
- Absolute path should not warn
- child assignment
- child-local search
- configured Web evidence tool
- expected authentication error, got
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/0216f5609ef623af.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/client.rs:8078
.is_err(),
"a request cannot silently change an existing client's protocol"
);
} else {
client
.create_message(minimal_zen_request(model))
.await
.unwrap();
}
let requests = server.received_requests().await.unwrap();
assert_eq!(requests.len(), 1);
let request = &requests[0];
let header = |name: &str| {
request
.headers
.get(name)
.and_then(|value| value.to_str().ok())
};
let observed_session = header("x-opencode-session").expect("stable session header");
assert!(!observed_session.is_empty());
if let Some(previous) = &session {
assert_eq!(observed_session, previous);
}
session = Some(observed_session.to_string());
assert!(header("user-agent").is_some_and(|value| value.contains("Codewhale") || value.contains("codewhale")));
for forbidden in ["openai-beta", "originator", "chatgpt-account-id"] {
assert!(
header(forbidden).is_none(),
"gateway requests cannot carry {forbidden}"
);
}
let body: Value = serde_json::from_slice(&request.body).unwrap();
assert_eq!(body["model"], model);
if wire == WireFormat::AnthropicMessages {
assert_eq!(header("x-api-key"), Some("go-test-key"));
assert_eq!(header("anthropic-version"), Some("2023-06-01"));
assert!(header("authorization").is_none());View on GitHub (pinned to 73e0f67d83)