Skip to content

fix(proxy): Bedrock Knowledge Base pass-through: preserve SigV4 headers and signed request body#27526

Merged
Sameerlite merged 4 commits into
litellm_internal_stagingfrom
litellm_bedrock_kb_passthrough_sigv4_fix
May 25, 2026
Merged

fix(proxy): Bedrock Knowledge Base pass-through: preserve SigV4 headers and signed request body#27526
Sameerlite merged 4 commits into
litellm_internal_stagingfrom
litellm_bedrock_kb_passthrough_sigv4_fix

Conversation

@milan-berri

@milan-berri milan-berri commented May 9, 2026

Copy link
Copy Markdown
Collaborator

Linear ticket

Resolves LIT-2966

Bug: Bedrock KB pass-through (POST /bedrock/knowledgebases/…/retrieve) dropped botocore SigV4 headers (HeadersDict was treated as non-dict{}), could forward Bearer alongside AWS4 auth, and re-json=’d a body hooks had mutated so it no longer matched the signed payload → 403 (“missing equal-sign” in Authorization) or hangs.

Fix: Coerce Mapping headers to dict; when forward_headers=True, strip request headers that case-insensitively collide with signed/custom headers; stash prepped.body on request.state and send it as content=… in pass_through_request so the wire matches SigV4.

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • 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 (maintainer / author to confirm CI)
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

CI (LiteLLM team)

CI status guideline:

  • 50-55 passing tests: main is stable with minor issues.
  • 45-49 passing tests: acceptable but needs attention
  • <= 40 passing tests: unstable; be careful with your merges and assess the risk.
  • Branch creation CI run
    Link:

  • CI run for the last commit
    Link:

  • Merge / cherry-pick CI run
    Links:

Screenshots / Proof of Fix

Problem

  • Bedrock KB retrieve via proxy (POST /bedrock/knowledgebases/{id}/retrieve) could drop botocore SigV4 headers because prepped.headers is a HeadersDict (Mapping, not dict), so pass-through coerced “custom headers” to {} and only the client’s Authorization: Bearer … reached AWS → malformed auth / hangs depending on transport.
  • With forward_headers=True, request authorization (lowercase) could coexist with signed Authorization, duplicating auth semantics.
  • After hooks inject fields into the parsed JSON dict, re-encoding with json= can diverge from the bytes SigV4 signed (Content-Length / body mismatch).

After (runtime)

  • Proxy returns an AWS response quickly (no client hang). Example against a misconfigured KB: HTTP 400 with AWS body
    {"message":"Invalid input or configuration provided. ..."} — same class of response as a direct signed POST to bedrock-agent-runtime, proving SigV4 and body reach AWS.
  • Debug log shows outbound URL https://bedrock-agent-runtime.<region>.amazonaws.com/knowledgebases/.../retrieve, Authorization: AWS4-HMAC-SHA256…, and Content-Length matching the signed payload.

Important: For the KB used in the capture below (TIXO72HGCJ in eu-north-1), HTTP 400 from the fixed proxy is expected and correct. It means the request reached bedrock-agent-runtime with valid SigV4 and AWS returned a ValidationException-style message about KB input/configuration—not the previous 403 “missing equal-sign” in Authorization from malformed client auth reaching AWS.

Live requests (same curl; RC vs fix)

Use litellm-proxy venv, source .env, and config litellm_config/config_venv_1_83_3_minimal.yaml. Knowledge base path: /bedrock/knowledgebases/TIXO72HGCJ/retrieve.

Shared request

POST /bedrock/knowledgebases/TIXO72HGCJ/retrieve
Authorization: Bearer sk-123
Content-Type: application/json

{"retrievalQuery":{"text":"test"}}

curl (add -i for response headers)

curl -sS -i --max-time 35 -X POST 'http://127.0.0.1:<PORT>/bedrock/knowledgebases/TIXO72HGCJ/retrieve' \
  -H 'Authorization: Bearer sk-123' \
  -H 'Content-Type: application/json' \
  -d '{"retrievalQuery":{"text":"test"}}'

Before — v1.84.0-rc.1 (no fix), proxy e.g. port 4036

HTTP/1.1 403 Forbidden
date: Sat, 09 May 2026 08:53:17 GMT
server: uvicorn
x-litellm-call-id: 6581523b-3a65-45ea-8bf5-02d78792a165
x-litellm-model-api-base: https://bedrock-agent-runtime.eu-north-1.amazonaws.com/knowledgebases/TIXO72HGCJ/retrieve
x-litellm-key-spend: 0.0
content-length: 246
content-type: application/json

