Skip to content

fix(cors): correct vary handling and credentialed wildcard behavior#1456

Merged
pi0 merged 2 commits into
mainfrom
fix/cors
Jul 11, 2026
Merged

fix(cors): correct vary handling and credentialed wildcard behavior#1456
pi0 merged 2 commits into
mainfrom
fix/cors

Conversation

@pi0x

@pi0x pi0x commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes several spec-level correctness issues in the CORS utils found during a review of src/utils/cors.ts / src/utils/internal/cors.ts.

vary: origin was missing when the origin is rejected (cache poisoning)

When origin is an allowlist/function, an allowed origin got access-control-allow-origin + vary: origin, but a rejected origin got neither. The response varies by request origin without declaring it, so a shared cache could store the ACAO-less response from a disallowed origin and serve it to an allowed one (or cache the reflected header and serve it to everyone). createOriginHeaders now always emits vary: origin when the origin option is not "*".

Wildcards are treated literally on credentialed requests

Per the Fetch spec, * in access-control-allow-methods / access-control-expose-headers is a literal token when credentials are included:

  • methods: "*" + credentials: true now reflects access-control-request-method back (with vary: access-control-request-method) instead of sending a literal * that browsers would reject — same technique already used for allowHeaders.
  • exposeHeaders: "*" + credentials: true now omits the useless header and logs a warning suggesting an explicit list.

vary: access-control-request-headers was missing when the header is absent

With wildcard allowHeaders, the preflight response reflects access-control-request-headers, but no vary was declared when the request omitted that header — same undeclared-variance class as the origin issue.

origin: "null" was inconsistent with isCorsOriginAllowed

createOriginHeaders unconditionally emitted access-control-allow-origin: null for any request origin, while isCorsOriginAllowed only matches a literal "null" origin. Both now agree: null is only reflected when the request origin is actually null.

The vary merge in appendCorsPreflightHeaders was generalized since three header groups can now emit vary independently.

Testing

  • Updated unit tests for the new vary behavior and added regression tests for each fix (test/unit/cors.test.ts, 44 tests)
  • Full suite passes: 1345 passed, no type errors

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved credentialed CORS preflight handling for methods: "*" by reflecting the requested method when available.
    • Suppressed access-control-expose-headers for exposeHeaders: "*" when credentials are enabled.
    • Corrected Vary header behavior for allowed and rejected origins, and for preflight responses (including combined variation across relevant dimensions).
    • Deduplicated and clarified warnings for ineffective credentialed wildcard configurations.
  • Tests

    • Expanded and reorganized CORS unit coverage for wildcard + credentials scenarios, origin edge cases, and updated expected Vary/header outputs.

- Emit `vary: origin` even when the request origin is rejected, so shared
  caches never serve a response cached for one origin to another
- Always emit `vary: access-control-request-headers` when reflecting
  request headers with wildcard `allowHeaders`
- Reflect the requested method (with `vary: access-control-request-method`)
  when `methods: "*"` is combined with `credentials: true`, since browsers
  treat a literal `*` as a method name on credentialed requests
- Omit `access-control-expose-headers: *` and warn when combined with
  `credentials: true` (browsers treat it literally)
- Handle `origin: "null"` consistently with `isCorsOriginAllowed` (only
  reflect `null` when the request origin is actually `null`)

Co-Authored-By: Claude Fable 5 <[email protected]>
@pi0x
pi0x requested a review from pi0 as a code owner July 11, 2026 13:15
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9960abb-99c6-48e5-94db-d8474ee19511

📥 Commits

Reviewing files that changed from the base of the PR and between 2b5a7ce and 1482e17.

📒 Files selected for processing (2)
  • src/utils/internal/cors.ts
  • test/unit/cors.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/utils/internal/cors.ts
  • test/unit/cors.test.ts

📝 Walkthrough

Walkthrough

Changes

CORS behavior updates

