Skip to content

Commit 547425a

Browse files
authored
fix(router): stop dropping client-sent default headers like anthropic-version (#320)
The strip_headers list in send_backend_request included default_headers names (e.g. anthropic-version), which removed the client's value. The already_sent check then saw the original input still contained the header and skipped injecting the route default. Neither value reached upstream. Remove default_headers names from strip_headers so client values pass through. The existing already_sent guard still injects defaults only when the client omits them.
1 parent 42f0763 commit 547425a

2 files changed

Lines changed: 64 additions & 15 deletions

File tree

crates/openshell-router/src/backend.rs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -79,24 +79,14 @@ async fn send_backend_request(
7979
}
8080
}
8181

82-
// Collect header names we need to strip (auth, host, and any default header
83-
// names that will be set from route defaults).
84-
let strip_headers: Vec<String> = {
85-
let mut s = vec![
86-
"authorization".to_string(),
87-
"x-api-key".to_string(),
88-
"host".to_string(),
89-
];
90-
for (name, _) in &route.default_headers {
91-
s.push(name.to_ascii_lowercase());
92-
}
93-
s
94-
};
82+
// Strip auth and host headers — auth is re-injected above from the route
83+
// config, and host must match the upstream.
84+
let strip_headers: [&str; 3] = ["authorization", "x-api-key", "host"];
9585

96-
// Forward non-sensitive headers (skip auth, host, and any we'll override)
86+
// Forward non-sensitive headers.
9787
for (name, value) in &headers {
9888
let name_lc = name.to_ascii_lowercase();
99-
if strip_headers.contains(&name_lc) {
89+
if strip_headers.contains(&name_lc.as_str()) {
10090
continue;
10191
}
10292
builder = builder.header(name.as_str(), value.as_str());

crates/openshell-router/tests/backend_integration.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,65 @@ async fn proxy_anthropic_does_not_send_bearer_auth() {
387387
assert_eq!(response.status, 200);
388388
}
389389

390+
/// Regression test: when the client sends `anthropic-version`, the header must
391+
/// reach the upstream. Previously, the header was added to the strip list
392+
/// (because it appeared in `default_headers`) AND the default injection was
393+
/// skipped (because `already_sent` checked the *original* input), so neither
394+
/// the client's value nor the default reached the backend.
395+
#[tokio::test]
396+
async fn proxy_forwards_client_anthropic_version_header() {
397+
let mock_server = MockServer::start().await;
398+
399+
// The upstream requires anthropic-version — wiremock will reject if missing.
400+
Mock::given(method("POST"))
401+
.and(path("/v1/messages"))
402+
.and(header("x-api-key", "test-anthropic-key"))
403+
.and(header("anthropic-version", "2024-10-22"))
404+
.respond_with(ResponseTemplate::new(200).set_body_string("{}"))
405+
.mount(&mock_server)
406+
.await;
407+
408+
let router = Router::new().unwrap();
409+
let candidates = vec![ResolvedRoute {
410+
name: "inference.local".to_string(),
411+
endpoint: mock_server.uri(),
412+
model: "claude-sonnet-4-20250514".to_string(),
413+
api_key: "test-anthropic-key".to_string(),
414+
protocols: vec!["anthropic_messages".to_string()],
415+
auth: AuthHeader::Custom("x-api-key"),
416+
default_headers: vec![("anthropic-version".to_string(), "2023-06-01".to_string())],
417+
}];
418+
419+
let body = serde_json::to_vec(&serde_json::json!({
420+
"model": "claude-sonnet-4-20250514",
421+
"max_tokens": 1,
422+
"messages": [{"role": "user", "content": "hi"}]
423+
}))
424+
.unwrap();
425+
426+
// Client explicitly sends anthropic-version: 2024-10-22 — this value should
427+
// reach the upstream, NOT be silently dropped.
428+
let response = router
429+
.proxy_with_candidates(
430+
"anthropic_messages",
431+
"POST",
432+
"/v1/messages",
433+
vec![
434+
("content-type".to_string(), "application/json".to_string()),
435+
("anthropic-version".to_string(), "2024-10-22".to_string()),
436+
],
437+
bytes::Bytes::from(body),
438+
&candidates,
439+
)
440+
.await
441+
.unwrap();
442+
443+
assert_eq!(
444+
response.status, 200,
445+
"upstream should have received anthropic-version header"
446+
);
447+
}
448+
390449
#[test]
391450
fn config_resolves_routes_with_protocol() {
392451
let config = RouterConfig {

0 commit comments

Comments
 (0)