Skip to content

test(passthrough): regression coverage for Bedrock Content-Length override - #29549

Open
mateo-berri wants to merge 4 commits into
litellm_internal_stagingfrom
litellm_bedrock_passthrough_content_length_regression
Open

test(passthrough): regression coverage for Bedrock Content-Length override#29549
mateo-berri wants to merge 4 commits into
litellm_internal_stagingfrom
litellm_bedrock_passthrough_content_length_regression

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Follow-up to the Bedrock passthrough RuntimeError: Response content longer than Content-Length regression fixed in #29120 (introduced by #27412). The fix shipped without a test that exercises the response serialization layer, so this adds that coverage. Reported via an internal bug report; affected v1.85.0 through v1.88.0-dev.1, fixed in v1.88.0-rc.1

Linear ticket

N/A

Pre-Submission checklist

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

  • I have added meaningful tests
  • 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
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Background

The non-streaming passthrough finalizer base_passthrough_process_llm_request in common_request_processing.py merges dict(fastapi_response.headers) into the outgoing response so LiteLLM metadata headers like x-litellm-call-id survive. At that point fastapi_response is an empty placeholder Response(), and an empty FastAPI Response carries a default content-length: 0. Before #29120, get_response_headers sanitized the upstream headers against an exclusion list (which drops content-length) but did not run custom_headers through that same list, so content-length: 0 leaked through and overrode the real upstream Content-Length. For empty bodies this was harmless; for any non-empty Bedrock Invoke body uvicorn/h11 saw a body longer than the declared length and aborted, leaving the client with a 200 and an empty/truncated payload

The unit test added in #27412 asserted on the response object in-process and never re-derived the Content-Length a client receives, so the Content-Length/body mismatch was invisible to it. That gap is what this PR closes

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

This is a test-only change, so a live-proxy curl can't demonstrate it (the fix is already on staging and there is no behavior change to curl). The meaningful proof is that the new tests fail against the pre-#29120 behavior and pass with the sanitization in place

With the sanitization in place:

$ python -m pytest tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py \
    -k "sanitizes_custom_headers or survives_http_serialization" -q
2 passed

After temporarily reverting #29120 (return_headers.update(custom_headers), no exclusion filter) and re-running, both tests fail on the leaked content-length: 0:

test_bedrock_passthrough_nonempty_body_survives_http_serialization
>       assert int(response.headers["content-length"]) == len(upstream_body)
E       assert 0 == 91

test_get_response_headers_sanitizes_custom_headers
>       assert "content-length" not in lowered
E       AssertionError: assert 'content-length' not in {'content-length': '0', ...}
2 failed

The integration test serializes the finalizer's output through an in-process ASGI server (httpx.ASGITransport), so the Content-Length the client receives is the leaked 0 while the served body is 91 bytes. That declared-length/body mismatch is exactly what aborted non-empty Bedrock bodies in production once uvicorn/h11 enforced the declared length over a real socket

Type

✅ Test

Changes

Adds two tests to tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py

test_get_response_headers_sanitizes_custom_headers pins the custom_headers exclusion branch that #29120 added; the existing get_response_headers test passes no custom headers, so that branch had no coverage

test_bedrock_passthrough_nonempty_body_survives_http_serialization drives the actual base_passthrough_process_llm_request finalizer rather than reconstructing the header assembly inline. A subclass overrides the upstream call (no monkeypatching) to return a stubbed non-empty Bedrock-style body, and a real placeholder Response() is passed in as the leak source, so the test exercises the exact get_response_headers(headers=result.headers, custom_headers=dict(fastapi_response.headers)) call the bug lived in. The finalizer's output is then serialized through an in-process ASGI server (httpx.ASGITransport) and the served body, recomputed Content-Length, upstream x-amzn-requestid, and injected x-litellm-call-id are asserted to arrive intact. tests/test_litellm/ is documented as mock-only (tests/test_litellm/readme.md), so the test stays in-process instead of binding a real loopback socket; the leaked content-length: 0 still surfaces as the value the client receives, which is the mismatch the object-level assertion in #27412 never checked

The non-streaming passthrough finalizer merges dict(fastapi_response.headers)
into the outgoing response, and an empty FastAPI Response carries a default
content-length: 0. Before the get_response_headers sanitization fix, that
leaked through and overrode the real upstream Content-Length, so uvicorn/h11
aborted any non-empty Bedrock Invoke body and the client saw a 200 with an
empty/truncated payload.

Adds a unit test pinning the custom_headers exclusion path and an integration
test that drives a non-empty passthrough body through a real ASGI server, which
is the only level at which the Content-Length mismatch surfaces. Both fail
against the pre-fix behavior and pass with the sanitization in place.
@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…sion

The transport test reconstructed the header assembly inline, so it pinned
get_response_headers over a socket but never exercised the finalizer that
actually leaked content-length: 0 (base_passthrough_process_llm_request
passing dict(fastapi_response.headers)). Drive that finalizer directly via a
subclass that overrides the upstream call, with a real placeholder Response as
the leak source, and serve its output over a real uvicorn/h11 server so a
regression in the finalizer itself is caught, not just in the helper.
@mateo-berri
mateo-berri marked this pull request as ready for review June 6, 2026 16:42
@mateo-berri
mateo-berri requested a review from yuneng-berri June 6, 2026 16:43
@greptile-apps

greptile-apps Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This is a test-only PR adding regression coverage for the Bedrock passthrough Content-Length: 0 bug fixed in #29120. No production code is changed.

  • test_get_response_headers_sanitizes_custom_headers pins the custom_headers sanitization branch added by fix(bedrock): support tool search results + chat annotations #29120, which had zero coverage before this PR (the existing test passed no custom headers).
  • test_bedrock_passthrough_nonempty_body_survives_http_serialization drives the real base_passthrough_process_llm_request finalizer end-to-end using httpx.ASGITransport (fully in-process, no real sockets) and asserts the Content-Length the client receives matches the actual body, the exact mismatch that was invisible to the object-level assertion in Fix Bedrock passthrough call ID headers #27412.

Confidence Score: 5/5

Safe to merge — test-only change, no production code altered.

The two new tests are narrowly scoped to the bug they document, use an in-process ASGI transport (no real sockets, satisfying the mock-only folder rule), correctly exercise the exact code path that contained the bug, and would have caught the original regression. No production code is touched.

No files require special attention.

Important Files Changed

Filename Overview
tests/test_litellm/proxy/pass_through_endpoints/test_pass_through_endpoints.py Adds two regression tests: a unit test for get_response_headers custom-header sanitization and an in-process ASGI integration test that verifies Content-Length is never leaked from the FastAPI placeholder response into a non-empty Bedrock body.

Reviews (3): Last reviewed commit: "Merge remote-tracking branch 'origin/lit..." | Re-trigger Greptile

…cess

tests/test_litellm is documented as mock-only (tests/test_litellm/readme.md),
but the Bedrock passthrough regression test bound a real loopback socket and ran
a live uvicorn server in a background thread. Drive the same finalizer output
through an in-process ASGI server (httpx.ASGITransport) instead, asserting the
recomputed Content-Length the client receives against the served body.

The test still fails against the pre-#29120 leak, where the placeholder
Response's content-length: 0 overrides the real upstream length, and passes once
get_response_headers sanitizes custom headers, without opening a real socket.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

Addressed the folder-rule concern from the last review. tests/test_litellm/ is documented as mock-only in tests/test_litellm/readme.md, so the regression test no longer binds a real loopback socket or runs a live uvicorn server in a thread. It now serializes the finalizer's output through an in-process ASGI server via httpx.ASGITransport, which still re-derives the Content-Length the client receives and catches the leaked content-length: 0 (the test fails against the pre-#29120 behavior on assert 0 == 91 and passes with the sanitization in place). The unused socket/threading/uvicorn imports and the _serve_app helper are gone


Generated by Claude Code

…itellm_bedrock_passthrough_content_length_regression
@mateo-berri

Copy link
Copy Markdown
Contributor Author

Merged the latest litellm_internal_staging into this branch. The branch had fallen behind and was missing #29702 (route Claude Opus 4.8 through adaptive thinking), which an unrelated live-Bedrock CI test (test_reasoning_effort_grid[bedrock-claude-opus-4-8]) depends on; that was the only red on the previous head and it is fixed on the base. The test change in this PR is unchanged.

@greptileai


Generated by Claude Code

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🚅 Hi, thanks for the PR! I'm Agent Shin, the automated triage bot for this repository. What's this and why am I getting it?

I read the description against our contribution rubric. Here's how it lined up:

What you got right:

  • ✅ Linked a related GitHub issue
  • ✅ Clear problem description
  • ✅ Expected vs. actual behavior

What's still missing:

  • end-to-end QA proof with real commands and real output, or a screenshot/video
  • non-mocked external-system demonstration; the provided pytest output is unit/in-process test output and does not count as end-to-end proof

The PR has solid context and a clear problem/expected-vs-actual description, but the only evidence shown is pytest/in-process test output. Per policy, mocked or repository unit tests do not satisfy QA proof, so the PR must fail triage.

If the description isn't updated in the next 24 hours, I'll auto-close this PR. That's not us saying we don't care about the change; we want the open-PR list to mirror what a maintainer can act on right now, so contributors don't get lost in a backlog. A closed PR is a soft "park this for later," not a rejection. Take your time; everything below still works after the close.

During the grace period: just update the PR description with the missing pieces. No need to ping me; I'll re-check on the next sweep and skip the auto-close if it now passes. See what counts as QA proof for the full rubric (a linked issue alone isn't enough; it covers context, not proof).

If the PR does get auto-closed in 24 hours, you still have easy recovery paths:

  • Comment @agent-shin reconsider after updating the description. I'll re-evaluate and reopen the PR if it now passes.
  • Comment @greptileai to request a fresh Greptile review; that still works even after the PR is closed, and a stronger score is one of the signals that lifts the PR back into the queue. So a low Greptile score isn't a blocker either.

Internal BerriAI contributors: this rubric doesn't apply to you; ping a maintainer.

(I'm an LLM, so I'm not infallible. If you think I got this wrong, ping a maintainer; they'll override me.)

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.

1 participant