Layer / File(s) Summary
Credentialed header semantics
src/utils/cors.ts, src/utils/internal/cors.ts, test/unit/cors.test.ts
Credentialed wildcard methods reflect the requested method, wildcard expose headers are omitted, warnings are deduplicated, rejected origins emit Vary: origin, and missing request headers emit their corresponding Vary value.
Preflight header assembly
src/utils/cors.ts, test/unit/cors.test.ts
Preflight headers are assembled from ordered groups, with Vary collected across all groups and event-aware method generation tested.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • h3js/h3#1323: Related CORS warnings for credentialed wildcard configurations.
  • h3js/h3#1352: Related changes to CORS header appending.
  • h3js/h3#1396: Related Vary composition changes in preflight handling.

Suggested reviewers: pi0

Poem

A rabbit hops through headers bright,
Reflecting methods just right.
Origins vary, wildcards bow,
Preflight groups merge now.
CORS carrots bloom tonight! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main CORS fixes around Vary handling and credentialed wildcard behavior.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 fix/cors

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
test/unit/cors.test.ts (1)

667-670: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the complete merged Vary value.

Default wildcard allowHeaders also contributes access-control-request-headers. The current partial checks would pass if that group were dropped, weakening coverage of the PR’s generalized merge.

Proposed assertion
-        const vary = eventMock.res.headers.get("vary") ?? "";
-        expect(vary).toContain("origin");
-        expect(vary).toContain("access-control-request-method");
+        expect(eventMock.res.headers.get("vary")).toEqual(
+          "origin, access-control-request-method, access-control-request-headers",
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/cors.test.ts` around lines 667 - 670, Update the Vary-header
assertions in the CORS test to validate the complete merged value, including
access-control-request-headers alongside origin and
access-control-request-method. Assert the expected combined header value rather
than checking only individual substrings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/cors.ts`:
- Around line 35-37: Update the credentials guidance near the `methods`
documentation in `cors.ts` to clarify that `methods: "*"` is supported by
reflecting the requested method when credentials are enabled, rather than
advising credentialed callers never to use it. Keep the existing wildcard
behavior and surrounding documentation unchanged.

---

Nitpick comments:
In `@test/unit/cors.test.ts`:
- Around line 667-670: Update the Vary-header assertions in the CORS test to
validate the complete merged value, including access-control-request-headers
alongside origin and access-control-request-method. Assert the expected combined
header value rather than checking only individual substrings.
🪄 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: CHILL

Plan: Pro

Run ID: 84fdd227-0bd6-47f8-b0cb-b90968e54a96

📥 Commits

Reviewing files that changed from the base of the PR and between 3813e9b and a8775d2.

📒 Files selected for processing (3)
  • src/utils/cors.ts
  • src/utils/internal/cors.ts
  • test/unit/cors.test.ts

Comment thread src/utils/cors.ts
Comment on lines +35 to +37
* When `credentials` is enabled, browsers treat `"*"` as a literal method name — in that
* case the requested method is reflected back instead of sending a literal `*`.
*

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Reconcile the wildcard-method documentation.

Lines 35-37 say methods: "*" is supported through reflection, while Line 65 says credentialed callers should not set methods to "*". Update the credentials documentation to distinguish this supported behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/cors.ts` around lines 35 - 37, Update the credentials guidance near
the `methods` documentation in `cors.ts` to clarify that `methods: "*"` is
supported by reflecting the requested method when credentials are enabled,
rather than advising credentialed callers never to use it. Keep the existing
wildcard behavior and surrounding documentation unchanged.

resolveCorsOptions runs on every request via handleCors, so the
credential warnings previously re-logged per request. Route them through
a warn-once helper and check the resolved options so a defaulted wildcard
exposeHeaders under credentials is diagnosed consistently with origin.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pi0
pi0 merged commit 303fd86 into main Jul 11, 2026
10 checks passed
@pi0
pi0 deleted the fix/cors branch July 11, 2026 17:28
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.

2 participants