From 53832e3ed642c9b1c19b38efe30dfa7a1144055b Mon Sep 17 00:00:00 2001 From: Connor Graham Date: Wed, 20 May 2026 11:13:02 -0400 Subject: [PATCH] fix(bedrock/base_aws_llm): remove lowercase authorization duplicate after SigV4 override MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- litellm/llms/bedrock/base_aws_llm.py | 1 + .../llms/bedrock/test_base_aws_llm.py | 45 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/litellm/llms/bedrock/base_aws_llm.py b/litellm/llms/bedrock/base_aws_llm.py index 9c43c010f57..89ce668e223 100644 --- a/litellm/llms/bedrock/base_aws_llm.py +++ b/litellm/llms/bedrock/base_aws_llm.py @@ -1524,6 +1524,7 @@ class BaseAWSLLM: if header_value is not None: request_headers_dict[header_name] = header_value if "authorization" in headers: # prevent sigv4 from overwriting the auth header + request_headers_dict.pop("authorization", None) request_headers_dict["Authorization"] = headers["authorization"] return request_headers_dict, request.body diff --git a/tests/test_litellm/llms/bedrock/test_base_aws_llm.py b/tests/test_litellm/llms/bedrock/test_base_aws_llm.py index a4969e5dacc..b79b0b1e7c6 100644 --- a/tests/test_litellm/llms/bedrock/test_base_aws_llm.py +++ b/tests/test_litellm/llms/bedrock/test_base_aws_llm.py @@ -539,6 +539,51 @@ def test_sign_request_with_sigv4(): assert result_body == mock_request.body +def test_sign_request_caller_authorization_overrides_sigv4(): + """ + When a caller pre-supplies an Authorization header, _sign_request must not + let SigV4's Authorization win. The returned dict should contain exactly one + Authorization key (uppercase, canonical) with the caller's value — no + duplicate lowercase 'authorization' entry alongside it. + """ + llm = BaseAWSLLM() + + mock_credentials = Credentials("test_key", "test_secret", "test_token") + mock_sigv4 = MagicMock() + mock_request = MagicMock() + mock_request.headers = { + "Authorization": "AWS4-HMAC-SHA256 Credential=sigv4-generated", + "Content-Type": "application/json", + } + mock_request.body = b'{"prompt": "test"}' + + caller_auth = "Bearer caller-supplied-token" + + with ( + patch("botocore.auth.SigV4Auth", return_value=mock_sigv4), + patch("botocore.awsrequest.AWSRequest", return_value=mock_request), + patch.object(llm, "get_credentials", return_value=mock_credentials), + patch.object(llm, "_get_aws_region_name", return_value="us-west-2"), + ): + result_headers, _ = llm._sign_request( + service_name="aws-external-anthropic", + headers={"Authorization": caller_auth, "content-type": "application/json"}, + optional_params={"aws_region_name": "us-west-2"}, + request_data={"prompt": "test"}, + api_base="https://aws-external-anthropic.us-west-2.api.aws/v1/messages", + ) + + # Caller's value must win over SigV4's generated one + assert result_headers["Authorization"] == caller_auth + + # Must not have a separate lowercase 'authorization' duplicate + auth_keys = [k for k in result_headers if k.lower() == "authorization"] + assert len(auth_keys) == 1, ( + f"Expected exactly one Authorization key, got {auth_keys}. " + "Duplicate keys can cause unpredictable behaviour in HTTP clients." + ) + + def test_sign_request_with_api_key_bearer_token(): """ Test that _sign_request uses the api_key parameter as a bearer token when provided