Skip to content

fix(bedrock/claude-platform): normalize content-type header case to fix SigV4 401 on aws-external-anthropic - #28257

Closed
ConnorGraham wants to merge 4 commits into
BerriAI:litellm_internal_stagingfrom
ConnorGraham:fix/claude-platform-sigv4-duplicate-content-type
Closed

fix(bedrock/claude-platform): normalize content-type header case to fix SigV4 401 on aws-external-anthropic#28257
ConnorGraham wants to merge 4 commits into
BerriAI:litellm_internal_stagingfrom
ConnorGraham:fix/claude-platform-sigv4-duplicate-content-type

Conversation

@ConnorGraham

Copy link
Copy Markdown

Relevant issues

Fixes #28256

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory, Adding at least 1 test is a hard requirement - see details
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem

Type

🐛 Bug Fix

Changes

litellm/llms/bedrock/base_aws_llm.py_sign_request() (lines 1502-1505)

Before:

if headers is not None:
    headers = {"Content-Type": "application/json", **headers}
else:
    headers = {"Content-Type": "application/json"}

After:

normalized: dict = {k.lower(): v for k, v in (headers or {}).items()}
normalized.setdefault("content-type", "application/json")
headers = normalized

tests/test_litellm/llms/bedrock/test_claude_platform_provider.py — added test_sigv4_no_duplicate_content_type_in_canonical_string which captures the headers dict passed to AWSRequest and asserts exactly one content-type key is present.

Root cause

get_anthropic_headers() sets "content-type": "application/json" (lowercase). _sign_request() then prepends "Content-Type": "application/json" (uppercase). Python dicts are case-sensitive, so both keys survive. botocore's AWSRequest uses a case-insensitive HeadersDict and joins both values into "application/json, application/json" in the SigV4 canonical string. The actual wire request sends only "application/json", so the signatures never match → 401.

This affects all requests to the bedrock/claude_platform/<model> route (aws-external-anthropic.<region>.api.aws), making the feature unusable since it was introduced in #27678.

Screenshots / Proof of Fix

Before (canonical string from AWS error response):

content-type:application/json, application/json

After: content-type:application/json (single value, signature matches).

All 12 unit tests in test_claude_platform_provider.py pass.

…re SigV4 signing

When the caller passes headers with a lowercase "content-type" key (as
get_anthropic_headers() does), and _sign_request prepends an uppercase
"Content-Type" key, both survive as separate entries in a plain Python
dict.  botocore's AWSRequest uses a case-insensitive HeadersDict, so it
sees two values for the same header and joins them into
"application/json, application/json" in the SigV4 canonical string.

The actual HTTP request only sends one value, so the signature never
matches → 401 authentication_error from aws-external-anthropic.

Fix: normalise all header keys to lowercase before signing with
setdefault() to add content-type only if not already present.

Adds a regression test that captures the AWSRequest headers and asserts
exactly one content-type key reaches the signer.

Fixes: duplicate Content-Type in aws-external-anthropic SigV4 canonical string
@CLAassistant

CLAassistant commented May 19, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ConnorGraham
ConnorGraham changed the base branch from main to litellm_internal_staging May 19, 2026 14:10
@codspeed-hq

codspeed-hq Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Congrats! CodSpeed is installed 🎉

🆕 16 new benchmarks were detected.

You will start to see performance impacts in the reports once the benchmarks are run from your default branch.

Detected benchmarks


Open in CodSpeed

@codecov

codecov Bot commented May 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: This PR is currently BLOCKED from merge.

Score: 2/5

Why blocked:

  • 1 PR-related CI failure (Greptile gate: score not yet reviewed below required 4/5 — request a Greptile review (@greptileai) and resolve its comments before maintainer review.) (pr_related_failures, -2 pts)
  • Greptile commented but no Confidence Score line was found (greptile_null, -1 pts)

