Skip to content

fix(cors): set Vary: Origin on the public no-credential branch - #9814

Merged
JSONbored merged 1 commit into
JSONbored:mainfrom
shin-core:fix/cors-public-vary-9712
Jul 29, 2026
Merged

fix(cors): set Vary: Origin on the public no-credential branch#9814
JSONbored merged 1 commit into
JSONbored:mainfrom
shin-core:fix/cors-public-vary-9712

Conversation

@shin-core

Copy link
Copy Markdown
Contributor

What & why

The CORS middleware in createApp() has two branches. The public no-credential branch fires when origin && isPublicNoCredentialRoute(path) and sets Access-Control-Allow-Origin: *, Access-Control-Allow-Headers/Methods/Max-Age — but no Vary. Because the branch is guarded by origin &&, the response headers differ depending on whether the request carried an Origin (with one → the ACAO/allow headers; without → none). Nothing tells a shared cache that, so a cache keyed only on URL could serve the Origin-bearing response to a later no-Origin request, or vice versa. The credentialed else branch already appends Vary: Origin; the public branch simply omitted it.

The fix

Append Vary: Origin on the public branch with { append: true } — the identical call the credentialed branch uses — so any Vary set elsewhere is preserved rather than overwritten. No change to which origins are allowed, to Access-Control-Allow-Credentials (which stays absent on this branch), or to any Cache-Control value.

Tests (test/unit/routes-cors.test.ts)

  • GET /health with an Origin returns Access-Control-Allow-Origin: * and a Vary containing Origin (fails on main).
  • Same for GET /v1/public/stats and the GET /v1/public/github/repos/:owner/:repo/stats badge route (fails on main).
  • GET /health with no Origin sets no Access-Control-Allow-Origin — pinning that the response genuinely varies, which is what makes the Vary load-bearing.
  • Regression: the credentialed branch still returns exactly one Vary: Origin (appended, not duplicated) plus Access-Control-Allow-Credentials: true.

Validation

  • npm run typecheck green; npm run ui:openapi:check clean (a response header is not part of the schema); the CORS suite (11 tests) green.
  • Diff coverage on src/api/routes.ts is 100% (the single added header call).
  • git diff --check <base> HEAD clean; diff is two files, no schema/migration change.

Closes #9712

The public CORS branch fires only when the request carried an `Origin` and sets
`Access-Control-Allow-Origin: *`, but never set `Vary: Origin`. So the response
headers differ by whether an Origin was present, yet nothing told a shared cache
that — a cache could serve the Origin-bearing response (with the ACAO header) to a
later no-Origin request, or vice versa. The credentialed branch already appends
`Vary: Origin`; the public branch just omitted it.

Append `Vary: Origin` on the public branch with `{ append: true }`, identical to
the credentialed branch, so any Vary set elsewhere is preserved. No change to
which origins are allowed, to Access-Control-Allow-Credentials (still absent on
this branch), or to any cache header.

Closes JSONbored#9712
@shin-core
shin-core requested a review from JSONbored as a code owner July 29, 2026 10:49
@loopover-orb

loopover-orb Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-29 11:02:43 UTC

2 files · 1 AI reviewer · no blockers · CI green · unstable

⏸️ Suggested Action - Manual Review

Review summary
This PR appends `Vary: Origin` with `{ append: true }` on the public no-credential CORS branch in src/api/routes.ts, mirroring the existing credentialed else-branch's behavior, closing a real cache-poisoning gap where the response headers differ based on Origin presence but nothing signaled that to shared caches. The fix is minimal, correctly scoped, and the added tests exercise both the with-Origin and without-Origin cases plus a regression check on the credentialed branch to confirm no duplication. This is a small, well-targeted, single-purpose fix tied to issue #9712.

Nits — 2 non-blocking

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9712
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ❌ 5/25 Preflight is holding this PR: the review lane is unavailable, so it is not ready for automated review.
Contributor workload ✅ 10/10 Author activity: 63 registered-repo PR(s), 46 merged, 0 issue(s).
Contributor context ✅ Confirmed Gittensor contributor shin-core; Gittensor profile; 63 PR(s), 0 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The diff adds c.header("Vary", "Origin", { append: true }) to the public no-credential branch exactly as required, and the test file adds all the specified cases: Origin-present checks for /health, /v1/public/stats, and the badge route, a no-Origin regression proving the response varies, and a regression test confirming the credentialed branch still emits exactly one Vary: Origin plus Access-Contr

Review context
  • Author: shin-core
  • Role context: outside_contributor
  • Public audience mode: oss maintainer
  • Lane context: Repository is registered but has no active allocation in the current snapshot.
  • Public profile languages: not available
  • Official Gittensor activity: 63 PR(s), 0 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Await review-lane availability.
  • Then work through the remaining 1 step in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: success
  • config: 861e0109eec8f04ba8144a96f5552cdb29bc3d34ce4fbcd13781040cf5b26658 · pack: oss-anti-slop · ci: passed
  • record: 41a823bc50abfca9f225930aa2637509f741cdb2fb139c7db2ff7528fc8554bc (schema v5, head 59b62f7)

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security superagent-security Bot added the contributor:flagged Contributor flagged for review by trust analysis. label Jul 29, 2026
@superagent-security

Copy link
Copy Markdown
Contributor

🚨 Contributor flagged. Click here for more info: Superagent Dashboard

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@loopover-orb loopover-orb Bot added the gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. label Jul 29, 2026
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 77.92%. Comparing base (fa4c6d4) to head (59b62f7).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9814      +/-   ##
==========================================
+ Coverage   77.18%   77.92%   +0.73%     
==========================================
  Files         283      284       +1     
  Lines       59729    62211    +2482     
  Branches     6696     7501     +805     
==========================================
+ Hits        46104    48477    +2373     
- Misses      13342    13363      +21     
- Partials      283      371      +88     
Flag Coverage Δ
backend 95.60% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/api/routes.ts 95.60% <100.00%> (ø)

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 29, 2026
@loopover-orb

loopover-orb Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Held for manual review: the gate and required CI are green, but GitHub reports this pull request's mergeable state as unstable because a non-required check or status is not passing, so LoopOver will not auto-merge. A maintainer can resolve the failing check or review and merge manually. This is an automated maintenance action.

@JSONbored
JSONbored merged commit 0c6b441 into JSONbored:main Jul 29, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor:flagged Contributor flagged for review by trust analysis. gittensor:bug Gittensor-scored bug fix — scores a 0.05x multiplier. manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api(cors): the public no-credential branch sets Access-Control-Allow-Origin without Vary: Origin

2 participants