Skip to content

Fix upstream url forming on forwardRequest - #188

Merged
vsits-proxy-builder[bot] merged 3 commits into
cnighswonger:mainfrom
nisqatsi:fix/nested-upstream-url-processing
Jun 7, 2026
Merged

Fix upstream url forming on forwardRequest#188
vsits-proxy-builder[bot] merged 3 commits into
cnighswonger:mainfrom
nisqatsi:fix/nested-upstream-url-processing

Conversation

@nisqatsi

@nisqatsi nisqatsi commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Upstream URL path after top-level domain was cut off by forwardRequest() on URL combining, e.g.:

config.upstream           clientReq.url                upstreamUrl - "/d/e" path lost
"https://a.b.c.net/d/e" + "/v1/messages?param=value" = "https://a.b.c.net/v1/messages?param=value"

@cnighswonger cnighswonger added bug Something isn't working community-reported Originally reported by a community member P1 High — near-term target labels Jun 4, 2026

@vsits-proxy-builder vsits-proxy-builder Bot left a comment

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.

Thanks for catching this! The bug is real and I reproduced it locally to confirm.

Empirical verification

Current behavior on main (your case 2):

new URL("/v1/messages", "https://corp-proxy.example.net/anthropic-mirror")
  → "https://corp-proxy.example.net/v1/messages"  ❌ "/anthropic-mirror" dropped

With your fix:

"https://corp-proxy.example.net/anthropic-mirror" + "/v1/messages"
  → "https://corp-proxy.example.net/anthropic-mirror/v1/messages"  ✓

The trailing-slash handling on the base + leading-slash on the relative is the right shape. Query strings flow through cleanly — verified ?beta=true&x=y etc. round-trip via url.search.

What the fix is doing correctly

  • Idempotent on whether config.upstream has a trailing / (both "https://x.net/d" and "https://x.net/d/" produce the same final URL)
  • Preserves query strings unchanged (the URL constructor parses them on the final concatenated string, so clientReq.url = "/v1/messages?beta=true" round-trips correctly)
  • Handles both no-path and with-path config.upstream values

One subtle behavior change worth noting (probably not a regression in practice)

The original new URL(clientReq.url, config.upstream) is RFC 3986 relative-resolution: if clientReq.url is an absolute URL (https://api.anthropic.com/v1/messages), the base is ignored and the absolute URL wins. The new string-concat path produces https://corp-proxy.example.net/mirror/https://api.anthropic.com/... instead.

In practice this won't surface for cache-fix because Node's HTTP server populates req.url with the path-only form (/v1/messages) for normal proxy use — but worth flagging in case anyone embeds this proxy in a forward-proxy scenario where clients send absolute URLs.

Could you add a regression test?

test/proxy-upstream-corp-proxy.test.mjs already has the CACHE_FIX_PROXY_UPSTREAM env-var scaffolding; a test there with CACHE_FIX_PROXY_UPSTREAM=http://127.0.0.1:<port>/mirror and an upstream that asserts req.url === "/mirror/v1/messages" would pin this against future regression. Happy to walk through the test shape if it'd help, or I can push the test directly to your branch if you'd prefer (you have maintainer_can_modify enabled which makes that clean).

Disposition

The fix itself is good and the bug is real. Holding off on a formal gh pr review --approve until either:

  • You add the regression test, OR
  • You give me a thumbs-up to push the test to your branch myself, OR
  • Project lead waves the test requirement (this is a small targeted fix; reasonable to ship without if speed matters)

— Proxy Builder

@vsits-codex-review-agent vsits-codex-review-agent Bot left a comment

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.

Codex review:

I reproduced the upstream path-prefix bug and verified this patch fixes the supported proxy flow: a CACHE_FIX_PROXY_UPSTREAM such as https://corp-proxy.example.net/anthropic-mirror now correctly forwards /v1/messages as /anthropic-mirror/v1/messages. I also checked the authority edge cases most likely to matter operationally (IPv6 host, explicit port, userinfo, request query string) and did not find a correctness issue that warrants blocking this first community fix.

The one real gap is regression coverage: the current upstream tests do not yet pin the /mirror/v1/messages case. I recommend backfilling that test, but I do not consider it a blocker here. Review artifact is committed at docs/code-reviews/pr-188-upstream-url-composition-codex-review-2026-06-04.md on branch consult/pr-188-codex-review.

— Codex review

@vsits-codex-review-agent vsits-codex-review-agent Bot added the reviewed-by-codex-agent Directive/spec reviewed by Codex — no blocking findings label Jun 4, 2026
Aleksandr Usenko and others added 2 commits June 7, 2026 19:58
Extract the URL-construction logic into a pure `buildUpstreamUrl(base,
clientUrl)` helper exported from proxy/upstream.mjs, then table-test
it in test/proxy-upstream-corp-proxy.test.mjs with 8 cases covering:

- No-path base (preserves the pre-fix behavior for default
  https://api.anthropic.com upstream)
- Trailing-slash base normalization
- Mirror / corp-proxy base with one-segment path (the bug @nisqatsi
  caught — `new URL("/v1/messages", base)` drops the base path)
- Trailing-slash idempotence on a base-path
- Multi-segment base path
- Query string flow-through for the no-path case
- Query string flow-through with the base-path fix
- http + non-standard port + base-path

Matches the pure-function table-test pattern already established for
`selectProxyUrl` in the same file. forwardRequest now calls
buildUpstreamUrl(config.upstream, clientReq.url) for the same result.

1005/1005 tests pass.

Refs PR cnighswonger#188 / @nisqatsi.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vsits-proxy-builder

Copy link
Copy Markdown
Contributor

Per Chris's call, I pushed the regression test to your branch (`fix/nested-upstream-url-processing` on your fork). HEAD is now `3c3fd17`.

What landed

Two commits on the branch:

  1. Your original fix, rebased onto current main. Your single-commit branch was behind by the v4.0.0 cycle (`fix(proxy): surface extension load failures via /health (#196) #197`, `feat(proxy): hot-reload is now opt-in via CACHE_FIX_HOT_RELOAD (#196) #200`, `feat(proxy): thinking-block-sanitize v1 is now on by default (v4.0.0) #201`, `docs(proxy): post-#201 wording sweep — sanitize is now default-on #202` — the hot-reload opt-in, sanitize default-on, and observability work). I rebased cleanly; your fix replays as-is, just on top of the new defaults.

  2. `test(upstream): regression coverage for nested upstream URL preservation` — extracts the URL-construction into a pure `buildUpstreamUrl(base, clientUrl)` helper, then table-tests it in `test/proxy-upstream-corp-proxy.test.mjs` with 8 cases:

    • No-path base (preserves pre-fix behavior for default `api.anthropic.com`)
    • Trailing-slash base normalization
    • Mirror / corp-proxy with one-segment path (your bug — pins the fix against future regression)
    • Trailing-slash + base-path idempotence
    • Multi-segment base path
    • Query string with no-path base
    • Query string with base-path
    • http + non-standard port + base-path

Matches the pure-function table-test pattern already established for `selectProxyUrl` in the same file. `forwardRequest` now calls `buildUpstreamUrl(config.upstream, clientReq.url)` — same end-to-end behavior, just testable in isolation.

1005/1005 tests passing locally.

Why the force-push

Your branch was 5+ commits behind main; a rebase was the cleanest way to avoid the test landing on a stale base that would have masked unrelated post-v3.9.0 regressions. Sorry for the force-push without a heads-up — let me know if you'd prefer I always ask before that, or if the maintainer-can-modify lane is the standard you want me to use for these.

Holding off on `approved-by-code-agent` and `ready-for-merge` for now since Chris is the merge gate. Codex's `reviewed-by-codex-agent` from 2026-06-04 was on `2942aa8`; technically that approval is stale relative to the new HEAD, but the only delta is the test addition + the function extraction (no behavior change in the fix itself). I'll let Chris decide whether a Codex re-review is warranted before merge.

— Proxy Builder

@vsits-codex-review-agent vsits-codex-review-agent Bot left a comment

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.

Review: PR #188 Fix upstream url forming on forwardRequest

Date: 2026-06-07
Reviewed: PR #188 at bb7164e
Round: 2
Label applied: approved-by-codex-agent

What Is Correct

  • buildUpstreamUrl in proxy/upstream.mjs:194 is a faithful extraction of the already-reviewed 2bead94 URL-building logic. forwardRequest now delegates to that helper at proxy/upstream.mjs:202; the only substantive implementation change remains the original base-path preservation fix.
  • The regression table in test/proxy-upstream-corp-proxy.test.mjs:68 covers the real failure mode: RFC 3986 relative resolution dropping the configured base path for path-absolute request URLs. The cases also pin no-path upstreams, trailing-slash normalization, multi-segment mirror paths, preserved query strings, and non-default http ports.
  • Verification passed on the refreshed head. node --test test/proxy-upstream-corp-proxy.test.mjs passed, and the full npm test suite passed 1005/1005 on the implementation head before the docs-only review-artifact commit. This review artifact is committed at docs/code-reviews/pr-188-round-2-codex.md on the PR branch.

Blockers

None.

What Needs Attention

None.

Bloat / Non-Functional

None.

Recommendations

  • Merge on the refreshed approval state; I found no new regression risk in the post-2026-06-04 delta.

Bottom Line

Approve. The post-approval delta is a behavior-preserving helper extraction plus the missing regression coverage for the exact corp-proxy/base-path failure mode, and the PR remains green after full-suite verification.

— Codex review

@vsits-codex-review-agent vsits-codex-review-agent Bot added reviewed-by-codex-agent Directive/spec reviewed by Codex — no blocking findings approved-by-codex-agent Final implementation approval from Codex Agent and removed reviewed-by-codex-agent Directive/spec reviewed by Codex — no blocking findings labels Jun 7, 2026
@cnighswonger cnighswonger added approved-by-lead Final implementation approval from project lead ready-for-merge Required reviews are complete and no known blockers remain labels Jun 7, 2026

@vsits-proxy-builder vsits-proxy-builder Bot left a comment

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.

Approved per Chris's explicit go-ahead in-turn (2026-06-07). Codex round-2 APPROVED at bb7164e; @nisqatsi's fix + Proxy Builder's regression test coverage both vetted. Merging now.

— AI Team Lead (via Proxy Builder, on Chris's authorization)

@vsits-proxy-builder
vsits-proxy-builder Bot merged commit 19b38b7 into cnighswonger:main Jun 7, 2026
5 checks passed
This was referenced Jun 8, 2026
codeslake added a commit to codeslake/claude-code-cache-fix that referenced this pull request Jul 18, 2026
A proxy-configured client does not always tunnel: axios's built-in proxy
mode (which the Claude Code CLI's auto-updater and telemetry paths use)
sends `GET https://host/path HTTP/1.1` on the plain proxy connection
instead of issuing CONNECT. buildUpstreamUrl() treated that absolute URI
as an origin-form path and concatenated it onto the configured upstream
(`api.anthropic.com/https://downloads.claude.ai/...`), misrouting every
such request to the upstream host. Cloudflare answers 404, the CLI's
version check fails 3.5s after every session start, and the user gets a
permanent "✘ Auto-update failed" banner (1P event export and Datadog
flush 404 the same way).

- upstream.mjs: parseAbsoluteForm() + honor the absolute-form authority
  in buildUpstreamUrl(); origin-form concatenation (PR cnighswonger#188 contract)
  unchanged.
- server.mjs: forward mode normalizes absolute-form targets — upstream-
  origin targets reduce to origin-form so the normal routing (incl. the
  /v1/messages transform) applies; downloads.claude.ai routes through
  the existing rewrite; foreign hosts relay via handlePassthrough.
  Reverse mode keeps its 404 contract.
- forward-proxy.mjs: handleDownloadsAbsolute() adapter so both arrival
  styles (CONNECT-MITM and absolute-form) get the download rewrite.

Verified live A/B on macOS (CC 2.1.214): an agents view through the
buggy proxy renders the banner with two 404s in the debug log; the same
launch through the fixed proxy renders no banner and zero 404s, and the
/v1/messages cache transform still applies. Full suite: 1379 tests,
1377 pass; the one failure (proxy-server.test.mjs "routes to upstream")
reproduces identically on the unmodified base — corp-network artifact
(direct egress 403s), same hygiene class as cnighswonger#253 item 3.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
vsits-proxy-builder Bot pushed a commit that referenced this pull request Jul 31, 2026
axios's built-in proxy mode — which the CC CLI uses for its auto-updater, 1P event export, and Datadog flush — skips CONNECT and sends absolute-form targets on the plain proxy connection. buildUpstreamUrl() treated the absolute URI as an origin-form path and concatenated it onto the upstream, so every session start 404'd its version check and pinned a permanent 'Auto-update failed' banner.

parseAbsoluteForm() in upstream.mjs returns the authority when present. Forward mode normalizes before route dispatch: upstream-origin targets reduce to origin-form (so /v1/messages still gets the cache transform), downloads.claude.ai routes through the download rewrite, foreign hosts relay to their real destination. Reverse mode keeps its 404 contract, test-pinned.

Security posture is unchanged in kind: forward mode already blind-tunnels arbitrary CONNECT targets, and the proxy binds 127.0.0.1 by default. This is parity for clients that skip CONNECT.

Verified locally merged onto main: full suite 1440/0. The #188 base-path contract is intact.

Closes #261
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved-by-codex-agent Final implementation approval from Codex Agent approved-by-lead Final implementation approval from project lead bug Something isn't working community-reported Originally reported by a community member P1 High — near-term target ready-for-merge Required reviews are complete and no known blockers remain reviewed-by-codex-agent Directive/spec reviewed by Codex — no blocking findings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants