Skip to content

add proxy-compatible upstream transport - #134

Merged
ndycode merged 15 commits into
git-plan/02-workspace-routingfrom
git-plan/03-proxy-compat
Mar 20, 2026
Merged

add proxy-compatible upstream transport#134
ndycode merged 15 commits into
git-plan/02-workspace-routingfrom
git-plan/03-proxy-compat

Conversation

@ndycode

@ndycode ndycode commented Mar 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • honor standard proxy environment variables for upstream Codex runtime requests without widening the change into auth or quota probes

What Changed

  • add a shared proxy-compatible request helper that resolves http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, and NO_PROXY semantics and attaches an undici proxy dispatcher only when the caller did not already provide transport overrides
  • wire that helper into both the primary upstream fetch path and the retry/fallback fetch path
  • add helper and integration regressions for env precedence, bypass behavior, explicit dispatcher precedence, and fallback retry transport reuse

Validation

  • npm run lint
  • npm run typecheck
  • npm test
  • npm test -- test/documentation.test.ts
  • npm run build

Docs and Governance Checklist

  • README updated (if user-visible behavior changed)
  • docs/getting-started.md updated (if onboarding flow changed)
  • docs/features.md updated (if capability surface changed)
  • relevant docs/reference/* pages updated (if commands/settings/paths changed)
  • docs/upgrade.md updated (if migration behavior changed)
  • SECURITY.md and CONTRIBUTING.md reviewed for alignment

Risk and Rollback

  • Risk level: Medium
  • Rollback plan: revert this PR or revert 7b40b05 and b526074 if it lands as separate commits

Additional Notes

  • Third PR in the stack; base this review on git-plan/02-workspace-routing.
  • The inherited test/__snapshots__/copy-oauth-success.test.ts.snap line-ending dirt was left out of the commit.

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

this pr adds a proxy-compatible fetch transport layer that honors standard http_proxy / https_proxy / no_proxy env vars and wires it into both the primary and fallback upstream fetch paths in index.ts. it introduces applyProxyCompatibleInit, a shared ProxyAgent dispatcher cache backed by undici, closeSharedProxyDispatchers with a while-loop drainer for concurrent-close safety, and cleanup registration via the existing lib/shutdown.ts mechanism.

  • subdomain bypass gap (p1): shouldBypassProxyForUrl uses exact-match for bare hostname NO_PROXY entries. NO_PROXY=internal.example will not bypass service.internal.example, unlike curl/wget convention — this is a silent token-safety risk on corporate networks where auth headers shouldn't reach the proxy
  • unbounded while-loop: closeSharedProxyDispatchers loops until the dispatcher map is empty with no iteration cap; a buggy or pathological close() that re-inserts dispatchers could hang shutdown indefinitely, blocking windows temp-file cleanup
  • test indentation: the new afterEach block uses spaces while the rest of test/fetch-helpers.test.ts uses tabs
  • the * wildcard guard inside parseNoProxyEntries loop (addressing a previous review concern) is correctly in place
  • the two-pass while loop to catch dispatchers created during close is correct in principle, just needs a safety cap
  • undici ^6.24.1 is the only new production dep; node >=18.17 engine requirement is within the project's existing >=18 baseline

Confidence Score: 3/5

  • safe to merge after fixing the subdomain NO_PROXY bypass gap — current behavior silently routes subdomain traffic through the proxy when a bare apex hostname is listed
  • the proxy plumbing is well-structured and the dispatcher-sharing/cleanup logic correctly addresses the concurrent-close scenarios flagged in earlier review rounds. the one functional correctness issue — bare NO_PROXY entries not covering subdomains — is a curl-incompatible behavior that could cause unexpected proxy leakage of upstream auth headers on corporate networks. the unbounded while-loop is a low-probability availability risk on windows during shutdown. everything else (env precedence, wildcard bypass, explicit dispatcher precedence, fallback retry wiring) looks correct and is well-covered by the new vitest cases.
  • lib/request/fetch-helpers.ts — subdomain bypass logic in shouldBypassProxyForUrl and the while-loop cap in closeSharedProxyDispatchers

Important Files Changed

Filename Overview
lib/request/fetch-helpers.ts adds proxy env resolution, NO_PROXY parsing, shared ProxyAgent dispatcher cache, and cleanup registration — functional but has a curl-incompatible subdomain bypass gap and an unbounded while-loop in the drainer
index.ts minimal change — wraps both primary and fallback fetch calls with applyProxyCompatibleInit; correct and low-risk
test/fetch-helpers.test.ts good env-precedence, bypass, and dispatcher-sharing coverage; missing a subdomain-bypass case for bare NO_PROXY entries; afterEach uses spaces while file uses tabs
test/index.test.ts adds applyProxyCompatibleInit mock stub and integration tests confirming dispatcher is attached on primary and fallback retry paths; also backfills getCurrentWorkspace stub needed for workspace-routing base
package.json adds undici ^6.24.1 as a production dependency and sorts devDependencies — straightforward, node >=18.17 engine requirement is compatible with project's node >=18 baseline

Sequence Diagram

sequenceDiagram
    participant I as index.ts (fetch pipeline)
    participant A as applyProxyCompatibleInit
    participant R as resolveProxyUrlForRequest
    participant E as process.env
    participant G as getSharedProxyDispatcher
    participant P as ProxyAgent (undici)
    participant F as fetch()

    I->>A: applyProxyCompatibleInit(url, init)
    A->>A: check init.dispatcher / init.agent
    alt explicit dispatcher or agent already set
        A-->>I: return init unchanged
    else no existing transport
        A->>R: resolveProxyUrlForRequest(url, env)
        R->>E: read http_proxy / https_proxy / no_proxy
        E-->>R: env values
        R->>R: shouldBypassProxyForUrl(parsed, noProxy)
        alt bypass matches
            R-->>A: undefined
            A-->>I: return init unchanged
        else proxy applies
            R-->>A: proxyUrl string
            A->>G: getSharedProxyDispatcher(proxyUrl)
            G->>G: check sharedProxyDispatchers map
            alt cached
                G-->>A: existing ProxyAgent
            else new
                G->>P: new ProxyAgent(proxyUrl)
                P-->>G: dispatcher
                G->>G: store in map
                G-->>A: new ProxyAgent
            end
            A-->>I: init + { dispatcher }
        end
    end
    I->>F: fetch(url, { ...init, dispatcher })
Loading

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/request/fetch-helpers.ts
Line: 482-491

Comment:
**bare hostname in NO_PROXY doesn't cover its subdomains**

curl convention treats `NO_PROXY=internal.example` as matching both `internal.example` and all subdomains (`service.internal.example`, etc.). the current exact-match path only returns `true` when `hostname === entry.hostname`, so `NO_PROXY=internal.example` silently routes `service.internal.example` through the proxy. users who copy a standard curl no-proxy config will get unexpected proxy leakage — a token safety concern on corporate or vpn-gated networks where the proxy shouldn't see upstream auth headers.

the fix is to also test whether the target hostname ends with a `.`-prefixed version of the bare entry:

```typescript
if (!/^[.*]/.test(entry.hostname)) {
    if (hostname === entry.hostname || hostname.endsWith(`.${entry.hostname}`)) {
        return true;
    }
    continue;
}
```

also missing a vitest case: `NO_PROXY: 'internal.example'` with target `service.internal.example` should return `undefined`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: lib/request/fetch-helpers.ts
Line: 533-545

Comment:
**`closeSharedProxyDispatchers` while-loop has no iteration cap**

the `while (sharedProxyDispatchers.size > 0)` loop is necessary to catch dispatchers created during an async `close()`, but it has no bound. if a `dispatcher.close()` implementation keeps re-inserting a new dispatcher (e.g., a buggy mock or a close that triggers a reconnect attempt), the loop never terminates and blocks shutdown. on windows this is a concrete risk: hung shutdown leaves undici socket handles open, causing EBUSY/EPERM against temp files that `removeWithRetry` is designed to paper over.

a two-pass drain (current iteration + one re-check pass) covers every realistic concurrent injection scenario without the unbounded spin:

```typescript
export async function closeSharedProxyDispatchers(): Promise<void> {
    let iterations = 0;
    const MAX_DRAIN_ITERATIONS = 10;
    while (sharedProxyDispatchers.size > 0 && iterations++ < MAX_DRAIN_ITERATIONS) {
        const dispatchers = [...sharedProxyDispatchers.values()] as ClosableDispatcher[];
        sharedProxyDispatchers.clear();
        await Promise.allSettled(
            dispatchers.map(async (dispatcher) => {
                if (typeof dispatcher.close === "function") {
                    await dispatcher.close();
                }
            }),
        );
    }
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: test/fetch-helpers.test.ts
Line: 25-28

Comment:
**mixed indentation in afterEach block**

the new `afterEach` block uses 8-space indentation while the rest of the test file uses tabs. vitest's formatter won't care, but it breaks the file's consistent tab-indented style and will produce noisy diffs on any future edit.

```suggestion
	afterEach(async () => {
		await closeSharedProxyDispatchers();
		vi.restoreAllMocks();
	});
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: "chore(stack): finali..."

Greptile also left 1 inline comment on this PR.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Mar 19, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Summary

This PR adds proxy environment-variable support for upstream Codex runtime requests, introducing a new applyProxyCompatibleInit helper that resolves http_proxy/HTTP_PROXY, https_proxy/HTTPS_PROXY, and no_proxy/NO_PROXY environment variables and attaches undici ProxyAgent dispatchers to outbound fetch requests. Medium severity: introduces a new production dependency (undici 6.24.1) and manages shared dispatcher lifecycle with acknowledged cleanup race conditions, but auth and quota probes remain unchanged and comprehensive test coverage is included.

Key Changes

Core proxy implementation (lib/request/fetch-helpers.ts):

  • Shared ProxyAgent dispatcher cache keyed by proxy URL with lifecycle management via closeSharedProxyDispatchers()
  • resolveProxyUrlForRequest() that reads proxy env vars with precedence (uppercase variants checked first) and implements no_proxy bypass logic supporting wildcard (*), exact host matches, and suffix patterns (e.g., .example.com)
  • applyProxyCompatibleInit() that preserves explicit dispatcher/agent overrides, otherwise attaches appropriate shared proxy dispatcher
  • Exports: ProxyCompatibleRequestInit interface, resolveProxyUrlForRequest(), closeSharedProxyDispatchers(), and applyProxyCompatibleInit()

Integration points (index.ts):

  • Wrapped two fetch calls (primary request and streaming failover) through applyProxyCompatibleInit() before execution
  • No changes to control flow, error handling, or metrics

Dependency addition (package.json):

  • Added undici at ^6.24.1 as production dependency

Test coverage (test/fetch-helpers.test.ts, test/index.test.ts, test/index-retry.test.ts):

  • Unit tests for proxy URL resolution precedence, NO_PROXY bypass (including wildcard), shared dispatcher reuse, cleanup races, and explicit dispatcher/agent preservation
  • Integration tests verifying dispatcher attachment to fetch calls and preservation across retry attempts
  • Updated mocks to include applyProxyCompatibleInit()

Risks & Gaps

  • New production dependency: undici 6.24.1 introduces external risk; version is pinned in lockfile
  • Acknowledged test gaps: PR notes missing vitest case for port-specific NO_PROXY filtering and minor inefficiency in resolveProxyUrlForRequest() for http: requests when only HTTPS_PROXY is set
  • Dispatcher cleanup race: PR notes potential while-loop drain race during concurrent dispatcher creation/shutdown
  • Security consideration: Proxy configuration via environment variables—ensure environment is trusted and not user-controlled

Regression Testing

Comprehensive unit and integration tests added; no regression risk to auth/quota paths (unchanged). Shared dispatcher reuse and cleanup race conditions are tested. Some acknowledged coverage gaps for edge cases (port-specific NO_PROXY filtering).

Walkthrough

adds proxy support to outgoing http/https requests via environment-driven proxy resolution and shared dispatcher management. introduces proxy url detection from standard env vars, no_proxy bypass matching, and request init adaptation while preserving explicit agent/dispatcher assignments.

Changes

Cohort / File(s) Summary
Proxy request initialization
index.ts
Wraps both initial and failover fetch calls with applyProxyCompatibleInit() to apply proxy-compatible request initialization before execution.
Proxy infrastructure
lib/request/fetch-helpers.ts
Adds proxy resolution from environment (http_proxy, https_proxy, no_proxy with support for wildcard and suffix matching), shared ProxyAgent dispatcher lifecycle management via closeSharedProxyDispatchers(), and applyProxyCompatibleInit() that attaches or preserves dispatcher on request init.
Runtime dependency
package.json
Adds undici at ^6.24.1 as new dependency to support proxy transport; reorders existing dependency entries.
Proxy feature tests
test/fetch-helpers.test.ts
Adds comprehensive proxy test section covering url resolution precedence, no_proxy bypass matching (including wildcards), dispatcher reuse and caching, cleanup race handling, and explicit agent/dispatcher preservation.
Test mock updates
test/index-retry.test.ts, test/index.test.ts
Extends mocked fetch-helpers.js with applyProxyCompatibleInit() export; adds workspace selection helper to mocked AccountManager; adds two integration tests verifying proxy dispatcher attachment across primary and retry fetch calls.

Sequence Diagram(s)

sequenceDiagram
    participant client as Client Code
    participant init as applyProxyCompatibleInit
    participant resolver as resolveProxyUrlForRequest
    participant pool as Shared Dispatcher Pool
    participant fetch as globalThis.fetch
    
    client->>init: url, requestInit, env
    init->>resolver: url, env
    resolver-->>init: proxyUrl or undefined
    alt Explicit dispatcher/agent in init
        init-->>client: init unchanged
    else Proxy configured
        init->>pool: get or create dispatcher for proxyUrl
        pool-->>init: dispatcher instance
        init->>init: attach dispatcher to init
        init-->>client: init with dispatcher
    else No proxy
        init-->>client: init unchanged
    end
    client->>fetch: url, init (with optional dispatcher)
    fetch-->>client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes


review notes

proxy logic concerns:

  • lib/request/fetch-helpers.ts:resolveProxyUrlForRequest() implements no_proxy matching with wildcard/suffix patterns. the * wildcard is checked as exact match; suffix patterns require leading * (e.g., *.example.com). flag: verify this handles edge cases like example.com (no leading dot) matching *.example.com correctly, and confirm windows env var case-sensitivity is tested.

  • dispatcher caching in the shared pool uses proxy url as key. closeSharedProxyDispatchers() drains and clears all dispatchers, but concurrent requests during cleanup could create new dispatchers while shutdown is in flight. test/fetch-helpers.test.ts:+line covers "shutdown/cleanup race handling" — confirm this test actually blocks new dispatcher creation during cleanup or documents the expected eventual consistency behavior.

  • undici dependency added at ^6.24.1. verify this version is stable and aligns with node version support in your package.json engines field. also confirm ProxyAgent is the correct undici export being used (not Agent or HttpProxyAgent).

test coverage gaps:

  • test/fetch-helpers.test.ts tests proxy resolution and dispatcher management well, but missing: requests to http vs https targets with mixed proxy configs (http_proxy for http targets, https_proxy for https). verify both paths are covered.

  • test/index.test.ts adds two tests for dispatcher attachment across primary and retry calls. missing: test coverage for actual proxy failover (e.g., proxy times out, fallback retries without proxy or with different proxy). current tests only verify dispatcher presence, not functional proxy behavior.

  • no regression tests for non-proxy paths. index.ts:+line modified both fetch calls; confirm existing tests still pass for requests when no proxy env is set.

windows compatibility:

  • resolveProxyUrlForRequest() reads HTTP_PROXY, http_proxy, HTTPS_PROXY, https_proxy, NO_PROXY, no_proxy with precedence (uppercase first). windows env vars are case-insensitive but node exposes them as-is. confirm test suite runs on windows or explicitly documents this as unix-only. test/fetch-helpers.test.ts should include a test for lowercase env var fallback on case-sensitive systems.

  • proxy url parsing (host, port extraction for dispatcher) — does new URL(proxyUrl) handle edge cases like malformed proxy strings or non-http/https schemes? no validation visible in diff.

concurrency/lifecycle:

  • closeSharedProxyDispatchers() is async and calls dispatcher.close() on each. if two requests race and call this simultaneously, the pool is cleared but could be repopulated by concurrent requests. the test mentions "race handling" but verify the implementation actually serializes cleanup or documents non-blocking semantics.

  • shared dispatcher is keyed by proxy url only. if the same proxy url is used for both http and https targets, both will share one dispatcher. confirm this is intentional and that undici's ProxyAgent handles both schemes correctly.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'add proxy-compatible upstream transport' follows the required format with lowercase imperative, is 39 characters (under 72), but is missing the required type prefix (fix/feat/chore/docs/refactor/test). Prepend a type prefix: 'feat: add proxy-compatible upstream transport' or 'chore: add proxy-compatible upstream transport'.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description is comprehensive with summary, what changed, validation checklist (all marked complete), risk/rollback, and additional notes. Most required sections are populated meaningfully.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch git-plan/03-proxy-compat
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch git-plan/03-proxy-compat
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread lib/request/fetch-helpers.ts
Comment thread lib/request/fetch-helpers.ts
Comment thread lib/request/fetch-helpers.ts
@ndycode ndycode added the passed label Mar 20, 2026
Comment on lines +522 to +530
function getSharedProxyDispatcher(proxyUrl: string): ProxyDispatcher {
const existing = sharedProxyDispatchers.get(proxyUrl);
if (existing) {
return existing;
}

const dispatcher = new ProxyAgent(proxyUrl) as unknown as ProxyDispatcher;
sharedProxyDispatchers.set(proxyUrl, dispatcher);
return dispatcher;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 ProxyAgent constructor throw misrouted as network error

new ProxyAgent(proxyUrl) calls new URL(uri) internally and throws a synchronous TypeError when proxyUrl is not a valid URL (e.g., HTTPS_PROXY=bad-value). that throw propagates out of getSharedProxyDispatcherapplyProxyCompatibleInit, which is evaluated before fetch is called at both index.ts:1634 and index.ts:2196. the surrounding catch (networkError) block in index.ts then captures it, increments runtimeMetrics.totalRequests, and triggers account rotation and retry logic — treating a permanent configuration error as a transient network failure. this masks the root cause, burns quota on every affected account, and never surfaces a useful error to the user.

wrap the constructor call so config errors are distinguished from network errors:

function getSharedProxyDispatcher(proxyUrl: string): ProxyDispatcher {
	const existing = sharedProxyDispatchers.get(proxyUrl);
	if (existing) {
		return existing;
	}

	let dispatcher: ProxyDispatcher;
	try {
		dispatcher = new ProxyAgent(proxyUrl) as unknown as ProxyDispatcher;
	} catch (err) {
		throw new Error(
			`invalid proxy URL in environment (${proxyUrl}): ${err instanceof Error ? err.message : String(err)}`,
			{ cause: err },
		);
	}
	sharedProxyDispatchers.set(proxyUrl, dispatcher);
	return dispatcher;
}

the caller applyProxyCompatibleInit should re-throw without swallowing so index.ts can distinguish the error type and not retry it. also worth noting: on windows, the runtimeMetrics.totalRequests++ at line 1633 fires before applyProxyCompatibleInit is evaluated, so a bad proxy config still inflates the counter even though no actual request is made.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/request/fetch-helpers.ts
Line: 522-530

Comment:
**`ProxyAgent` constructor throw misrouted as network error**

`new ProxyAgent(proxyUrl)` calls `new URL(uri)` internally and throws a synchronous `TypeError` when `proxyUrl` is not a valid URL (e.g., `HTTPS_PROXY=bad-value`). that throw propagates out of `getSharedProxyDispatcher``applyProxyCompatibleInit`, which is evaluated *before* `fetch` is called at both index.ts:1634 and index.ts:2196. the surrounding `catch (networkError)` block in `index.ts` then captures it, increments `runtimeMetrics.totalRequests`, and triggers account rotation and retry logic — treating a permanent configuration error as a transient network failure. this masks the root cause, burns quota on every affected account, and never surfaces a useful error to the user.

wrap the constructor call so config errors are distinguished from network errors:

```typescript
function getSharedProxyDispatcher(proxyUrl: string): ProxyDispatcher {
	const existing = sharedProxyDispatchers.get(proxyUrl);
	if (existing) {
		return existing;
	}

	let dispatcher: ProxyDispatcher;
	try {
		dispatcher = new ProxyAgent(proxyUrl) as unknown as ProxyDispatcher;
	} catch (err) {
		throw new Error(
			`invalid proxy URL in environment (${proxyUrl}): ${err instanceof Error ? err.message : String(err)}`,
			{ cause: err },
		);
	}
	sharedProxyDispatchers.set(proxyUrl, dispatcher);
	return dispatcher;
}
```

the caller `applyProxyCompatibleInit` should re-throw without swallowing so index.ts can distinguish the error type and not retry it. also worth noting: on windows, the `runtimeMetrics.totalRequests++` at line 1633 fires *before* `applyProxyCompatibleInit` is evaluated, so a bad proxy config still inflates the counter even though no actual request is made.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@coderabbitai coderabbitai 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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
package.json (1)

95-96: ⚠️ Potential issue | 🟠 Major

tighten package.json engines.node before shipping undici 6.x.

undici 6.24.1 requires node >=18.17.0, but package.json still advertises >=18.0.0. this leaves node 18.0–18.16 inside the published range even though lib/request/fetch-helpers.ts:7 will break on those versions. missing regression test that validates minimum node version floor—your existing test suite mocks fetch and globalThis but doesn't verify behavior or failure on unsupported node releases.

raise the engine floor to >=18.17.0 or pin undici to a version matching your declared range.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 95 - 96, The package.json engines.node entry is
too permissive for the undici 6.x requirement; update the "engines": {"node":
...} value to ">=18.17.0" (or alternatively pin the undici dependency to a 6.x
release that supports ">=18.0.0") so runtime matches what
lib/request/fetch-helpers.ts (line referenced) expects; also add or update a
test that fails when running under unsupported Node versions (simulate or check
process.versions.node) to prevent regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/fetch-helpers.test.ts`:
- Around line 218-235: Add tests to cover port-specific NO_PROXY behavior for
resolveProxyUrlForRequest: include a case where NO_PROXY contains a host:port
(e.g., "service.internal.example:8443") and assert that a request to the same
host with matching port (https://service.internal.example:8443/...) bypasses the
proxy (returns undefined), and another request to the same host but a different
port (https://service.internal.example:9443/...) does not bypass (returns the
proxy URL). This ensures the new parsing logic in lib/request/fetch-helpers.ts
(lines ~455-481) that checks host:port entries is exercised and prevents
regressions.

In `@test/index-retry.test.ts`:
- Around line 21-25: The mocked applyProxyCompatibleInit is currently a no-op so
the test doesn't verify the helper runs for delayed fetches; modify the mock in
index-retry.test.ts to either (a) stamp a sentinel property (e.g., dispatcher:
"sentinel") onto the returned RequestInit from applyProxyCompatibleInit or (b)
replace applyProxyCompatibleInit with a vitest.spyOn of the real helper and
assert it was called; then update the assertion to wait for the delayed path and
verify globalThis.fetch was ultimately invoked with a Request/RequestInit that
contains that sentinel (or assert the spy was called), and apply the same change
to the corresponding block around lines 221-248 to ensure determinism for the
concurrency/regression case.

In `@test/index.test.ts`:
- Around line 1470-1513: Add a new unit test that verifies caller-provided
transport overrides take precedence over proxy attachment: in the test import
fetch-helpers and mock applyProxyCompatibleInit to return the incoming init
object
(vi.mocked(fetchHelpers.applyProxyCompatibleInit).mockImplementation((_url,
init) => init)); create a unique callerDispatcher object and set
globalThis.fetch to a resolved vi.fn(); call setupPlugin() then sdk.fetch! with
a request that includes dispatcher: callerDispatcher; finally assert the
response is 200 and that the first call to globalThis.fetch received an init
whose dispatcher is exactly the callerDispatcher (use
vi.mocked(globalThis.fetch).mock.calls[0]?.[1]). This ensures
applyProxyCompatibleInit does not overwrite an explicit dispatcher.

---

Outside diff comments:
In `@package.json`:
- Around line 95-96: The package.json engines.node entry is too permissive for
the undici 6.x requirement; update the "engines": {"node": ...} value to
">=18.17.0" (or alternatively pin the undici dependency to a 6.x release that
supports ">=18.0.0") so runtime matches what lib/request/fetch-helpers.ts (line
referenced) expects; also add or update a test that fails when running under
unsupported Node versions (simulate or check process.versions.node) to prevent
regressions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ec250c4-294a-467e-a393-8a2fc2e26cf6

📥 Commits

Reviewing files that changed from the base of the PR and between 49b10d4 and 8f7f1c6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • index.ts
  • lib/request/fetch-helpers.ts
  • package.json
  • test/fetch-helpers.test.ts
  • test/index-retry.test.ts
  • test/index.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
📓 Path-based instructions (2)
test/**

⚙️ CodeRabbit configuration file

tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior. reject changes that mock real secrets or skip assertions.

Files:

  • test/index-retry.test.ts
  • test/fetch-helpers.test.ts
  • test/index.test.ts
lib/**

⚙️ CodeRabbit configuration file

focus on auth rotation, windows filesystem IO, and concurrency. verify every change cites affected tests (vitest) and that new queues handle EBUSY/429 scenarios. check for logging that leaks tokens or emails.

Files:

  • lib/request/fetch-helpers.ts
🔇 Additional comments (5)
test/index.test.ts (5)

201-217: lgtm on the fetch-helpers mock update.

the applyProxyCompatibleInit mock setup is correct—returns init unchanged by default and allows per-test override for dispatcher injection.


357-364: getCurrentWorkspace implementation looks correct.

handles missing workspaces array, non-numeric index, and out-of-bounds access with safe defaults. matches the helper pattern in buildRoutingManager.


1210-1220: buildRoutingManager workspace helper is consistent.

same safe-access pattern as the mock account manager. properly returns null when workspace isn't found.


1447-1468: good coverage for primary request proxy dispatcher propagation.

the test verifies the dispatcher from applyProxyCompatibleInit reaches globalThis.fetch. one note: line 1466 casts to RequestInit but accesses .dispatcher which is undici-specific—not a blocker for test code but slightly misleading type-wise.


2266-2266: minor addition to support workspace interface.

returning null is the correct fallback for accounts without workspace configuration.

Comment on lines +218 to +235
it('bypasses the proxy when NO_PROXY matches the request host', () => {
const env = {
HTTPS_PROXY: 'http://proxy.example:8080',
NO_PROXY: 'api.openai.com,.internal.example',
} as NodeJS.ProcessEnv;

expect(resolveProxyUrlForRequest('https://api.openai.com/v1/chat', env)).toBeUndefined();
expect(resolveProxyUrlForRequest('https://service.internal.example/v1/chat', env)).toBeUndefined();
});

it('treats wildcard entries inside NO_PROXY lists as an explicit global bypass', () => {
const env = {
HTTPS_PROXY: 'http://proxy.example:8080',
NO_PROXY: 'api.openai.com,*,.internal.example',
} as NodeJS.ProcessEnv;

expect(resolveProxyUrlForRequest('https://unlisted.example/v1/chat', env)).toBeUndefined();
});

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.

⚠️ Potential issue | 🟡 Minor

add the missing port-specific no_proxy regression.

lib/request/fetch-helpers.ts:455-481 now parses host:port entries and skips bypasses when the port does not match, but test/fetch-helpers.test.ts:218-235 only covers host-only matches. add a matched/mismatched port case here so that branch does not regress silently.

suggested test
+		it('only bypasses when no_proxy host:port matches the request port', () => {
+			const env = {
+				HTTPS_PROXY: 'http://proxy.example:8080',
+				NO_PROXY: 'api.openai.com:8443',
+			} as NodeJS.ProcessEnv;
+
+			expect(
+				resolveProxyUrlForRequest('https://api.openai.com:8443/v1/chat', env),
+			).toBeUndefined();
+			expect(
+				resolveProxyUrlForRequest('https://api.openai.com/v1/chat', env),
+			).toBe('http://proxy.example:8080');
+		});

As per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('bypasses the proxy when NO_PROXY matches the request host', () => {
const env = {
HTTPS_PROXY: 'http://proxy.example:8080',
NO_PROXY: 'api.openai.com,.internal.example',
} as NodeJS.ProcessEnv;
expect(resolveProxyUrlForRequest('https://api.openai.com/v1/chat', env)).toBeUndefined();
expect(resolveProxyUrlForRequest('https://service.internal.example/v1/chat', env)).toBeUndefined();
});
it('treats wildcard entries inside NO_PROXY lists as an explicit global bypass', () => {
const env = {
HTTPS_PROXY: 'http://proxy.example:8080',
NO_PROXY: 'api.openai.com,*,.internal.example',
} as NodeJS.ProcessEnv;
expect(resolveProxyUrlForRequest('https://unlisted.example/v1/chat', env)).toBeUndefined();
});
it('bypasses the proxy when NO_PROXY matches the request host', () => {
const env = {
HTTPS_PROXY: 'http://proxy.example:8080',
NO_PROXY: 'api.openai.com,.internal.example',
} as NodeJS.ProcessEnv;
expect(resolveProxyUrlForRequest('https://api.openai.com/v1/chat', env)).toBeUndefined();
expect(resolveProxyUrlForRequest('https://service.internal.example/v1/chat', env)).toBeUndefined();
});
it('treats wildcard entries inside NO_PROXY lists as an explicit global bypass', () => {
const env = {
HTTPS_PROXY: 'http://proxy.example:8080',
NO_PROXY: 'api.openai.com,*,.internal.example',
} as NodeJS.ProcessEnv;
expect(resolveProxyUrlForRequest('https://unlisted.example/v1/chat', env)).toBeUndefined();
});
it('only bypasses when no_proxy host:port matches the request port', () => {
const env = {
HTTPS_PROXY: 'http://proxy.example:8080',
NO_PROXY: 'api.openai.com:8443',
} as NodeJS.ProcessEnv;
expect(
resolveProxyUrlForRequest('https://api.openai.com:8443/v1/chat', env),
).toBeUndefined();
expect(
resolveProxyUrlForRequest('https://api.openai.com/v1/chat', env),
).toBe('http://proxy.example:8080');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/fetch-helpers.test.ts` around lines 218 - 235, Add tests to cover
port-specific NO_PROXY behavior for resolveProxyUrlForRequest: include a case
where NO_PROXY contains a host:port (e.g., "service.internal.example:8443") and
assert that a request to the same host with matching port
(https://service.internal.example:8443/...) bypasses the proxy (returns
undefined), and another request to the same host but a different port
(https://service.internal.example:9443/...) does not bypass (returns the proxy
URL). This ensures the new parsing logic in lib/request/fetch-helpers.ts (lines
~455-481) that checks host:port entries is exercised and prevents regressions.

Comment thread test/index-retry.test.ts
Comment on lines 21 to 25
vi.mock("../lib/request/fetch-helpers.js", () => ({
extractRequestUrl: (input: any) => (typeof input === "string" ? input : String(input)),
rewriteUrlForCodex: (url: string) => url,
applyProxyCompatibleInit: (_url: string, init: RequestInit) => init,
transformRequestForCodex: async (init: any) => ({ updatedInit: init, body: { model: "gpt-5.1" } }),

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.

⚠️ Potential issue | 🟡 Minor

make this retry test prove the helper output survives the waited fetch.

test/index-retry.test.ts:21-25 turns applyProxyCompatibleInit into a pass-through, so this file still passes if the delayed request path stops calling the helper altogether. have the mock stamp a sentinel dispatcher, or spy on the helper directly, and assert the eventual globalThis.fetch receives it.

As per coding guidelines, test/**: tests must stay deterministic and use vitest. demand regression cases that reproduce concurrency bugs, token refresh races, and windows filesystem behavior.

Also applies to: 221-248

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index-retry.test.ts` around lines 21 - 25, The mocked
applyProxyCompatibleInit is currently a no-op so the test doesn't verify the
helper runs for delayed fetches; modify the mock in index-retry.test.ts to
either (a) stamp a sentinel property (e.g., dispatcher: "sentinel") onto the
returned RequestInit from applyProxyCompatibleInit or (b) replace
applyProxyCompatibleInit with a vitest.spyOn of the real helper and assert it
was called; then update the assertion to wait for the delayed path and verify
globalThis.fetch was ultimately invoked with a Request/RequestInit that contains
that sentinel (or assert the spy was called), and apply the same change to the
corresponding block around lines 221-248 to ensure determinism for the
concurrency/regression case.

Comment thread test/index.test.ts
Comment on lines +1470 to +1513
it("preserves proxy dispatcher on fallback retry requests", async () => {
const { AccountManager } = await import("../lib/accounts.js");
const fetchHelpers = await import("../lib/request/fetch-helpers.js");
const proxyDispatcher = { kind: "proxy-dispatcher" };
const manager = buildRoutingManager([
{
index: 0,
accountId: "token-primary",
accountIdSource: "token",
email: "alpha@example.com",
refreshToken: "refresh-1",
accessToken: "access-alpha",
},
{
index: 1,
accountId: "workspace-fallback",
accountIdSource: "org",
email: "beta@example.com",
refreshToken: "refresh-2",
accessToken: "access-beta",
},
]);
vi.spyOn(AccountManager, "loadFromDisk").mockResolvedValueOnce(manager as never);
vi.mocked(fetchHelpers.applyProxyCompatibleInit).mockImplementation((_url, init) => ({
...(init ?? {}),
dispatcher: proxyDispatcher,
}));
globalThis.fetch = vi
.fn()
.mockRejectedValueOnce(new Error("Network timeout"))
.mockRejectedValueOnce(new Error("Network timeout"))
.mockResolvedValueOnce(new Response(JSON.stringify({ content: "ok" }), { status: 200 }));

const { sdk } = await setupPlugin();
const response = await sdk.fetch!("https://api.openai.com/v1/chat", {
method: "POST",
body: JSON.stringify({ model: "gpt-5.1" }),
});

expect(response.status).toBe(200);
expect(vi.mocked(fetchHelpers.applyProxyCompatibleInit)).toHaveBeenCalledTimes(3);
const thirdInit = vi.mocked(globalThis.fetch).mock.calls[2]?.[1] as RequestInit;
expect(thirdInit.dispatcher).toBe(proxyDispatcher);
});

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.

🧹 Nitpick | 🔵 Trivial

fallback retry test covers dispatcher preservation across retries.

verifies applyProxyCompatibleInit is called 3 times and dispatcher is on the final retry init. solid.

however, per the pr summary, the helper should only attach a proxy dispatcher "when the caller has not provided transport overrides." i don't see a test verifying explicit caller-provided dispatcher/agent is preserved rather than overwritten.

consider adding a test case like:

it("preserves caller-provided dispatcher over proxy resolution", async () => {
    const fetchHelpers = await import("../lib/request/fetch-helpers.js");
    const callerDispatcher = { kind: "caller-dispatcher" };
    vi.mocked(fetchHelpers.applyProxyCompatibleInit).mockImplementation((_url, init) => init);
    globalThis.fetch = vi.fn().mockResolvedValue(
        new Response(JSON.stringify({ content: "ok" }), { status: 200 }),
    );

    const { sdk } = await setupPlugin();
    const response = await sdk.fetch!("https://api.openai.com/v1/chat", {
        method: "POST",
        body: JSON.stringify({ model: "gpt-5.1" }),
        dispatcher: callerDispatcher,
    });

    expect(response.status).toBe(200);
    const init = vi.mocked(globalThis.fetch).mock.calls[0]?.[1];
    expect(init.dispatcher).toBe(callerDispatcher);
});

this would cover the explicit transport override precedence behavior documented in the pr.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/index.test.ts` around lines 1470 - 1513, Add a new unit test that
verifies caller-provided transport overrides take precedence over proxy
attachment: in the test import fetch-helpers and mock applyProxyCompatibleInit
to return the incoming init object
(vi.mocked(fetchHelpers.applyProxyCompatibleInit).mockImplementation((_url,
init) => init)); create a unique callerDispatcher object and set
globalThis.fetch to a resolved vi.fn(); call setupPlugin() then sdk.fetch! with
a request that includes dispatcher: callerDispatcher; finally assert the
response is 200 and that the first call to globalThis.fetch received an init
whose dispatcher is exactly the callerDispatcher (use
vi.mocked(globalThis.fetch).mock.calls[0]?.[1]). This ensures
applyProxyCompatibleInit does not overwrite an explicit dispatcher.

@ndycode

ndycode commented Mar 20, 2026

Copy link
Copy Markdown
Owner Author

Shipped via release integration on main in 046993a (v1.2.0). Closing this stacked PR because there are no remaining commits relative to main.

@ndycode ndycode closed this Mar 20, 2026
@ndycode ndycode reopened this Mar 20, 2026
@ndycode
ndycode merged commit 64665cd into git-plan/02-workspace-routing Mar 20, 2026
1 check passed
Comment on lines +482 to +491
if (!/^[.*]/.test(entry.hostname)) {
if (hostname === entry.hostname) {
return true;
}
continue;
}

if (hostname.endsWith(entry.hostname.replace(/^\*/, ""))) {
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 bare hostname in NO_PROXY doesn't cover its subdomains

curl convention treats NO_PROXY=internal.example as matching both internal.example and all subdomains (service.internal.example, etc.). the current exact-match path only returns true when hostname === entry.hostname, so NO_PROXY=internal.example silently routes service.internal.example through the proxy. users who copy a standard curl no-proxy config will get unexpected proxy leakage — a token safety concern on corporate or vpn-gated networks where the proxy shouldn't see upstream auth headers.

the fix is to also test whether the target hostname ends with a .-prefixed version of the bare entry:

if (!/^[.*]/.test(entry.hostname)) {
    if (hostname === entry.hostname || hostname.endsWith(`.${entry.hostname}`)) {
        return true;
    }
    continue;
}

also missing a vitest case: NO_PROXY: 'internal.example' with target service.internal.example should return undefined.

Prompt To Fix With AI
This is a comment left during a code review.
Path: lib/request/fetch-helpers.ts
Line: 482-491

Comment:
**bare hostname in NO_PROXY doesn't cover its subdomains**

curl convention treats `NO_PROXY=internal.example` as matching both `internal.example` and all subdomains (`service.internal.example`, etc.). the current exact-match path only returns `true` when `hostname === entry.hostname`, so `NO_PROXY=internal.example` silently routes `service.internal.example` through the proxy. users who copy a standard curl no-proxy config will get unexpected proxy leakage — a token safety concern on corporate or vpn-gated networks where the proxy shouldn't see upstream auth headers.

the fix is to also test whether the target hostname ends with a `.`-prefixed version of the bare entry:

```typescript
if (!/^[.*]/.test(entry.hostname)) {
    if (hostname === entry.hostname || hostname.endsWith(`.${entry.hostname}`)) {
        return true;
    }
    continue;
}
```

also missing a vitest case: `NO_PROXY: 'internal.example'` with target `service.internal.example` should return `undefined`.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex

@ndycode
ndycode deleted the git-plan/03-proxy-compat branch March 20, 2026 13:26
@coderabbitai coderabbitai Bot mentioned this pull request May 27, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant