fix(ui): stop cloning body-carrying requests into stream uploads in fetchClient middleware - #34122
Merged
ryan-crabbe-berri merged 2 commits intoJul 21, 2026
Conversation
…etchClient middleware The openapi-fetch middleware rebuilt every outgoing request with new Request(url, request), which converts a string JSON body into a ReadableStream with duplex=half. Chromium only allows streaming uploads over HTTP/2 or HTTP/3, so against any HTTP/1.1 hop (uvicorn serves HTTP/1.1 only) the fetch dies at the network layer with net::ERR_ALPN_NEGOTIATION_FAILED, surfaced as "Failed to fetch". GET callers were unaffected (null body); the first body-carrying caller arrived with the MCP BYOK credential modal, breaking that flow on plain http deployments in the v1.94.0 RCs. The middleware now mutates headers on the original request when no runtime base is registered, and when rebasing onto a runtime base it rebuilds the request with the body materialized as bytes via arrayBuffer(), which fetch sends with Content-Length instead of a streaming upload
Contributor
Greptile SummaryThis PR changes how the dashboard middleware handles body-carrying requests. The main changes are:
Confidence Score: 5/5The upload fix looks mergeable after preserving the original referrer policy.
ui/litellm-dashboard/src/lib/http/api.ts
|
| Filename | Overview |
|---|---|
| ui/litellm-dashboard/src/lib/http/api.ts | Avoids streaming request clones during URL rebasing, but omits the original request's referrer policy. |
| ui/litellm-dashboard/src/lib/http/api.test.ts | Adds focused tests for byte-backed POST bodies on rebased and non-rebased requests. |
Reviews (1): Last reviewed commit: "fix(ui): stop cloning body-carrying requ..." | Re-trigger Greptile
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Contributor
yuneng-berri
approved these changes
Jul 21, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Relevant issues
Linear ticket
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
The bug is a browser network-layer failure, so the proof is captured in Chromium against a live HTTP/1.1 server (
python3 -m http.server 47831, same protocol uvicorn speaks), running the middleware's exact old and new request constructions from the page's consoleBefore (old construction, parent commit 212a921): a plain
fetchPOST with a string body reaches the server, while the middleware'snew Request(url, request)clone of the identical request turns the body into a stream and dies at the network layerAfter (new construction from this PR, commit 979e869): rebuilding with the body materialized as bytes reaches the server like the plain fetch does
(501 is python http.server declining POST; the point is the request reaches the server instead of failing before it leaves the browser)
To see it in the product: run the proxy over plain http on localhost:4000, run the dashboard dev server on localhost:3000, open the MCP Servers page, pick a server and use the "connect with your API key" credential modal. On the parent commit the save fails with "Failed to fetch"; on this branch it completes
Type
🐛 Bug Fix
Changes
The openapi-fetch middleware in
ui/litellm-dashboard/src/lib/http/api.tsrebuilt every outgoing request withnew Request(url, request). Per the fetch spec that converts a string JSON body into aReadableStreamwithduplex: "half", i.e. a streaming upload, which Chromium only allows over HTTP/2 or HTTP/3. LiteLLM's uvicorn server only speaks HTTP/1.1, so on any deployment where the browser reaches the proxy over plain http (docker-p 4000:4000, an HTTP/1.1-only edge) every body-carrying request failed withnet::ERR_ALPN_NEGOTIATION_FAILED, surfaced to the user as "Failed to fetch"The middleware landed in #29884 with only GET callers (null body, unaffected), which is why nothing broke in v1.93.0. #33103 added the first POST caller (the MCP BYOK credential modal), so that flow is broken on plain-http deployments in the v1.94.0 RCs. https deployments behind an h2 edge never saw it
The fix removes the unconditional clone. When no runtime base url is registered the middleware now injects the auth header on the original request in place; when a rebase is needed it rebuilds the request with the body read out as bytes via
arrayBuffer(), which fetch sends with Content-Length instead of a streaming upload, carrying over method, headers, credentials, signal and the other standard init fieldsThe regression test spies on the global
Requestconstructor and asserts no construction receives a body-carryingRequestas init or aReadableStreambody, for both the no-rebase and rebase paths, plus that the body round-trips as the exact JSON string with the auth header applied. Both tests fail on the parent commit and pass here. Existing coverage for header injection, url rebasing and ApiError mapping still passesNote #34116 deliberately kept
useSetKeyBlockedStateon the legacyapiClientbecause of this bug; with this fix it can move back tofetchClient, left out of here to keep the scope isolatedFinal Attestation