Skip to content

fix(server): vary on Origin whenever CORS headers depend on the request origin - #1993

Merged
dinwwwh merged 1 commit into
middleapi:mainfrom
dinwwwh:claude/vary-origin-wildcard-cors-b0deec
Sep 7, 2026
Merged

fix(server): vary on Origin whenever CORS headers depend on the request origin#1993
dinwwwh merged 1 commit into
middleapi:mainfrom
dinwwwh:claude/vary-origin-wildcard-cors-b0deec

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 7, 2026

Copy link
Copy Markdown
Member

Responses with a wildcard origin and an origin-specific timingOrigin now carry Vary: Origin. The header was previously tied only to the origin option, so Timing-Allow-Origin could 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: Origin is emitted whenever origin or timingOrigin is anything other than a static wildcard, on every variant including responses to requests without an Origin header, so caches keep per-origin responses apart.
  • Function configs always vary, even when they return *, so a cached wildcard answer can no longer be served to an origin the function would have denied.
  • A static null or empty origin / timingOrigin no longer adds Vary: Origin, since that response never depends on the request.

Behavior changes

  • origin and timingOrigin functions only run for requests that carry an Origin header and receive it as string instead of string | undefined. Callbacks typed with string | undefined keep compiling.
  • Static wildcards are still sent to requests without an Origin header, so timingOrigin: '*' 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 an Origin header, and static wildcards without one.
  • pnpm --filter @orpc/server type:check and eslint pass.

…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.
@pkg-pr-new

pkg-pr-new Bot commented Sep 7, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1993

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1993

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@1993

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1993

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@1993

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1993

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@1993

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@1993

@orpc/hibernation

npm i https://pkg.pr.new/@orpc/hibernation@1993

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1993

@orpc/experimental-msw

npm i https://pkg.pr.new/@orpc/experimental-msw@1993

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1993

@orpc/next

npm i https://pkg.pr.new/@orpc/next@1993

@orpc/node

npm i https://pkg.pr.new/@orpc/node@1993

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1993

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@1993

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@1993

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@1993

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@1993

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@1993

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1993

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1993

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@1993

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1993

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1993

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1993

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1993

commit: c91d0e3

@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 30 untouched benchmarks


Comparing dinwwwh:claude/vary-origin-wildcard-cors-b0deec (c91d0e3) with main (9379b50)

Open in CodSpeed

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

ℹ️ 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: Origin logic (packages/server/src/plugins/cors.ts): new resolveOrigin helper and emit Vary: Origin whenever origin/timingOrigin can yield different header values per request origin — function configs always vary, static non-wildcard lists vary, static wildcards and null/empty/undefined configs do not.
  • Function invocation contract: origin/timingOrigin functions are only invoked when the request carries an Origin header and receive it as string; 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-specific timingOrigin behind a wildcard origin, no-Origin requests (function skip and static wildcard), and timingOrigin: 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/timingOrigin functions only run for requests that carry an Origin header, and non-wildcard static lists now mark responses as varying even without one — are well documented in the PR description, but apps/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 an Origin header would keep the docs aligned with the new runtime contract.

Pullfrog  | Fix it ➔View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit d637f30 into middleapi:main Sep 7, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant