package executor import ( "bufio" "bytes" "net/http" "net/http/httptest" "os" "sort" "strings" "testing" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth") // claudeCode2_1_220WireHeaderOrder is the header name sequence captured from a// real Claude Code 2.1.220 OAuth POST /v1/messages over HTTP/1.1, minus the four// names the Node HTTP layer appends after the sorted block (Connection, Host,// Accept-Encoding, Content-Length) and minus User-Agent. Go hardcodes Host,// User-Agent and Content-Length ahead of the sorted block, so those four// positions cannot be matched without replacing the request serialiser; the real// client carries User-Agent inside the sorted block at index 3.var claudeCode2_1_220WireHeaderOrder = []string{ "Accept", "Authorization", "Content-Type", "X-Claude-Code-Session-Id", "X-Stainless-Arch", "X-Stainless-Lang", "X-Stainless-OS", "X-Stainless-Package-Version", "X-Stainless-Retry-Count", "X-Stainless-Runtime", "X-Stainless-Runtime-Version", "X-Stainless-Timeout", "anthropic-beta", "anthropic-dangerous-direct-browser-access", "anthropic-version", "x-app", "x-client-request-id",} func newClaudeWireProbeRequest(t *testing.T, rawURL string) *http.Request { t.Helper() auth := &cliproxyauth.Auth{ID: "wire", Metadata: map[string]any{"access_token": "sk-ant-oat01-wire"}} req := httptest.NewRequest(http.MethodPost, rawURL, strings.NewReader("{}")) req.Header = http.Header{} body := []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":"hi"}]}`) if err := applyClaudeHeaders(req, auth, "sk-ant-oat01-wire", false, nil, body, nil, nil, false); err != nil { t.Fatalf("applyClaudeHeaders: %v", err) } // Mirror the production sequence: the casing pass runs at the send boundary, // not inside applyClaudeHeaders, so Header.Get keeps working everywhere else. applyClaudeWireHeaderCasing(req) return req} // The casing pass must stay at the send boundary. Running it inside// applyClaudeHeaders would make these headers invisible to Header.Get for the// rest of the pipeline, which is how the first attempt broke ten other tests.func TestApplyClaudeHeaders_LeavesHeadersCanonicalForThePipeline(t *testing.T) { auth := &cliproxyauth.Auth{ID: "wire", Metadata: map[string]any{"access_token": "sk-ant-oat01-wire"}} req := httptest.NewRequest(http.MethodPost, "https://api.anthropic.com/v1/messages?beta=true", strings.NewReader("{}")) req.Header = http.Header{} body := []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":"hi"}]}`) if err := applyClaudeHeaders(req, auth, "sk-ant-oat01-wire", false, nil, body, nil, nil, false); err != nil { t.Fatalf("applyClaudeHeaders: %v", err) } for canonical := range claudeWireHeaderCasing { if req.Header.Get(canonical) == "" { t.Fatalf("%s is unreadable through Header.Get right after applyClaudeHeaders", canonical) } }} // serializedHeaderNames reads the names off the actual serialized request, which// is the only representation the server ever sees.func serializedHeaderNames(t *testing.T, req *http.Request) []string { t.Helper() var buf bytes.Buffer if err := req.Write(&buf); err != nil { t.Fatalf("write request: %v", err) } var names []string scanner := bufio.NewScanner(&buf) scanner.Scan() // request line for scanner.Scan() { line := scanner.Text() if line == "" { break } name, _, found := strings.Cut(line, ":") if !found { t.Fatalf("malformed header line %q", line) } names = append(names, name) } return names} // The wire casing is a fingerprint in its own right: CPA negotiates ALPN// http/1.1, so names are not lowercased by HPACK and reach Anthropic verbatim.func TestApplyClaudeHeaders_WireCasingMatchesRealClient(t *testing.T) { req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true") got := serializedHeaderNames(t, req) transportOwned := map[string]bool{ "Host": true, "Content-Length": true, "Connection": true, "Accept-Encoding": true, // Go writes User-Agent before the sorted block; the real client keeps it // inside it. Tracked separately below. "User-Agent": true, } var sdkNames []string for _, name := range got { if !transportOwned[name] { sdkNames = append(sdkNames, name) } } want := claudeCode2_1_220WireHeaderOrder if len(sdkNames) != len(want) { t.Fatalf("header count = %d, want %d\n got %v", len(sdkNames), len(want), sdkNames) } for i := range want { if sdkNames[i] != want[i] { t.Fatalf("wire header %d = %q, want %q\n got %v\n want %v", i, sdkNames[i], want[i], sdkNames, want) } }} // Documents the one ordering gap the casing fix cannot close. If Go ever stops// hoisting User-Agent, or the serialiser is replaced, this test fails and the// name can move back into claudeCode2_1_220WireHeaderOrder.func TestApplyClaudeHeaders_UserAgentStillHoistedByGo(t *testing.T) { req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true") names := serializedHeaderNames(t, req) uaIndex, acceptIndex := -1, -1 for i, name := range names { switch name { case "User-Agent": uaIndex = i case "Accept": acceptIndex = i } } if uaIndex == -1 || acceptIndex == -1 { t.Fatalf("missing User-Agent or Accept: %v", names) } if uaIndex > acceptIndex { t.Fatal("User-Agent now sorts with the block: fold it back into the expected wire order") } if got := req.Header.Get("User-Agent"); !strings.HasPrefix(got, "claude-cli/") { t.Fatalf("User-Agent = %q, want the Claude Code identity", got) }} // Guards the property that makes the casing fix sufficient: the real client's// order is a plain bytewise sort, which is also what Go emits.func TestClaudeWireHeaderOrderIsBytewiseSorted(t *testing.T) { sorted := append([]string(nil), claudeCode2_1_220WireHeaderOrder...) sort.Strings(sorted) for i := range sorted { if sorted[i] != claudeCode2_1_220WireHeaderOrder[i] { t.Fatalf("captured order is not a bytewise sort at %d: %q vs %q", i, claudeCode2_1_220WireHeaderOrder[i], sorted[i]) } }} // Every fingerprint rule is keyed on the upstream host, never on the caller.func TestApplyClaudeHeaders_WireCasingIsAnthropicOnly(t *testing.T) { req := newClaudeWireProbeRequest(t, "https://api.moonshot.cn/v1/messages") for _, name := range serializedHeaderNames(t, req) { if name == "anthropic-beta" || name == "x-app" || name == "X-Stainless-OS" { t.Fatalf("Anthropic wire casing leaked to a third-party gateway: %q", name) } } if req.Header.Get("Anthropic-Version") == "" { t.Fatal("third-party gateway lost its canonical headers") }} // The rewritten keys are unreachable through Header.Get, so the pass has to run// after every other mutation. This pins that the values survived the rewrite.func TestApplyClaudeHeaders_WireCasingPreservesValues(t *testing.T) { req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true") for canonical, wire := range claudeWireHeaderCasing { if _, stillCanonical := req.Header[canonical]; stillCanonical { t.Fatalf("%s was not rewritten to %s", canonical, wire) } if len(req.Header[wire]) == 0 || req.Header[wire][0] == "" { t.Fatalf("%s lost its value during the rewrite", wire) } }} // The three Claude request paths must all leave through doClaudeUpstreamRequest.// A direct client.Do would skip the wire-casing pass silently, and no behavioural// test can catch that for a path it does not exercise, so the invariant is// checked structurally.func TestClaudeExecutorHasSingleUpstreamSendBoundary(t *testing.T) { paths := []string{ "claude_executor_execute.go", "claude_executor_stream.go", "claude_executor_tokens.go", } for _, name := range paths { src, err := os.ReadFile(name) if err != nil { t.Fatalf("read %s: %v", name, err) } text := string(src) if strings.Contains(text, "httpClient.Do(") { t.Errorf("%s bypasses the send boundary with a direct httpClient.Do", name) } if !strings.Contains(text, "doClaudeUpstreamRequest(") { t.Errorf("%s does not route through doClaudeUpstreamRequest", name) } }}
package executor
import (
"bufio"
"bytes"
"net/http"
"net/http/httptest"
"os"
"sort"
"strings"
"testing"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
)
// claudeCode2_1_220WireHeaderOrder is the header name sequence captured from a
// real Claude Code 2.1.220 OAuth POST /v1/messages over HTTP/1.1, minus the four
// names the Node HTTP layer appends after the sorted block (Connection, Host,
// Accept-Encoding, Content-Length) and minus User-Agent. Go hardcodes Host,
// User-Agent and Content-Length ahead of the sorted block, so those four
// positions cannot be matched without replacing the request serialiser; the real
// client carries User-Agent inside the sorted block at index 3.
var claudeCode2_1_220WireHeaderOrder = []string{
"Accept",
"Authorization",
"Content-Type",
"X-Claude-Code-Session-Id",
"X-Stainless-Arch",
"X-Stainless-Lang",
"X-Stainless-OS",
"X-Stainless-Package-Version",
"X-Stainless-Retry-Count",
"X-Stainless-Runtime",
"X-Stainless-Runtime-Version",
"X-Stainless-Timeout",
"anthropic-beta",
"anthropic-dangerous-direct-browser-access",
"anthropic-version",
"x-app",
"x-client-request-id",
}
func newClaudeWireProbeRequest(t *testing.T, rawURL string) *http.Request {
t.Helper()
auth := &cliproxyauth.Auth{ID: "wire", Metadata: map[string]any{"access_token": "sk-ant-oat01-wire"}}
req := httptest.NewRequest(http.MethodPost, rawURL, strings.NewReader("{}"))
req.Header = http.Header{}
body := []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":"hi"}]}`)
if err := applyClaudeHeaders(req, auth, "sk-ant-oat01-wire", false, nil, body, nil, nil, false); err != nil {
t.Fatalf("applyClaudeHeaders: %v", err)
}
// Mirror the production sequence: the casing pass runs at the send boundary,
// not inside applyClaudeHeaders, so Header.Get keeps working everywhere else.
applyClaudeWireHeaderCasing(req)
return req
}
// The casing pass must stay at the send boundary. Running it inside
// applyClaudeHeaders would make these headers invisible to Header.Get for the
// rest of the pipeline, which is how the first attempt broke ten other tests.
func TestApplyClaudeHeaders_LeavesHeadersCanonicalForThePipeline(t *testing.T) {
auth := &cliproxyauth.Auth{ID: "wire", Metadata: map[string]any{"access_token": "sk-ant-oat01-wire"}}
req := httptest.NewRequest(http.MethodPost, "https://api.anthropic.com/v1/messages?beta=true", strings.NewReader("{}"))
req.Header = http.Header{}
body := []byte(`{"model":"claude-opus-5","messages":[{"role":"user","content":"hi"}]}`)
if err := applyClaudeHeaders(req, auth, "sk-ant-oat01-wire", false, nil, body, nil, nil, false); err != nil {
t.Fatalf("applyClaudeHeaders: %v", err)
}
for canonical := range claudeWireHeaderCasing {
if req.Header.Get(canonical) == "" {
t.Fatalf("%s is unreadable through Header.Get right after applyClaudeHeaders", canonical)
}
}
}
// serializedHeaderNames reads the names off the actual serialized request, which
// is the only representation the server ever sees.
func serializedHeaderNames(t *testing.T, req *http.Request) []string {
t.Helper()
var buf bytes.Buffer
if err := req.Write(&buf); err != nil {
t.Fatalf("write request: %v", err)
}
var names []string
scanner := bufio.NewScanner(&buf)
scanner.Scan() // request line
for scanner.Scan() {
line := scanner.Text()
if line == "" {
break
}
name, _, found := strings.Cut(line, ":")
if !found {
t.Fatalf("malformed header line %q", line)
}
names = append(names, name)
}
return names
}
// The wire casing is a fingerprint in its own right: CPA negotiates ALPN
// http/1.1, so names are not lowercased by HPACK and reach Anthropic verbatim.
func TestApplyClaudeHeaders_WireCasingMatchesRealClient(t *testing.T) {
req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true")
got := serializedHeaderNames(t, req)
transportOwned := map[string]bool{
"Host": true, "Content-Length": true, "Connection": true, "Accept-Encoding": true,
// Go writes User-Agent before the sorted block; the real client keeps it
// inside it. Tracked separately below.
"User-Agent": true,
}
var sdkNames []string
for _, name := range got {
if !transportOwned[name] {
sdkNames = append(sdkNames, name)
}
}
want := claudeCode2_1_220WireHeaderOrder
if len(sdkNames) != len(want) {
t.Fatalf("header count = %d, want %d\n got %v", len(sdkNames), len(want), sdkNames)
}
for i := range want {
if sdkNames[i] != want[i] {
t.Fatalf("wire header %d = %q, want %q\n got %v\n want %v", i, sdkNames[i], want[i], sdkNames, want)
}
}
}
// Documents the one ordering gap the casing fix cannot close. If Go ever stops
// hoisting User-Agent, or the serialiser is replaced, this test fails and the
// name can move back into claudeCode2_1_220WireHeaderOrder.
func TestApplyClaudeHeaders_UserAgentStillHoistedByGo(t *testing.T) {
req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true")
names := serializedHeaderNames(t, req)
uaIndex, acceptIndex := -1, -1
for i, name := range names {
switch name {
case "User-Agent":
uaIndex = i
case "Accept":
acceptIndex = i
}
}
if uaIndex == -1 || acceptIndex == -1 {
t.Fatalf("missing User-Agent or Accept: %v", names)
}
if uaIndex > acceptIndex {
t.Fatal("User-Agent now sorts with the block: fold it back into the expected wire order")
}
if got := req.Header.Get("User-Agent"); !strings.HasPrefix(got, "claude-cli/") {
t.Fatalf("User-Agent = %q, want the Claude Code identity", got)
}
}
// Guards the property that makes the casing fix sufficient: the real client's
// order is a plain bytewise sort, which is also what Go emits.
func TestClaudeWireHeaderOrderIsBytewiseSorted(t *testing.T) {
sorted := append([]string(nil), claudeCode2_1_220WireHeaderOrder...)
sort.Strings(sorted)
for i := range sorted {
if sorted[i] != claudeCode2_1_220WireHeaderOrder[i] {
t.Fatalf("captured order is not a bytewise sort at %d: %q vs %q", i, claudeCode2_1_220WireHeaderOrder[i], sorted[i])
}
}
}
// Every fingerprint rule is keyed on the upstream host, never on the caller.
func TestApplyClaudeHeaders_WireCasingIsAnthropicOnly(t *testing.T) {
req := newClaudeWireProbeRequest(t, "https://api.moonshot.cn/v1/messages")
for _, name := range serializedHeaderNames(t, req) {
if name == "anthropic-beta" || name == "x-app" || name == "X-Stainless-OS" {
t.Fatalf("Anthropic wire casing leaked to a third-party gateway: %q", name)
}
}
if req.Header.Get("Anthropic-Version") == "" {
t.Fatal("third-party gateway lost its canonical headers")
}
}
// The rewritten keys are unreachable through Header.Get, so the pass has to run
// after every other mutation. This pins that the values survived the rewrite.
func TestApplyClaudeHeaders_WireCasingPreservesValues(t *testing.T) {
req := newClaudeWireProbeRequest(t, "https://api.anthropic.com/v1/messages?beta=true")
for canonical, wire := range claudeWireHeaderCasing {
if _, stillCanonical := req.Header[canonical]; stillCanonical {
t.Fatalf("%s was not rewritten to %s", canonical, wire)
}
if len(req.Header[wire]) == 0 || req.Header[wire][0] == "" {
t.Fatalf("%s lost its value during the rewrite", wire)
}
}
}
// The three Claude request paths must all leave through doClaudeUpstreamRequest.
// A direct client.Do would skip the wire-casing pass silently, and no behavioural
// test can catch that for a path it does not exercise, so the invariant is
// checked structurally.
func TestClaudeExecutorHasSingleUpstreamSendBoundary(t *testing.T) {
paths := []string{
"claude_executor_execute.go",
"claude_executor_stream.go",
"claude_executor_tokens.go",
}
for _, name := range paths {
src, err := os.ReadFile(name)
if err != nil {
t.Fatalf("read %s: %v", name, err)
}
text := string(src)
if strings.Contains(text, "httpClient.Do(") {
t.Errorf("%s bypasses the send boundary with a direct httpClient.Do", name)
}
if !strings.Contains(text, "doClaudeUpstreamRequest(") {
t.Errorf("%s does not route through doClaudeUpstreamRequest", name)
}
}
}