-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(transport): apply the fresh-connection policy to a selected provider transport #5022
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -45,9 +45,20 @@ modules merely because those imports existed in the pre-split `responses.ts` mon | |
|
|
||
| `OCX_FRESH_CONNECTION_HOSTS` accepts comma-separated hostnames whose outbound HTTP sends bypass | ||
| keep-alive reuse with `Connection: close` and `keepalive: false`; exact hosts and their subdomains | ||
| match case-insensitively. The helper applies this policy at the final executor boundary, after a | ||
| dispatch override has selected or rebuilt the destination, so matching follows the URL sent on the | ||
| wire rather than the URL supplied before credential revalidation. | ||
| match case-insensitively. `sendWithConnectionPolicy` applies the policy around the fetch that | ||
| performs the physical send, after a dispatch override has selected or rebuilt the destination, so | ||
| matching follows the URL sent on the wire rather than the URL supplied before credential | ||
| revalidation. | ||
|
|
||
| The wrapped executor alone is not that boundary. An override that revalidates credentials re-reads | ||
| `route.provider.fetch` at send time, because reselection can install a different provider transport | ||
| after the wrapper was built, and then calls that implementation instead of the executor. Both | ||
| production overrides do this -- `oauthDispatch` in `request-transport.ts` and the native Chat | ||
| key-revalidation override in `chat-native.ts` -- so both wrap the selected implementation rather | ||
| than choosing between policy and provider transport. Reporting the executor as the boundary while | ||
| the code let a provider-scoped transport past it is what #4992 recorded, and it is why a | ||
| regression for this policy has to enter through `handleResponses` rather than through a | ||
|
Comment on lines
+58
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This passage records the prior #4992 failure and why the regression was written rather than stating only the contract that holds now. That turns the architecture source-of-truth into an incident log that can become stale; retain the present-tense requirement about testing the real dispatch boundary, but move or remove the retrospective explanation. AGENTS.md reference: structure/AGENTS.md:L9-L14 Useful? React with 👍 / 👎. |
||
| hand-written override that cooperates by calling the executor it was handed. | ||
|
|
||
| ### Semantic progress ownership | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,14 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { mkdtempSync } from "node:fs"; | ||
| import { tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { wantsFreshConnection, providerFetch } from "../../src/server/responses/fetch-helpers"; | ||
| import type { OcxProviderConfig } from "../../src/types"; | ||
| import { saveCredential } from "../../src/oauth/store"; | ||
| import { XAI_GROK_CLI_BASE_URL } from "../../src/providers/xai-transport"; | ||
| import { handleResponses } from "../../src/server/responses"; | ||
| import type { RequestLogContext } from "../../src/server/request-log"; | ||
| import type { OcxConfig, OcxProviderConfig } from "../../src/types"; | ||
| import { removeTreeWithRetry } from "../helpers/remove-tree"; | ||
|
|
||
| describe("wantsFreshConnection", () => { | ||
| test("returns false when env is unset or empty", () => { | ||
|
|
@@ -246,3 +254,90 @@ describe("providerFetch fresh connection dispatch", () => { | |
| } | ||
| }); | ||
| }); | ||
|
|
||
| /** | ||
| * The cases above drive `providerFetch` with an override that cooperates by calling the executor | ||
| * it was handed. Production's OAuth override does not: it re-reads `route.provider.fetch` at the | ||
| * send boundary, because credential reselection can install a different provider transport after | ||
| * the wrapper was built, and calls that implementation directly. Every test above still passed | ||
| * while the operator's configured host reused a pooled socket on that path (#4992), so the | ||
| * regression has to enter through `handleResponses` rather than through a hand-written override. | ||
| * | ||
| * xAI OAuth is the live consumer: `resolveProviderTransport` installs a provider-scoped fetch and | ||
| * rewrites the destination to the Grok CLI host, which is what makes it observable here. | ||
| */ | ||
| describe("the OAuth dispatch boundary", () => { | ||
| test("a provider-scoped transport selected at dispatch receives the fresh-connection policy", async () => { | ||
| const freshHost = new URL(XAI_GROK_CLI_BASE_URL).hostname; | ||
| const home = mkdtempSync(join(tmpdir(), "ocx-fresh-connection-oauth-")); | ||
| const nativeFetch = globalThis.fetch; | ||
| const previousHosts = process.env.OCX_FRESH_CONNECTION_HOSTS; | ||
| const previousOpencodexHome = process.env.OPENCODEX_HOME; | ||
| const previousCodexHome = process.env.CODEX_HOME; | ||
| process.env.OPENCODEX_HOME = home; | ||
| process.env.CODEX_HOME = home; | ||
| process.env.OCX_FRESH_CONNECTION_HOSTS = freshHost; | ||
| const sends: Array<{ url: string; init?: RequestInit }> = []; | ||
|
|
||
| try { | ||
| await saveCredential("xai", { | ||
| access: "xai-access-token", | ||
| refresh: "xai-refresh-token", | ||
| expires: Date.now() + 3_600_000, | ||
| accountId: "xai-acct-1", | ||
| source: "local-cli", | ||
| }); | ||
| globalThis.fetch = (async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit) => { | ||
| const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url; | ||
| sends.push({ url, init }); | ||
| return Response.json({ | ||
| id: "resp_fresh_connection", | ||
| status: "completed", | ||
| output: [], | ||
| usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 }, | ||
| }); | ||
| }) as typeof globalThis.fetch; | ||
|
|
||
| const config = { | ||
| defaultProvider: "xai", | ||
| providers: { | ||
| xai: { adapter: "openai-chat", baseUrl: "https://api.x.ai/v1", authMode: "oauth" }, | ||
| }, | ||
| } as OcxConfig; | ||
| const logCtx: RequestLogContext = { model: "", provider: "" }; | ||
| const response = await handleResponses( | ||
| new Request("http://localhost/v1/responses", { | ||
| method: "POST", | ||
| headers: { "content-type": "application/json" }, | ||
| body: JSON.stringify({ model: "grok-4.6", input: "hello", stream: false }), | ||
| }), | ||
| config, | ||
| logCtx, | ||
| {}, | ||
| ); | ||
|
|
||
| expect(response.status).toBe(200); | ||
| // Asserted on the host rather than a path, and on the mapped list rather than a filtered | ||
| // one, so a destination change reports the addresses it observed instead of an empty length. | ||
| expect(sends.map(send => new URL(send.url).hostname)).toContain(freshHost); | ||
| const policed = sends.filter(send => new URL(send.url).hostname === freshHost); | ||
| for (const send of policed) { | ||
| const headers = new Headers(send.init?.headers); | ||
| expect(headers.get("Connection")).toBe("close"); | ||
| expect((send.init as { keepalive?: boolean } | undefined)?.keepalive).toBe(false); | ||
| // And the provider's own implementation still ran: only the xAI wrapper pins this header, | ||
| // so wrapping the selected fetch did not replace it with the generic executor. | ||
| expect(headers.get("x-grok-req-id")).toBeTruthy(); | ||
| } | ||
|
Comment on lines
+320
to
+331
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add a native Chat dispatch regression test. This test configures xAI OAuth credentials, so it cannot execute the native Chat key-revalidation branch that now applies the same connection policy and sets As per path instructions, “A behavior change in src/ should come with a focused regression test near the existing tests for that subsystem.” Based on learnings, credential-bearing requests must not automatically follow redirects. 🤖 Prompt for AI AgentsSources: Path instructions, Learnings |
||
| } finally { | ||
| globalThis.fetch = nativeFetch; | ||
| if (previousHosts === undefined) delete process.env.OCX_FRESH_CONNECTION_HOSTS; | ||
| else process.env.OCX_FRESH_CONNECTION_HOSTS = previousHosts; | ||
| if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; | ||
| else process.env.OPENCODEX_HOME = previousOpencodexHome; | ||
| if (previousCodexHome === undefined) delete process.env.CODEX_HOME; | ||
| else process.env.CODEX_HOME = previousCodexHome; | ||
| removeTreeWithRetry(home); | ||
| } | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When
activeProvider.fetchexists, this new branch bypasses the supplied executor and now independently enforces both the fresh-connection policy and manual redirects. The added test exercises only the Responses OAuth override, whilecredential-redirect-guard.test.tsexercises a cooperativeproviderFetchoverride, so neither test reaches this native Chat path; reverting these lines would therefore leave all current tests green. Add a focused native Chat test using a provider-scoped fetch and assertConnection: close,keepalive: false, and manual redirect handling.AGENTS.md reference: AGENTS.md:L376-L379
Useful? React with 👍 / 👎.