litellm - ✅(Solved) Fix bug: Claude Platform on AWS (aws-external-anthropic) always returns 401 — duplicate content-type in SigV4 canonical string [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
BerriAI/litellm#28256Fetched 2026-05-20 03:40:27
View on GitHub
Comments
0
Participants
1
Timeline
2
Reactions
0
Participants
Timeline (top)
cross-referenced ×1labeled ×1

Error Message

"type": "error", "error": {

Root Cause

_sign_request() in base_aws_llm.py prepends an uppercase Content-Type key to the headers dict:

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

The caller (get_anthropic_headers()) already sets a lowercase content-type key. Because Python dicts are case-sensitive, both keys survive:

{
    "Content-Type": "application/json",   # added by _sign_request
    "content-type": "application/json",   # added by get_anthropic_headers
    ...
}

botocore's AWSRequest uses a case-insensitive HeadersDict, so it sees two values for the same header and joins them when building the canonical string:

content-type:application/json, application/json   ← doubled

The actual HTTP request only sends one content-type: application/json, so the signatures never match.

Fix Action

Fix

Normalise header keys to lowercase before signing so content-type can only appear once:

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

PR incoming.

PR fix notes

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

Description (problem / solution / changelog)

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.

Changed files

  • litellm/llms/bedrock/base_aws_llm.py (modified, +10/-8)
  • tests/test_litellm/llms/bedrock/test_claude_platform_provider.py (modified, +72/-1)

Code Example

The request signature we calculated does not match the signature you provided.

---

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

---

{
    "Content-Type": "application/json",   # added by _sign_request
    "content-type": "application/json",   # added by get_anthropic_headers
    ...
}

---

content-type:application/json, application/json   ← doubled

---

model_list:
  - model_name: claude-sonnet-4-6 (claude-on-aws)
    litellm_params:
      model: bedrock/claude_platform/claude-sonnet-4-6
      aws_region_name: us-east-1
      workspace_id: <your-workspace-id>

---

{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "The request signature we calculated does not match the signature you provided. ...\n\ncontent-type:application/json, application/json\n..."
  }
}

---

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

Describe the bug

When using the bedrock/claude_platform/<model> route (introduced in v1.86.0-rc.1 / PR #27678), every request to aws-external-anthropic.<region>.api.aws fails with a 401 authentication_error:

The request signature we calculated does not match the signature you provided.

Root cause

_sign_request() in base_aws_llm.py prepends an uppercase Content-Type key to the headers dict:

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

The caller (get_anthropic_headers()) already sets a lowercase content-type key. Because Python dicts are case-sensitive, both keys survive:

{
    "Content-Type": "application/json",   # added by _sign_request
    "content-type": "application/json",   # added by get_anthropic_headers
    ...
}

botocore's AWSRequest uses a case-insensitive HeadersDict, so it sees two values for the same header and joins them when building the canonical string:

content-type:application/json, application/json   ← doubled

The actual HTTP request only sends one content-type: application/json, so the signatures never match.

Steps to reproduce

Configure LiteLLM with:

model_list:
  - model_name: claude-sonnet-4-6 (claude-on-aws)
    litellm_params:
      model: bedrock/claude_platform/claude-sonnet-4-6
      aws_region_name: us-east-1
      workspace_id: <your-workspace-id>

Any request returns HTTP 401.

Expected behavior

Requests sign correctly and return 200.

LiteLLM version

v1.86.0-rc.1 (first version containing claude_platform route, PR #27678)

Relevant log output

{
  "type": "error",
  "error": {
    "type": "authentication_error",
    "message": "The request signature we calculated does not match the signature you provided. ...\n\ncontent-type:application/json, application/json\n..."
  }
}

Note the doubled application/json, application/json in the canonical string.

Fix

Normalise header keys to lowercase before signing so content-type can only appear once:

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

PR incoming.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

Requests sign correctly and return 200.

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - ✅(Solved) Fix bug: Claude Platform on AWS (aws-external-anthropic) always returns 401 — duplicate content-type in SigV4 canonical string [1 pull requests, 1 participants]