fix(proxy): Bedrock Knowledge Base pass-through: preserve SigV4 headers and signed request body#27526
Conversation
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 Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
|
@greptileai check |
Greptile SummaryThis 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.
Confidence Score: 5/5The 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 No files require special attention.
|
| 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 Mapping → dict 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
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]>
Medium: Bedrock pass-through bypasses request-body rewritesThis PR preserves the SigV4-signed Bedrock request bytes, but the pass-through path now sends those pre-hook bytes after Status: 1 new · 1 open |
| _parsed_body=_parsed_body, | ||
| forward_multipart=is_multipart, | ||
| params=requested_query_params, | ||
| content=state_raw_body, |
There was a problem hiding this comment.
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.
|
@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. |
|
@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! |
Linear ticket
Resolves LIT-2966
Bug: Bedrock KB pass-through (
POST /bedrock/knowledgebases/…/retrieve) dropped botocore SigV4 headers (HeadersDictwas treated as non-dict→{}), could forwardBeareralongsideAWS4auth, and re-json=’d a body hooks had mutated so it no longer matched the signed payload → 403 (“missing equal-sign” inAuthorization) or hangs.Fix: Coerce
Mappingheaders todict; whenforward_headers=True, strip request headers that case-insensitively collide with signed/custom headers; stashprepped.bodyonrequest.stateand send it ascontent=…inpass_through_requestso the wire matches SigV4.Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit(maintainer / author to confirm CI)@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewDelays 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)
Branch creation CI run
Link:
CI run for the last commit
Link:
Merge / cherry-pick CI run
Links:
Screenshots / Proof of Fix
Problem
POST /bedrock/knowledgebases/{id}/retrieve) could drop botocore SigV4 headers becauseprepped.headersis aHeadersDict(Mapping, notdict), so pass-through coerced “custom headers” to{}and only the client’sAuthorization: Bearer …reached AWS → malformed auth / hangs depending on transport.forward_headers=True, requestauthorization(lowercase) could coexist with signedAuthorization, duplicating auth semantics.json=can diverge from the bytes SigV4 signed (Content-Length/ body mismatch).After (runtime)
HTTP 400with AWS body{"message":"Invalid input or configuration provided. ..."}— same class of response as a direct signedPOSTtobedrock-agent-runtime, proving SigV4 and body reach AWS.https://bedrock-agent-runtime.<region>.amazonaws.com/knowledgebases/.../retrieve,Authorization: AWS4-HMAC-SHA256…, andContent-Lengthmatching the signed payload.Important: For the KB used in the capture below (
TIXO72HGCJineu-north-1),HTTP 400from the fixed proxy is expected and correct. It means the request reachedbedrock-agent-runtimewith valid SigV4 and AWS returned a ValidationException-style message about KB input/configuration—not the previous403“missing equal-sign” inAuthorizationfrom malformed client auth reaching AWS.Live requests (same curl; RC vs fix)
Use
litellm-proxyvenv,source .env, and configlitellm_config/config_venv_1_83_3_minimal.yaml. Knowledge base path:/bedrock/knowledgebases/TIXO72HGCJ/retrieve.Shared request
curl(add-ifor response headers)Before —
v1.84.0-rc.1(no fix), proxy e.g. port 4036After — branch
litellm_bedrock_kb_passthrough_sigv4_fix, proxy e.g. port 4035HTTP 400is expected here: AWS accepted the transport/auth and responded with a KB validation/configuration error for this resource.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-dictcustom_headers(e.g. StarletteHeaders) is coerced to a plain dict so values are not dropped.Type
🐛 Bug Fix
Changes
create_pass_through_route: Treatcustom_headersasOptional[Mapping[str, Any]]and normalize withdict(...)when it is aMapping, so botocoreHeadersDictfrom SigV4-prepared requests is not replaced by{}.BasePassthroughUtils.forward_headers_from_request: Whenforward_headers=True, remove incoming request header names that collide case-insensitively with keys already in the custom/signed header dict before merging, so clientBearerdoes not sit besideAuthorization: AWS4-....bedrock_proxy_route): Storeprepped.bodyonrequest.stateunderLITELLM_PASS_THROUGH_RAW_BODY_STATE_KEY(seelitellm/types/passthrough_endpoints/pass_through_endpoints.py).pass_through_request: If that state key is set, send the exact bytes withcontent=...(streaming and non-streaming paths); otherwise keep the existing JSON path. Clear the raw-body state key increate_pass_through_route’sfinallyalongside the existing custom-body key.