Details: Score docked for: 1 PR-related CI failure (Greptile gate: score not yet reviewed below required 4/5 — request a Greptile review (@greptileai) and resolve its comments before maintainer review.); Greptile commented but no Confidence Score line was found.

Fix the issues above and push an update — the bot will re-review automatically.

Note: This bot is still in beta and might not always work as expected. Please share any feedback via Slack.

@ConnorGraham

Copy link
Copy Markdown
Author

@greptileai

@greptile-apps

greptile-apps Bot commented May 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a SigV4 signing bug on the bedrock/claude_platform/<model> route where get_anthropic_headers() sets content-type (lowercase) and _sign_request() previously prepended Content-Type (uppercase); both keys survived in the Python dict and botocore joined their values into "application/json, application/json" in the canonical string, causing a 401 on every request.

  • base_aws_llm.py: Normalizes all incoming header keys to lowercase before signing, then uses setdefault to add content-type only if absent. The Authorization guard is updated to check the lowercase key and explicitly pops the lowercase entry before writing the canonical uppercase form, preventing a second duplicate.
  • Tests: Two new mock-only unit tests cover the content-type deduplication (headers captured at AWSRequest construction) and the caller-Authorization-wins behaviour (output dict has exactly one Authorization key with the caller's value).

Confidence Score: 5/5

Safe to merge — the change is confined to the header-normalization step in _sign_request, the root cause is well-understood, and targeted regression tests cover both the content-type deduplication and the caller-Authorization-override path.

The fix is minimal and surgical: it addresses the exact dict key-case collision that produced the malformed SigV4 canonical string. _filter_headers_for_aws_signature already lowercases keys internally before comparing, so it is unaffected. The Authorization handling is correct — the pop-then-uppercase-set pattern eliminates the intermediate duplicate before the method returns. No existing tests were modified, and both new tests are mock-only and run cleanly in CI.

No files require special attention.

Important Files Changed

Filename Overview
litellm/llms/bedrock/base_aws_llm.py Normalizes incoming header keys to lowercase before SigV4 signing, preventing duplicate content-type entries; also fixes the Authorization guard to use the post-normalization lowercase key and pops the lowercase entry before re-adding it uppercase.
tests/test_litellm/llms/bedrock/test_claude_platform_provider.py Adds regression test that captures headers passed to AWSRequest and asserts exactly one content-type key is present, directly covering the canonical-string duplication bug.
tests/test_litellm/llms/bedrock/test_base_aws_llm.py Adds test verifying that a caller-supplied Authorization header wins over SigV4's generated value, and that no duplicate authorization keys appear in the output.

Reviews (3): Last reviewed commit: "fix(bedrock/base_aws_llm): remove lowerc..." | Re-trigger Greptile

…der normalization

After lowercasing all header keys, the Authorization override guard was
checking for uppercase "Authorization" (always False) and the key
lookup would also fail. Update both to use lowercase "authorization" to
match the post-normalization dict.

Caught by Greptile review on PR BerriAI#28257.
@ConnorGraham

Copy link
Copy Markdown
Author

@greptileai

Comment thread litellm/llms/bedrock/base_aws_llm.py Outdated
@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: This PR is currently BLOCKED from merge.

Score: 4/5

Why blocked:

  • 1 unresolved reviewer concern (greptile) (unresolved_concern, -1 pts)

Details: Score docked for: 1 unresolved reviewer concern (greptile).

Fix the issues above and push an update — the bot will re-review automatically.

Note: This bot is still in beta and might not always work as expected. Please share any feedback via Slack.

…fter SigV4 override

After the header normalization, the merge loop writes lowercase
'authorization' into request_headers_dict and the Authorization-override
guard then writes uppercase 'Authorization' — both keys survive as
separate entries in the plain dict.

Fix by popping the lowercase key before writing the canonical uppercase
one so HTTP clients only see a single Authorization header.

Add test_sign_request_caller_authorization_overrides_sigv4 to cover
the override branch (fixes Codecov missing-line report) and assert no
duplicate key survives in the returned headers dict.
@ConnorGraham

Copy link
Copy Markdown
Author

Addressed the remaining Greptile concern (commit 53832e3):

Problem: the merge loop wrote lowercase "authorization" into request_headers_dict and the override guard then wrote uppercase "Authorization" — both keys survived as separate entries in the plain Python dict.

Fix: pop("authorization", None) before writing the canonical uppercase key, so only one Authorization entry exists in the returned dict.

Coverage: added test_sign_request_caller_authorization_overrides_sigv4 to test_base_aws_llm.py which exercises the override branch and asserts no duplicate key survives. This closes the Codecov missing-line report too.

All 90 unit tests pass locally.

@ConnorGraham

Copy link
Copy Markdown
Author

@greptileai

@oss-pr-review-agent-shin

Copy link
Copy Markdown
Contributor

🤖 litellm-agent: This PR is currently BLOCKED from merge.

Score: 0/5

Why blocked:

  • karpathy needs_human — The normalization {k.lower(): v for k, v in (headers or {}).items()} applies to every caller of _sign_request (vector_stores, passthrough, count_tokens, agentcore, invoke_agent, amazon_openai, base_invoke_transformation), not only the claude_platform route cited in the PR body. The PR body says the bug 'affects all requests to the bedrock/claude_platform/ route', yet the diff silently changes header key casing for all 8+ routes that share this method. HTTP clients treat headers case-insensitively so an outright breakage is unlikely, but the side-effect is undocumented and untested for those routes (karpathy, -2 pts)
  • all Phase B agent checks non-approving (phase_b_none_approved, -5 pts)

Details: Score docked for: karpathy needs_human — The normalization {k.lower(): v for k, v in (headers or {}).items()} applies to every caller of _sign_request (vector_stores, passthrough, count_tokens, agentcore, invoke_agent, amazon_openai, base_invoke_transformation), not only the claude_platform route cited in the PR body. The PR body says the bug 'affects all requests to the bedrock/claude_platform/ route', yet the diff silently changes header key casing for all 8+ routes that share this method. HTTP clients treat headers case-insensitively so an outright breakage is unlikely, but the side-effect is undocumented and untested for those routes; all Phase B agent checks non-approving (karpathy + security + coverage gap).

Fix the issues above and push an update — the bot will re-review automatically.

Note: This bot is still in beta and might not always work as expected. Please share any feedback via Slack.

…d header normalization

The previous normalization ({k.lower() for k in headers}) lowercased every
header key for all 8+ callers of _sign_request, not just the claude_platform
route that triggered the bug.  That was a broader change than necessary.

Replace with a targeted guard: only prepend Content-Type when no caller has
already set it under any casing.  This leaves all existing header keys
untouched for every other route while still preventing the duplicate
content-type entry in the SigV4 canonical string for claude_platform.

The Authorization override guard is restored to its original form since it
relied on the caller using uppercase "Authorization" — unchanged behavior.
@ConnorGraham

Copy link
Copy Markdown
Author

Addressed the karpathy scope concern (commit 93635e8):

Problem with the previous approach: {k.lower(): v for k, v in headers.items()} mutated the key casing for all 8+ callers of _sign_request (vector_stores, passthrough, agentcore, etc.) — broader than the single-route bug described in the PR.

New approach: replace the blanket normalization with a targeted case-insensitive guard:

if not any(k.lower() == "content-type" for k in headers):
    headers = {"Content-Type": "application/json", **headers}

This only skips the prepend when content-type is already present (in any casing) — zero change to key casing for any route, zero risk to other callers. The Authorization override guard is restored to its original form since it was never broken before the normalization was introduced.

91 unit tests pass locally.

@ConnorGraham

Copy link
Copy Markdown
Author

Closing in favor of a simpler fix — see replacement PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Claude Platform on AWS (aws-external-anthropic) always returns 401 — duplicate content-type in SigV4 canonical string

2 participants