fix(server): vary on Origin whenever CORS headers depend on the request origin - #1993
Conversation
…st origin `Vary: Origin` was only added when `origin` was non-wildcard, so a wildcard `origin` combined with an origin-specific `timingOrigin` produced origin-dependent responses that caches were told were constant. Both headers now go through one resolver. A static wildcard is emitted for every request without Vary. Any other configuration marks the response as varying by origin, including responses to requests without an Origin header, and origin functions only run when there is an Origin string to pass them.
More templates
@orpc/ai-sdk
@orpc/arktype
@orpc/bun
@orpc/client
@orpc/cloudflare
@orpc/contract
@orpc/experimental-effect
@orpc/evlog
@orpc/hibernation
@orpc/json-schema
@orpc/experimental-msw
@orpc/nest
@orpc/next
@orpc/node
@orpc/openapi
@orpc/opentelemetry
@orpc/pinia-colada
@orpc/pino
@orpc/publisher
@orpc/ratelimit
@orpc/server
@orpc/shared
@orpc/swr
@orpc/tanstack-query
@orpc/trpc
@orpc/valibot
@orpc/zod
commit: |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
ℹ️ Minor suggestions only.
Reviewed changes — this run reviewed the initial commit c91d0e3 against main (Base 9379b50), covering the CORS plugin rewrite and its tests.
Vary: Originlogic (packages/server/src/plugins/cors.ts): newresolveOriginhelper and emitVary: Originwheneverorigin/timingOrigincan yield different header values per request origin — function configs always vary, static non-wildcard lists vary, static wildcards andnull/empty/undefinedconfigs do not.- Function invocation contract:
origin/timingOriginfunctions are only invoked when the request carries anOriginheader and receive it asstring; the option types narrowed accordingly, while(origin: string | undefined)callbacks still compile via contravariance. - Tests (
packages/server/src/plugins/cors.test.ts): new cases for origin-specifictimingOriginbehind a wildcardorigin, no-Originrequests (function skip and static wildcard), andtimingOrigin: null; existing reflect- and vary-append/dedupe cases updated or retained.
Traced resolveOrigin across all option shapes (function/static × */list/single/null/undefined/Promise) × origin presence: the vary decision is exact, both the originally-broken case (wildcard allow-origin + origin-specific timing-allow-origin) and the no-Origin variants of static lists now carry Vary: Origin, and static wildcards correctly stay vary-free. Tests are meaningful — e.g. the function-skip assertions and the wildcard+timing vary case fail against the old code. Ran packages/server/src/plugins (210 passed, incl. 21 cors), tests/plugins/all-plugins.test.ts (3 passed), eslint, and tsc -b packages/server — all green.
ℹ️ Nitpicks
- The behavior changes —
origin/timingOriginfunctions only run for requests that carry anOriginheader, and non-wildcard static lists now mark responses as varying even without one — are well documented in the PR description, butapps/content/docs/plugins/cors.mdx("Dynamic Origin") still describes the function contract without this nuance. One sentence noting that the function receives the request origin only when the request carries anOriginheader would keep the docs aligned with the new runtime contract.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

Responses with a wildcard
originand an origin-specifictimingOriginnow carryVary: Origin. The header was previously tied only to theoriginoption, soTiming-Allow-Origincould change per origin while caches were told the response was constant, and a shared cache could serve one origin's variant to another. Browsers validate that header against the requesting page, so nothing leaked, but allowed origins could silently lose their timing data.Fixes
Vary: Originis emitted wheneveroriginortimingOriginis anything other than a static wildcard, on every variant including responses to requests without anOriginheader, so caches keep per-origin responses apart.*, so a cached wildcard answer can no longer be served to an origin the function would have denied.nullor emptyorigin/timingOriginno longer addsVary: Origin, since that response never depends on the request.Behavior changes
originandtimingOriginfunctions only run for requests that carry anOriginheader and receive it asstringinstead ofstring | undefined. Callbacks typed withstring | undefinedkeep compiling.Originheader, sotimingOrigin: '*'keeps working for subresources that never send one.Testing
pnpm vitest run packages/server/src/plugins/cors.test.ts: 21 passed, with new cases for a static timing list behind a wildcard CORS origin, functions skipped without anOriginheader, and static wildcards without one.pnpm --filter @orpc/server type:checkand eslint pass.