Support the standard two-arg fetch(url, init) form on egress fetch handles
Status summary: Implemented and unit-suite green. Both user-callable
fetch handles accept (input, init?), docs/examples/generated contract
regenerated, and a new e2e proves method+headers+body reach the upstream on
both handles (runs on the PR's preview deploy). Nothing known missing.
itx.egress.fetch(url, init) silently DROPS the init argument — headers,
method, and body are all lost. Verified against prod today:
// headers silently dropped — httpbin saw NO custom headers:
await itx.egress.fetch("https://httpbin.org/headers", {
headers: { "User-Agent": "iterate-header-probe/1.0", "X-Probe": "hello" },
});
// headers arrive fine only via the one-arg Request form:
await itx.egress.fetch(new Request("https://httpbin.org/headers", { headers: { "X-Probe": "hello" } }));Root cause: ProjectEgressRpcTarget.fetch(request: Request) in
apps/os/src/rpc-targets.ts takes a single parameter, so the second argument
vanishes at the RPC boundary. The two-arg browser-style form is every
agent's muscle memory — a prod agent session today repeatedly told the user it
sent an Authorization header when the egress lane had dropped it. Worse, our
own config repo template (config-repo-template.generated.ts) teaches the
two-arg form (itx.egress.fetch("https://api.acme.com/v1/me", {...})), and the
egress-fetch catalogue example documents the trap ("a second fetch-style init
is ignored") instead of fixing it.
Checklist#
-
ProjectEgressRpcTarget.fetchaccepts the standard signaturefetch(input: RequestInfo | URL, init?: RequestInit), buildingnew Request(input, init)server-side — rpc-targets.ts,new Request(input, init)wrapped in the existingwithStreamContext -
SecretRpcTarget.fetch(the other user-callable fetch-shaped handle, backed by the Secret DO) gets the same treatment — same construction beforedurableObjectStub.fetch -
__describe()instruction strings updated to advertisefetch(input, init?)— both targets -
egress-fetchcatalogue example (apps/os/src/itx/examples-source.ts) rewritten to show the two-arg form; regenerateexamples.generated.ts(pnpm generate:itx-examplesor equivalent) — example now leads with the two-arg form - Regenerate the public contract
itx-api.generated.tsviapnpm generate:itx-api(do not hand-edit) — both apps/os and packages/iterate copies, plus itx-api-graph - e2e test in
apps/os/e2e/vitest/itx-egress.e2e.test.tsproving the two-arg form's method, headers, and body reach the upstream (for bothproject.egress.fetchand the secret handle'sfetch) — "two-arg fetch(url, init) carries method, headers, and body to the upstream"; secret lane also asserts placeholder substitution in init headers - Full pre-PR gauntlet green: install, typecheck, lint, knip, format, test — all green locally; e2e runs on the PR preview
Non-goals#
- Internal platform-only fetch paths (e.g.
ProjectEgressEntrypointused as workerdglobalOutbound) — workerd always hands those a realRequest. ProjectAuthRpcTarget.fetch— it deliberately remains a one-argumentRequestmethod;IterateWorkerEntrypoint.fetchProjectAuth()preserves an app request body by sending a fresh bodyless request on paths auth may decline.- The browser binding target already accepts
(input, init)— no change.
Notes#
- When
inputis already aRequestandinitis undefined,new Request(input)preserves everything in workerd (the existingwithStreamContextalready reconstructs body-bearing requests withnew Request(request, { headers }), exercised by the JSON-template e2e). - The riskiest part is Request reconstruction semantics for body-bearing requests across the RPC boundary — covered by the new e2e's POST body assertion.