{"error":{"message":"{\"message\":\"Invalid key=value pair (missing equal-sign) in Authorization header (hashed with SHA-256 and encoded with Base64): 'Gp8sbH4GYN+oHjYn0EUrSOuivOCwhx+sCUf3IkSFTi0='.\"}","type":"None","param":"None","code":"403"}}

After — branch litellm_bedrock_kb_passthrough_sigv4_fix, proxy e.g. port 4035

HTTP 400 is expected here: AWS accepted the transport/auth and responded with a KB validation/configuration error for this resource.

HTTP/1.1 400 Bad Request
date: Sat, 09 May 2026 08:53:35 GMT
server: uvicorn
x-litellm-call-id: a6c44259-af96-48a8-8554-a46c22f2035b
x-litellm-model-api-base: https://bedrock-agent-runtime.eu-north-1.amazonaws.com/knowledgebases/TIXO72HGCJ/retrieve
x-litellm-key-spend: 0.0
content-length: 201
content-type: application/json

{"error":{"message":"{\"message\":\"Invalid input or configuration provided. Check the input and Knowledge Base configuration and try your request again.\"}","type":"None","param":"None","code":"400"}}

Tests

  • test_forward_headers_custom_wins_case_insensitive_over_request_authorization — custom/signed headers win over forwarded request headers when names differ only by case.
  • test_create_pass_through_route_custom_body_url_target — non-dict custom_headers (e.g. Starlette Headers) is coerced to a plain dict so values are not dropped.

Type

🐛 Bug Fix

Changes

  • create_pass_through_route: Treat custom_headers as Optional[Mapping[str, Any]] and normalize with dict(...) when it is a Mapping, so botocore HeadersDict from SigV4-prepared requests is not replaced by {}.
  • BasePassthroughUtils.forward_headers_from_request: When forward_headers=True, remove incoming request header names that collide case-insensitively with keys already in the custom/signed header dict before merging, so client Bearer does not sit beside Authorization: AWS4-....
  • Bedrock KB pass-through (bedrock_proxy_route): Store prepped.body on request.state under LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY (see litellm/types/passthrough_endpoints/pass_through_endpoints.py).
  • pass_through_request: If that state key is set, send the exact bytes with content=... (streaming and non-streaming paths); otherwise keep the existing JSON path. Clear the raw-body state key in create_pass_through_route’s finally alongside the existing custom-body key.

milan-berri and others added 2 commits May 9, 2026 11:32
Coerce botocore HeadersDict to a dict for pass-through routes. When
forward_headers is true, drop request headers that collide case-insensitively
with signed headers so client Bearer auth does not shadow AWS SigV4.
Send prepped.body as raw content so the outbound payload matches the
signature after logging hooks mutate the parsed dict.

Co-authored-by: Cursor <[email protected]>
Read the SigV4-signed bytes directly from request.state inside
pass_through_request instead of threading a custom_raw_body argument
through three functions. Helper methods are restored to their original
signatures, and the new branch lives in one place at each httpx call site.

Co-authored-by: Cursor <[email protected]>
@codecov

codecov Bot commented May 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ass_through_endpoints/llm_passthrough_endpoints.py 0.00% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@milan-berri

Copy link
Copy Markdown
Collaborator Author

@greptileai check

@greptile-apps

greptile-apps Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes three interconnected bugs in the Bedrock Knowledge Base pass-through route that together caused 403 "missing equal-sign" errors or hangs when proxying SigV4-signed KB retrieve requests.

  • HeadersDict coercion: create_pass_through_route now treats custom_headers as Optional[Mapping] and calls dict(...) on it, so botocore's HeadersDict (a Mapping, not a dict) no longer silently becomes {} and drops all SigV4-signed headers.
  • Auth header deduplication: forward_headers_from_request now case-insensitively removes incoming request headers that collide with custom/signed headers before merging, preventing a client authorization: Bearer … from coexisting with Authorization: AWS4-….
  • Signed-body preservation: bedrock_proxy_route stores prepped.body (the exact bytes SigV4 signed) on request.state; pass_through_request detects this state key and sends content=state_raw_body instead of re-JSON-encoding the dict that hooks mutate, keeping the wire body consistent with the signature.

Confidence Score: 5/5

The change is safe to merge; it narrows the Bedrock KB pass-through to send exactly the bytes that were SigV4-signed and prevents client auth headers from leaking alongside provider-signed headers.

All three root causes (HeadersDict dropped to {}, Bearer/AWS4 auth collision, hook-mutated body diverging from signed bytes) are addressed with targeted, well-contained changes. Callers already copy request headers before passing them in so the new in-place removal is safe. The new unit tests directly verify both the streaming and non-streaming content= paths, and the existing test is extended rather than weakened. No new network calls are added to the test suite.

No files require special attention.

Important Files Changed

Filename Overview
litellm/passthrough/utils.py Adds case-insensitive collision removal of request headers before merging with signed custom headers; callers already pass a .copy() so the mutation is safe.
litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py Sets LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY to prepped.body before invoking the pass-through endpoint func so the exact signed bytes can be forwarded instead of re-encoded JSON.
litellm/proxy/pass_through_endpoints/pass_through_endpoints.py Reads state_raw_body from request state and routes both streaming (build_request(content=…)) and non-streaming (async_client.request(content=…)) paths to forward exact bytes; also fixes Mapping-to-dict coercion for custom headers.
litellm/types/passthrough_endpoints/pass_through_endpoints.py Adds LITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY constant with a clear docstring explaining its purpose for SigV4-signed callers.
tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py Extends the existing custom_body route test to verify Mappingdict coercion; adds two new focused unit tests exercising state_raw_body on the streaming and non-streaming paths.
tests/test_litellm/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py Adds test_forward_headers_custom_wins_case_insensitive_over_request_authorization to verify signed headers win over a forwarded authorization: Bearer regardless of case.

Reviews (3): Last reviewed commit: "Test pass_through_request state_raw_body..." | Re-trigger Greptile

Comment thread litellm/proxy/pass_through_endpoints/pass_through_endpoints.py
milan-berri and others added 2 commits May 9, 2026 12:38
Guard missing request.state (test fixtures) and ignore non-bytes/str
values so MagicMock does not trigger the SigV4 raw-body path.

Co-authored-by: Cursor <[email protected]>
Cover non-streaming (async_client.request) and streaming (build_request)
paths so SigV4 bytes on request.state are not replaced by json= of a
hook-mutated dict.

Co-authored-by: Cursor <[email protected]>
Comment thread litellm/proxy/pass_through_endpoints/pass_through_endpoints.py
@veria-ai

veria-ai Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Medium: Bedrock pass-through bypasses request-body rewrites

This PR preserves the SigV4-signed Bedrock request bytes, but the pass-through path now sends those pre-hook bytes after pre_call_hook has already had a chance to modify the parsed body. Hooks that reject still stop the request, but hooks or guardrails that mask or rewrite fields no longer affect what is forwarded upstream on this signed path.


Status: 1 new · 1 open
Risk: 5/10

_parsed_body=_parsed_body,
forward_multipart=is_multipart,
params=requested_query_params,
content=state_raw_body,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Request-body rewrite bypass

state_raw_body is captured before proxy_logging_obj.pre_call_hook(...) runs, then this branch forwards those original bytes instead of the possibly modified _parsed_body. An authenticated caller can send a Bedrock pass-through request containing content that a configured pre-call hook or passthrough guardrail masks/rewrites, and LiteLLM will still forward the original signed body upstream; recompute the SigV4 body and headers after pre-call mutations, or restrict this raw-body path so it cannot bypass mutating hooks.

@milan-berri

Copy link
Copy Markdown
Collaborator Author

@greptileai update score

Referring to your P2 comment about state_raw_body + non-streaming async_client.request() potentially degrading SSE behavior: this is an existing pass-through limitation, not a regression introduced by this PR; this change only preserves SigV4-signed raw bytes for Bedrock KB (application/json) and does not alter the pre-existing non-streaming response model.

Also referring to your second comment on missing unit coverage for the state_raw_body path: that has been addressed in this PR by adding direct tests for both non-streaming (content=... on async_client.request) and streaming (content=... on build_request) raw-body branches.

@krrish-berri-2

Copy link
Copy Markdown
Contributor

@milan-berri — could you add a screenshot or short video showing that this change works as expected? It really helps reviewers verify the fix quickly. Thanks!

@Sameerlite Sameerlite left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Sameerlite Sameerlite merged commit f45909c into litellm_internal_staging May 25, 2026
115 checks passed
Sameerlite added a commit that referenced this pull request May 26, 2026