From 5c2482c307a9996b0d848cabaa0cda092a8914db Mon Sep 17 00:00:00 2001 From: matevip Date: Sun, 26 Apr 2026 08:34:12 +0800 Subject: [PATCH] fix(anthropic): log outgoing request headers on 429 --- .../RateLimitDiagnosticExchangeFilter.java | 25 +++++++++++++++++++ .../RateLimitDiagnosticInterceptor.java | 18 +++++++++++++ 2 files changed, 43 insertions(+) diff --git a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/RateLimitDiagnosticExchangeFilter.java b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/RateLimitDiagnosticExchangeFilter.java index 1dc7d8b9..1f0c969c 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/RateLimitDiagnosticExchangeFilter.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/RateLimitDiagnosticExchangeFilter.java @@ -41,11 +41,36 @@ class RateLimitDiagnosticExchangeFilter implements ExchangeFilterFunction { public Mono filter(ClientRequest request, ExchangeFunction next) { return next.exchange(request).doOnNext(response -> { if (response.statusCode().value() == 429) { + // Log REQUEST headers too — Spring AI's AnthropicApi.Builder + // calls clone() + defaultHeaders(consumer) on the rest/web + // client builder we hand in, and we want to verify our OAuth + // fingerprint headers actually survived that flow. If they + // didn't, no amount of correct fingerprinting fixes it. + logRequestHeaders(request.headers()); logHeaders(response.headers().asHttpHeaders()); } }); } + private static void logRequestHeaders(HttpHeaders requestHeaders) { + StringBuilder sb = new StringBuilder("[Anthropic 429] outgoing request headers (sanitized): "); + boolean any = false; + for (var entry : requestHeaders.entrySet()) { + String name = entry.getKey().toLowerCase(); + // Skip Authorization — never log Bearer tokens. Just show "Bearer ". + String displayValue; + if (name.equals("authorization")) { + displayValue = "Bearer "; + } else { + displayValue = String.join(",", entry.getValue()); + } + if (any) sb.append(", "); + sb.append(name).append('=').append(displayValue); + any = true; + } + log.warn(sb.toString()); + } + private static void logHeaders(HttpHeaders headers) { StringBuilder sb = new StringBuilder("[Anthropic 429] rate-limit headers: "); boolean any = false; diff --git a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/RateLimitDiagnosticInterceptor.java b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/RateLimitDiagnosticInterceptor.java index 8fd3b54c..a2b31caf 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/RateLimitDiagnosticInterceptor.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/chatmodel/RateLimitDiagnosticInterceptor.java @@ -52,11 +52,29 @@ class RateLimitDiagnosticInterceptor implements ClientHttpRequestInterceptor { ClientHttpRequestExecution execution) throws IOException { ClientHttpResponse response = execution.execute(request, body); if (response.getStatusCode().value() == 429) { + // Log REQUEST headers too so we can verify Spring AI didn't strip + // our OAuth fingerprint when it cloned the rest client builder. + logRequestHeaders(request.getHeaders()); logHeaders(response.getHeaders()); } return response; } + private static void logRequestHeaders(HttpHeaders requestHeaders) { + StringBuilder sb = new StringBuilder("[Anthropic 429] outgoing request headers (sanitized): "); + boolean any = false; + for (var entry : requestHeaders.entrySet()) { + String name = entry.getKey().toLowerCase(); + String displayValue = name.equals("authorization") + ? "Bearer " + : String.join(",", entry.getValue()); + if (any) sb.append(", "); + sb.append(name).append('=').append(displayValue); + any = true; + } + log.warn(sb.toString()); + } + private static void logHeaders(HttpHeaders headers) { StringBuilder sb = new StringBuilder("[Anthropic 429] rate-limit headers: "); boolean any = false;