fix: stop following upstream redirects on the client's behalf (#661) - #664
fix: stop following upstream redirects on the client's behalf (#661)#664ranxianglei wants to merge 1 commit into
Conversation
fetchWithTimeout inherited undici's default redirect:"follow", which downgrades a POST to a GET and drops the body when the upstream answers 301/302/303. A redirecting upstream (CDN/WAF in front of the API) therefore turned a valid POST into a 405 at the redirect target. A forward proxy must not follow redirects for the client: pass the 3xx through (status + Location) and let the client follow it with its own policy. ACP compression is untouched — the kernel round-trip still runs before the forward (covered by the new regression tests). - fetch-util.ts: fetchWithTimeout defaults redirect to "manual" - server.ts: upstream/test probe opts into follow (internal, unchanged) - registry.ts: models.dev fetch opts into follow (internal, unchanged) - tests/redirect-passthrough.test.ts: 302 pass-through for POST and GET, target never contacted, ACP tags still present in the forwarded body
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-664Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr664.tgz
npm install -g packageThis comment is automatically updated on each push. |
[bot] 🏷 Review: LGTM — recommend merge. CI is green (test ubuntu 22/24, windows 22/24, build-artifact, version-guard all pass). I independently re-ran the pre-flight on the PR branch and reproduced the bug both ways. What I verified
Layer judgmentThe 405 is a symptom; the root cause is the proxy following redirects on the client's behalf (undici's default Minor observations (non-blocking)
Merge is human-only per repo rules — the PR is ready when you are. |
Problem (issue #661, second symptom)
With bili proxy + CA in front of an upstream that redirects (CDN/WAF), requests failed with HTTP 405.
Root cause
fetchWithTimeout(src/fetch-util.ts) never set theredirectoption, so undici's globalfetchused its defaultredirect: "follow". Per the fetch spec, following a 301/302/303 on a POST downgrades the request to GET and drops the body. So when the upstream answered the POST with a redirect, bili re-issued it as a GET at the redirect target — an API endpoint that only accepts POST — which returned 405 Method Not Allowed.A forward proxy must not follow redirects on the client's behalf: the 3xx (status +
Location) must be passed through and the client follows it with its own policy.Changes
src/fetch-util.ts—fetchWithTimeoutnow defaultsredirectto"manual"(the proxied request path:forward, compat retry, fake-completion retry, compress-loop / preflight / loop re-sends all inherit this).src/server.ts— the internalPOST /__bili/upstream/testprobe explicitly opts intoredirect: "follow"(behavior unchanged).src/registry.ts— the internal models.dev registry fetch explicitly opts intoredirect: "follow"(behavior unchanged).tests/redirect-passthrough.test.ts(new) — regression tests:405when the default is reverted to"follow"(reproduces the reported symptom exactly).ACP compression impact
None. The fix is transport-only: the kernel round-trip (
prepareAnthropic→processTurn→coreToAnthropic) still runs before the forward; only the redirect policy of the outgoing fetch changed. The new tests assert ACP tags are present in the forwarded body.Hypotheses checked (per issue request)
content-lengthis stripped and recomputed from the actual body;content-encodingis decoded and stripped before forwarding (consistent). Side finding (minor, not fixed here): the Follow-up from #617: robust session identity + no-spurious-400 for header-less clients (e.g. ZCode) #619 unsupported-encoding path forwards an still-encoded body whilecontent-encodingis stripped as hop-by-hop, which would confuse the upstream (400-class, not 405).req.method ?? "GET"); ACP only rewrites the body.Pre-flight
npm run typecheck✅npm test✅ 1248/1248 (includes 2 new tests)npm run build✅Related: PR #662 (per-route
passthroughfor body-fingerprinting upstreams) is an independent, complementary fix for the first symptom of #661.