Skip to content

fix(middleware): Cors no longer emits raw comma list as Access-Control-Allow-Origin#2725

Merged
bpamiri merged 2 commits into
developfrom
fix/bot-2704-wheels-middleware-cors-emits-raw-comma-list-as-acc
May 16, 2026
Merged

fix(middleware): Cors no longer emits raw comma list as Access-Control-Allow-Origin#2725
bpamiri merged 2 commits into
developfrom
fix/bot-2704-wheels-middleware-cors-emits-raw-comma-list-as-acc

Conversation

@wheels-bot
Copy link
Copy Markdown
Contributor

@wheels-bot wheels-bot Bot commented May 15, 2026

Summary

wheels.middleware.Cors was emitting the raw comma-delimited allowOrigins list as the Access-Control-Allow-Origin header value whenever a request arrived without an Origin header (same-origin pages, server-to-server calls, curl without -H Origin: ...). This violates the CORS spec — Access-Control-Allow-Origin must be a single origin or *, never a list — and can poison CDN/proxy caches that key on response headers.

Origin resolution is now extracted into a pure $resolveAllowOrigin() method that returns "*" for wildcard configs, the reflected origin when the request Origin is in the allowlist, or "" otherwise. The header is only emitted when there is a value to emit. For specific-origin and comma-list configs, same-origin and S2S responses no longer carry the header at all. Wildcard configs (allowOrigins = "*") continue to emit Access-Control-Allow-Origin: * on every response — unchanged from prior behavior, which is the spec-allowed shape for wildcard.

Related Issue

Closes #2704

Reviewer findings — disposition

Type of Change

  • Bug fix
  • New feature
  • Enhancement to existing feature
  • Documentation update
  • Refactoring

Feature Completeness Checklist

  • DCO sign-off -- Every commit carries Signed-off-by: (use git commit -s); see CONTRIBUTING.md
  • Tests -- New Access-Control-Allow-Origin header resolution describe block in vendor/wheels/tests/specs/middleware/CorsSpec.cfc covering comma-list / single-origin / wildcard / matched / non-matched / empty allowlist cases against the absent-Origin path. Failing-then-passing TDD: pre-fix 6 errors ($resolveAllowOrigin not defined); post-fix CorsSpec passes 16/16 with no other middleware spec regressions.
  • Framework Docs -- Handled separately by bot-update-docs.yml
  • AI Reference Docs -- Handled separately by bot-update-docs.yml
  • CLAUDE.md -- Handled separately by bot-update-docs.yml
  • CHANGELOG.md -- Entry added under [Unreleased] / Fixed
  • Test runner passes -- vendor/wheels/tests/specs/middleware/ reports 150 pass / 0 fail / 0 error / 0 skip post-fix (CorsSpec 16/16, MiddlewareOrderResolverSpec 16/16, MiddlewarePipelineSpec 12/12, MultiTenantIntegrationSpec 9/9, PluginMiddlewarePipelineSpec 7/7, RateLimiterSpec 56/56, SecurityHeadersSpec 24/24, TenantResolverSpec 10/10) via curl http://localhost:60007/wheels/core/tests?db=sqlite&format=json&directory=wheels.tests.specs.middleware.

Test Plan

  1. Configure the middleware with a comma-delimited allowlist:
    set(middleware = [
        new wheels.middleware.Cors(allowOrigins="https://portal.pai.com,https://portal.paiindustries.com")
    ]);
  2. Send a request with no Origin header (curl -i http://localhost:.../api/v1/jvm).
  3. Confirm the response no longer includes Access-Control-Allow-Origin: https://portal.pai.com,https://portal.paiindustries.com — the header is omitted entirely.
  4. Send a request from one of the allowed origins (curl -i -H "Origin: https://portal.pai.com" ...) and confirm Access-Control-Allow-Origin: https://portal.pai.com is reflected back.
  5. Send a request from a disallowed origin and confirm no Access-Control-Allow-Origin header is emitted.
  6. Run the middleware spec suite: curl "http://localhost:60007/wheels/core/tests?db=sqlite&format=json&directory=wheels.tests.specs.middleware" — all 150 specs pass.

Screenshots / Output

n/a

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Docs updated

Added a doc commit to this PR:

  • web/sites/guides/src/content/docs/v4-0-1-snapshot/digging-deeper/cors.mdx — added "No Origin header, no CORS header" pitfall entry explaining that same-origin, server-to-server, and bare curl requests never receive Access-Control-Allow-Origin, and how to test the allowlist correctly
  • web/sites/guides/src/content/docs/v4-0-0/digging-deeper/cors.mdx — same addition for the v4-0-0 snapshot

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

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

Wheels Bot — Reviewer A

TL;DR: This PR correctly fixes a real CORS spec violation — a multi-origin allowOrigins comma list was flowing through as the raw Access-Control-Allow-Origin value on requests without an Origin header. The extraction into $resolveAllowOrigin() is clean and the new BDD specs cover the key edge cases. No cross-engine, security, or commitlint issues. Two non-blocking findings: a missing Vary: Origin header (pre-existing but this PR is the right place to fix it) and a small gap in integration-level test coverage for the regression scenario. Verdict: comment — no blocking issues, but the Vary concern is worth tracking.


Correctness

Finding 1: Missing Vary: Origin header — pre-existing, but worth fixing here

File: vendor/wheels/middleware/Cors.cfc, lines 77-87

if (Len(local.allowOrigin)) {
    try {
        cfheader(name = "Access-Control-Allow-Origin", value = local.allowOrigin);
        cfheader(name = "Access-Control-Allow-Methods", value = variables.allowMethods);
        ...
    } catch (any e) {
    }
}

When allowOrigin is a specific reflected origin (i.e., not "*"), the CORS spec (Fetch §3.2.3) requires the response to carry Vary: Origin. Without it, a shared CDN/reverse-proxy can cache the response with Access-Control-Allow-Origin: https://portal.pai.com and serve it to a subsequent request from https://portal.paiindustries.com, which the browser will then block. This bug predates this PR, but since the PR already touches the header-emission block and the PR body explicitly mentions "can poison CDN/proxy caches", this is a natural spot to add it.

Suggested addition inside the same try block, after the Access-Control-Allow-Origin line, conditional on non-wildcard:

if (local.allowOrigin != "*") {
    cfheader(name = "Vary", value = "Origin");
}

Finding 2: PR body is slightly misleading about wildcard behavior

The summary states "same-origin and S2S responses no longer carry it at all." This is true when allowOrigins is a specific origin or comma-list. However, when allowOrigins = "*", $resolveAllowOrigin("") returns "*" (line 54-56), so requests without an Origin header still receive Access-Control-Allow-Origin: *. This matches the old behavior (the original code defaulted local.allowOrigin = variables.allowOrigins which was already "*"), so it is not a regression, but the description overpromises. The code is fine — only the PR body needs a qualifier.


Tests

Finding 3: No integration test for the primary regression scenario

The new specs (lines 85-126 in CorsSpec.cfc) call $resolveAllowOrigin() directly — good for unit coverage — but there is no end-to-end test that drives the regression through handle():

  • No Origin header + comma-list allowOriginshandle() does NOT set the Access-Control-Allow-Origin response header.

The existing integration-style tests (lines 10-83) verify the handle() return value but do not inspect headers. Since cfheader calls in test mode do not write real HTTP headers, a mock-based approach (overriding $setHeader or capturing headers via a test-helper wrapper) or at minimum a comment explaining why unit coverage of $resolveAllowOrigin is sufficient would close this gap. Without it, a future refactor that breaks the wiring between handle() and $resolveAllowOrigin() might not be caught by this test suite.


Commits

Both commits conform to commitlint rules:

SHA Subject Type Scope DCO
48a4030 fix(middleware): Cors no longer emits raw comma list as Access-Control-Allow-Origin fix middleware present
4f80380 docs(web/guides): note that requests without Origin header omit Access-Control-Allow-Origin docs web/guides present

No issues.


Docs

Both doc versions correctly updated:

  • web/sites/guides/src/content/docs/v4-0-0/digging-deeper/cors.mdx
  • web/sites/guides/src/content/docs/v4-0-1-snapshot/digging-deeper/cors.mdx

The added bullet point under the troubleshooting checklist is accurate and helpful. No issues.


Summary

The core change is correct and well-scoped. The two actionable items:

  1. Add Vary: Origin when emitting a specific (non-wildcard) origin — prevents cache poisoning, which the PR body itself calls out as a concern.
  2. Consider an integration-level test through handle() for the comma-list + no-Origin regression path to guard the wiring, not just the logic.

Neither is blocking for a bug-fix PR, but (1) in particular would be a cheap addition that completes the correctness story the PR body promises.

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Reviewer B (round 1)

A's review is solid on all three findings. No sycophancy, no false positives, no missed issues on the diff I re-read. Verdict alignment holds.

Sycophancy

None detected. A's "comment" verdict is earned, not given — it names specific findings and explicitly marks neither as blocking.

False positives

None detected.

  • Finding 1 (Vary: Origin): Verified against vendor/wheels/middleware/Cors.cfc lines 77-87. When local.allowOrigin is a specific reflected origin (not "*"), no Vary: Origin header is emitted. The CORS spec and HTTP caching guidance (Fetch §3.2.3) both require it to prevent a CDN from serving a cached response to a different origin. A's description is accurate and the suggested addition is correct.

  • Finding 2 (wildcard behavior): Verified against $resolveAllowOrigin() lines 53-61. When variables.allowOrigins == "*", the function returns "*" unconditionally, regardless of whether requestOrigin is empty. Requests with no Origin header therefore still receive Access-Control-Allow-Origin: * on a wildcard config. This contradicts the PR body's "same-origin and S2S responses no longer carry it at all" — that statement is only true for specific-origin configurations. A's reading is correct; only the description needs a qualifier.

  • Finding 3 (no handle() integration test for regression path): Verified against CorsSpec.cfc lines 10-83. All existing handle()-level tests assert on the return value (passthrough or empty string for OPTIONS), not on emitted headers. None drive the specific regression scenario — comma-list allowOrigins + no Origin header — through handle(). A correctly notes the constraint: cfheader() in test mode does not write real HTTP headers, which limits what is directly assertable.

Missed issues

None detected. Cross-engine compatibility is clean — ListFindNoCase, StructKeyExists, and cfheader are all standard CFML with no engine-specific gotchas. The public access modifier with $ prefix on $resolveAllowOrigin is the correct Wheels internal-method convention (CLAUDE.md, "private mixin functions not integrated"). Both commits conform to commitlint. No .ai/ reference-doc updates are needed for this class of targeted bug fix.

Verdict alignment

"Comment" is consistent with two non-blocking findings. A does not approve over gaps — both are named and present — but correctly judges that neither warrants blocking a well-scoped bug fix.

Convergence

Aligned. A's findings are accurate and appropriately weighted. The Vary: Origin gap and the missing regression path through handle() are both real but pre-existing or infrastructure-constrained; neither is a reason to block this fix. Joint recommendation: approve the PR as-is, with A's suggestions tracked as follow-up work.

@bpamiri bpamiri marked this pull request as ready for review May 15, 2026 19:46
@bpamiri bpamiri closed this May 15, 2026
@bpamiri bpamiri reopened this May 15, 2026
@bpamiri bpamiri force-pushed the fix/bot-2704-wheels-middleware-cors-emits-raw-comma-list-as-acc branch from 2c74384 to 5e788e8 Compare May 15, 2026 23:13
Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

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

Wheels Bot — Reviewer A

TL;DR: This is an incremental update to a CORS bug-fix PR whose substantive changes (the $resolveAllowOrigin() extraction and BDD specs) were already reviewed in the prior Reviewer A pass. The only material change since that review is a CI-automated visual baseline refresh commit (5e788e80d), unrelated to the CORS fix itself. The previous substantive findings were properly addressed: the PR body now clarifies wildcard behavior, and the Vary: Origin gap is correctly deferred to PR #2724. No correctness, cross-engine, or security issues in either the original change or the new commit. Verdict: comment — one minor housekeeping note on the new commit.


Commits

Finding: chore(web): refresh visual baseline(s) (blog) — no DCO Signed-off-by: trailer

The commit 5e788e80d was authored by github-actions[bot] via .github/workflows/refresh-visual-baselines.yml and has no Signed-off-by: trailer:

commit 5e788e80d21f438f627663d7dc7ec8d305aa50c5
Author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
chore(web): refresh visual baseline(s) (blog)

The DCO App exempts bots if the repo's .github/dco.yml includes allowRemediationCommits: true or an explicit bot allowlist. If the DCO check goes red on this commit, the fix is to configure the DCO App to exempt github-actions[bot], not to push a manual Signed-off-by: from a human.

Separate concern — unrelated commit in a bug-fix PR: a visual baseline update for the blog page has no logical connection to a CORS header emission fix. It should land on develop independently or be targeted at a visual-regression sweep PR. This is a housekeeping nit, not a merge blocker, but it muddies git log --oneline for the CORS topic.

The other two commits (48a403058, 4f8038014) conform to commitlint and carry DCO sign-offs — no issues there.


Prior findings — resolved

Finding Disposition
A1: Missing Vary: Origin Tracked in #2707, addressed in #2724. Acceptable deferral for a focused bug-fix PR.
A2: PR body overpromised on wildcard behavior Addressed in the updated Summary section. Code unchanged.
A3: No handle()-level integration test for regression path Acknowledged: cfheader() in test mode does not write real HTTP headers; $resolveAllowOrigin() unit coverage is the assertable surface. Reviewer B concurred. Accepted.

Everything else — clean

  • Correctness: $resolveAllowOrigin() correctly returns "" for a comma-list config with no Origin header, "*" for wildcard, and the reflected origin when matched. The wiring in handle() (line 75) and the guard at line 77 (if (Len(local.allowOrigin))) are correct.
  • Cross-engine: No struct.map() collisions, no application scope function members, no bracket-notation calls, no client scope, no Left(str, 0). All code paths use vanilla CFML.
  • Mixin access: $resolveAllowOrigin() is declared public with $ prefix — correct for a standalone instantiated component where tests need to call it directly. Not a mixin CFC, so the private-integration rule (CLAUDE.md §10) does not apply.
  • Tests: BDD syntax throughout (describe/it/expect). Six new assertions in the Access-Control-Allow-Origin header resolution block cover all meaningful combinations of the absent-Origin path.
  • Docs: Both v4-0-0 and v4-0-1-snapshot CORS guides updated with the accurate troubleshooting bullet.
  • Security: The fix tightens security — stops emitting a spec-violating multi-origin header value that could have been cached and replayed incorrectly by proxies.

@bpamiri
Copy link
Copy Markdown
Collaborator

bpamiri commented May 15, 2026

Reviewer A round 2 — disposition

Thanks for the re-review on 5e788e80d. The three prior findings are all resolved/accepted as A noted. On the new housekeeping note:

  • Visual-baseline bot commit lacks DCO Signed-off-by: — Acknowledged. The proper remediation is at the DCO App configuration level (allowlist github-actions[bot]), not a human-pushed Signed-off-by:, per A's own guidance. There is no .github/dco.yml in the repo today and no DCO check is breaking CI on this PR, so this is not blocking. Filing as a follow-up for the visual-baseline-refresh workflow / DCO config rather than touching this PR.
  • Visual-baseline commit unrelated to CORS topic — The commit was produced by .github/workflows/refresh-visual-baselines.yml against this branch. Removing it would re-break Visual regression (which is currently green on this branch). Leaving it in place for now; the housekeeping concern is real but out of scope for a bug-fix PR.

CI is now fully green on 5e788e80d after the commit-body wrap fix.

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot -- Reviewer B (round 1)

A's review is accurate for the new HEAD SHA. The two findings (DCO trailer on the github-actions[bot] commit, unrelated blog baseline in a CORS PR) are real, appropriately weighted as non-blocking, and consistent with the 'comment' verdict. No sycophancy, no false positives. One contextual note that softens A's DCO finding, but not a false positive. Aligning on approve.

Sycophancy

None detected. A's 'comment' verdict is earned: A identifies two concrete issues and does not rubber-stamp the PR despite the prior substantive review having already converged-approve.

False positives

None detected.

  • DCO finding: Verified -- 5e788e80d was authored by github-actions[bot] and carries no Signed-off-by: trailer. A's claim is accurate. Contextual note (not a false positive, but relevant): the DCO check does not appear anywhere in the PR's current status checks list -- no 'DCO' entry, pass or fail. Looking at recent merged commits (8c35d8dc0, 46de7183fd1f) that also have empty Signed-off-by: trailers yet landed on develop, the DCO app either exempts automated commits by default on this installation or is not configured as a blocking required check. A hedges correctly ('if the DCO check goes red') rather than asserting it is failing. The concern is valid per CLAUDE.md policy; in practice it may not block this PR.

  • Unrelated commit finding: Verified -- 5e788e80d updates web/tests/visual-baselines/blog.png with no logical connection to the CORS header emission fix. A's characterization is accurate. Separately, the visual regression check now passes because of this update, so it is keeping CI green -- but that does not change its status as an unrelated commit in a focused bug-fix PR.

Missed issues

None detected. A's cross-engine check is thorough: ListFindNoCase, Len, StructKeyExists, and cfheader are vanilla CFML with no engine-specific gotchas. The public + $ prefix on $resolveAllowOrigin() is correct for a non-mixin standalone component where unit tests call it directly -- the CLAUDE.md anti-pattern about private mixin functions not being integrated does not apply here. Prior findings (A1 Vary: Origin, A2 wildcard PR body language, A3 no handle()-level regression test) are all correctly marked resolved. Commits 48a403058 and 4f8038014 carry DCO trailers from claude[bot] and conform to commitlint -- no issues.

Verdict alignment

'Comment' is consistent with the findings. Neither the DCO concern (conditional, repo-config resolution) nor the unrelated commit (nit) warrants 'request-changes' on an otherwise correct and well-tested fix.

Convergence

Aligned. A's two findings are accurate and appropriately non-blocking. The core CORS fix is correct, cross-engine clean, covered by six targeted unit tests, and documented in both snapshot guides. The DCO concern is a repo-configuration matter rather than a PR-code matter; maintainers can address it via .github/dco.yml independently of this PR. Joint recommendation: approve.

@bpamiri bpamiri force-pushed the fix/bot-2704-wheels-middleware-cors-emits-raw-comma-list-as-acc branch from 5e788e8 to e14f59d Compare May 15, 2026 23:39
Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

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

Wheels Bot — Reviewer A

TL;DR: This is the third Reviewer A pass on PR #2725, which fixes a CORS spec violation in wheels.middleware.Cors. The only change since the second pass is removal of the unrelated chore(web): refresh visual baseline(s) commit (5e788e80d) that was flagged as out-of-scope. The branch now contains exactly two commits — the fix(middleware) implementation and the docs(web/guides) troubleshooting note — both reviewed and found clean in prior passes. All findings from both previous reviews have been addressed or explicitly deferred. No correctness, cross-engine, or security issues remain. Verdict: approve (self-approval blocked by GitHub; human maintainer approval unblocked).


Prior findings — final disposition

Finding Pass Status
A1: Missing Vary: Origin header A/B round 1 Deferred to #2724. Acceptable for a focused bug-fix PR.
A2: PR body overpromised on wildcard behavior A round 1 Addressed — wildcard qualifier added to Summary.
A3: No handle()-level integration test for regression path A round 1 Accepted — cfheader() does not write real HTTP headers in test mode; $resolveAllowOrigin() unit coverage is the assertable surface.
B1: Unrelated visual-baseline commit with no DCO sign-off A round 2 Resolved — 5e788e80d (chore(web): refresh visual baseline(s) (blog)) removed from the branch.

Current state — clean

Correctness: $resolveAllowOrigin() returns "" for a comma-list config with no Origin header, "*" for wildcard, and the reflected origin when matched. The wiring in handle() (line 75) and the guard at line 77 (if (Len(local.allowOrigin))) are correct.

Cross-engine: All code uses vanilla CFML (Len(), ListFindNoCase(), StructKeyExists()). No struct.map() collisions, no application scope members, no client scope, no bracket-notation calls, no Left(str, 0). $resolveAllowOrigin() is declared public with $ prefix — correct access level for a standalone instantiated component (the private-integration rule in CLAUDE.md applies to mixin CFCs injected via $integrateComponents(), not to middleware components).

Tests: Six new BDD specs under describe("Access-Control-Allow-Origin header resolution") cover all meaningful combinations of the absent-Origin path. Syntax is correct (describe/it/expect, extending wheels.WheelsTest).

Commits: Both commits conform to commitlint, carry DCO Signed-off-by: trailers, and no unrelated commits remain on the branch.

Docs: Both v4-0-0 and v4-0-1-snapshot CORS guides updated with the accurate troubleshooting bullet.

Security: The fix tightens security — stops emitting a spec-violating multi-origin header value that could be cached and incorrectly replayed by proxies.

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 15, 2026

Wheels Bot — Reviewer B (round 1)

A's third-pass review is accurate and appropriately scoped. All prior findings are correctly marked resolved or deferred. No sycophancy, no false positives, no missed issues on the diff. Aligning on approve.

Sycophancy

None detected. A's approve verdict is earned: every prior finding is dispositioned with evidence, not waved away.

False positives

None detected. I verified the key claims against the diff:

  • $resolveAllowOrigin() return values: wildcard returns "*", matched origin returns the reflected origin, everything else (including absent Origin with a comma-list config) returns "". Correct.
  • Guard at if (Len(local.allowOrigin)) prevents header emission when the resolver returns empty string. Correct.
  • public access with $ prefix is the right pattern for a standalone middleware component; the CLAUDE.md anti-pattern about private functions not being integrated applies to mixin CFCs only, not instantiated middleware objects. A's note on this is accurate.
  • Six BDD specs in the diff cover comma-list/no-Origin, single-origin/no-Origin, matched origin, non-matched origin, wildcard (with and without Origin), and empty allowlist. All six are present and syntactically correct.
  • Both guide files (v4-0-0 and v4-0-1-snapshot) updated identically with the troubleshooting bullet. Correct.

Missed issues

None detected. Cross-engine compat is clean: Len(), ListFindNoCase(), StructKeyExists(), and cfheader are vanilla CFML with no engine-specific gotchas. Vary: Origin is correctly tracked as deferred to issue #2724 and previously accepted by both A and B. No .ai/ reference doc updates are warranted for a spec-compliance bug fix that restores documented behavior.

Verdict alignment

"Approve" (expressed under a COMMENTED review due to GitHub's bot self-approval block) is consistent with the findings. All prior blockers are resolved; remaining gaps are infrastructure-constrained or explicitly deferred with tracking issues.

Convergence

Aligned, no changes needed. The fix is correct, cross-engine clean, covered by six targeted unit tests, and documented in both snapshot guides. Joint recommendation: approve and merge.

…l-Allow-Origin

When `allowOrigins` was configured with a comma-delimited list and the
request arrived with no `Origin` header (same-origin, server-to-server,
curl-without-`-H`), the default `local.allowOrigin = variables.allowOrigins`
flowed the raw list through to the response header, violating the CORS spec
requirement that `Access-Control-Allow-Origin` be a single origin or `*`.

Extract origin resolution into a pure `$resolveAllowOrigin()` method that
returns `"*"` for wildcard configs, the reflected origin when the request
`Origin` is in the allowlist, or `""` otherwise. The header is now only
emitted when there is a value to emit — same-origin and S2S responses
no longer carry it at all.

Fixes #2704

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
@bpamiri bpamiri force-pushed the fix/bot-2704-wheels-middleware-cors-emits-raw-comma-list-as-acc branch from e14f59d to 40d5502 Compare May 16, 2026 00:37
Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

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

Wheels Bot — Reviewer A

TL;DR: This is the fourth Reviewer A pass on PR #2725. The branch was force-pushed to rebase onto develop after PR #2724 (Vary: Origin fix) merged — the only reason the head SHA changed from the third-pass SHA (e14f59dbd). The two remaining commits are byte-identical in diff to those reviewed and passed in the third pass. All prior findings are closed. No correctness, cross-engine, or security issues. Verdict: approve (GitHub blocks self-approval by the bot; this unblocks a human maintainer merge).


Prior findings — final disposition

Finding Origin Status
A1: Missing Vary: Origin header Pass 1 Resolved — #2724 merged into develop; $headersFor() now emits Vary: Origin when local.reflected is true.
A2: PR body overpromised on wildcard behavior Pass 1 Resolved — Summary qualifier added in pass 2.
A3: No handle()-level integration test for regression path Pass 1 Accepted — cfheader() does not write real HTTP headers in test mode; $resolveAllowOrigin() unit coverage is the assertable surface. Reviewer B concurred.
B1: Unrelated visual-baseline commit with no DCO sign-off Pass 2 Resolved — commit 5e788e80d removed; branch now has exactly two commits.

Current state — clean

Correctness: $resolveAllowOrigin() correctly handles all three cases:

  • allowOrigins == "*" — returns "*" (wildcard, no Origin needed)
  • non-empty requestOrigin in the comma-list — returns the reflected origin
  • anything else (no Origin header, disallowed origin, empty allowlist) — returns ""

The guard if (Len(local.allowOrigin)) in $headersFor() ensures the header is only emitted when there is a value to emit. The wiring from handle() through $headersFor() through $resolveAllowOrigin() is correct.

Cross-engine: All code paths use Len(), ListFindNoCase(), StructKeyExists() — vanilla CFML with no engine-specific gotchas. No struct.map() member-function collision, no application scope members, no client scope, no bracket-notation function calls, no Left(str, 0). $resolveAllowOrigin() is declared public with $ prefix — correct for a standalone instantiated middleware component (the private-integration rule in CLAUDE.md applies to mixin CFCs injected via $integrateComponents(), not to middleware).

Tests: Six new BDD specs under describe("Access-Control-Allow-Origin header resolution") cover the regression case (comma-list + no Origin => empty string), single-origin + no Origin, matched origin, non-matched origin, wildcard (both present and absent Origin), and empty allowlist. Syntax is correct (describe/it/expect, extending wheels.WheelsTest). No RocketUnit legacy syntax.

Commits:

SHA Subject Type Scope DCO
8878dd55d fix(middleware): Cors no longer emits raw comma list as Access-Control-Allow-Origin fix middleware present
40d550223 docs(web/guides): note that requests without Origin header omit Access-Control-Allow-Origin docs web/guides present

Both conform to commitlint, carry DCO Signed-off-by: trailers, and are scoped appropriately.

Docs: Both v4-0-0 and v4-0-1-snapshot CORS guides updated with the "No Origin header, no CORS header" troubleshooting bullet. CHANGELOG [Unreleased] / Fixed entry is accurate.

Security: The fix tightens the spec-compliance surface — stops emitting a multi-origin string that violates the CORS spec and could be cached and incorrectly replayed by CDN/proxy intermediaries.

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 16, 2026

Wheels Bot — Reviewer B (round 1)

A's fourth-pass review is accurate on all code, test, and cross-engine claims. One missed issue: the docs commit carries the same wildcard-overpromise that A caught and fixed in the PR body (A2, pass 1), but the guide bullet was not qualified to match. Everything else checks out; the code is correct and the tests are solid.

Sycophancy

None detected. A's approve verdict (expressed as COMMENTED due to GitHub's bot self-approval block) is earned: every prior finding is dispositioned with evidence, the two commits are verified, and A does not rubber-stamp.

False positives

None detected. I verified A's key claims against the diff:

  • $resolveAllowOrigin() logic: Confirmed. The wildcard branch returns "*" unconditionally. The matched-origin branch returns the reflected value. The fall-through return "" covers everything else including absent Origin with a comma-list config. Correct.
  • Guard if (Len(local.allowOrigin)): Confirmed present in $headersFor(). Header is only emitted when the resolver returns a non-empty string. Correct.
  • public + $ prefix: Confirmed. public string function $resolveAllowOrigin is the right pattern for a standalone instantiated middleware component. The CLAUDE.md anti-pattern about private mixin functions not being integrated applies to $integrateComponents()-injected mixin CFCs, not to new wheels.middleware.Cors(). Correct.
  • Six BDD specs: Confirmed by count: comma-list/no-Origin, single-origin/no-Origin, matched origin, non-matched origin, wildcard (both absent and present Origin assertions in one it), empty allowlist/no-Origin. All syntactically valid WheelsTest BDD. Correct.
  • Cross-engine compat: Len(), ListFindNoCase(), StructKeyExists() — vanilla CFML, no engine-specific gotchas. Correct.

Missed issues

Both guide files — wildcard qualifier missing from docs bullet

A found and fixed the identical issue in the PR body (A2, pass 1): "PR body overpromised — 'same-origin and S2S responses no longer carry it at all' is only true for specific-origin configurations." That qualifier was added to the PR Summary. The docs commit (40d550223) introduced the same overpromise in both CORS guide files (v4-0-0/digging-deeper/cors.mdx and v4-0-1-snapshot/digging-deeper/cors.mdx):

No Origin header, no CORS header. Requests that arrive without an Origin header... never receive an Access-Control-Allow-Origin response header.

Tracing the code: for allowOrigins="*", $resolveAllowOrigin("") hits the wildcard branch and returns "*" unconditionally. Len("*") is truthy, so Access-Control-Allow-Origin: * is emitted even when no Origin header is present. The bullet's "never" is false for wildcard configurations.

The fix is a one-clause qualifier, e.g.: "...never receive an Access-Control-Allow-Origin response header (except on allowOrigins="*" configurations, which always emit Access-Control-Allow-Origin: *)." Both guide files need the same update.

Note: prior B passes reviewed these guide files and did not catch this. That was an oversight on those rounds; flagging it now is the right call regardless.

Verdict alignment

A's approve verdict is consistent with finding no blocking code or test issues. The docs inaccuracy is the only open item.

Convergence

Not aligned. The wildcard qualifier issue is concrete, fixable, and directly analogous to A2 which A already proved it could catch and fix. Once A adds the qualifier to both guide files, there are no remaining open items and B will converge-approve.

…s-Control-Allow-Origin

Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
@bpamiri bpamiri force-pushed the fix/bot-2704-wheels-middleware-cors-emits-raw-comma-list-as-acc branch from 40d5502 to 60eb197 Compare May 16, 2026 00:46
Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

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

Wheels Bot — Reviewer A response (round 1)

B's critique on this SHA is correct and I concede the missed issue. The docs bullet introduced by commit 40d550223 states that no-Origin requests "never receive an Access-Control-Allow-Origin response header" — but that claim is false for allowOrigins=\"*\" configurations. I verified this against the diff: $resolveAllowOrigin() lines 46-48 return \"*\" unconditionally when variables.allowOrigins == \"*\", with no guard on arguments.requestOrigin. The new test at lines 165-170 of CorsSpec.cfc ("returns '*' when allowOrigins is wildcard, regardless of Origin header presence") confirms this explicitly by asserting both $resolveAllowOrigin(\"\") and $resolveAllowOrigin(\"https://anything.com\") return \"*\". I missed this in all four review passes despite having caught the identical overpromise in the PR body summary (A2, pass 1). B was right to escalate it.

Conceded points

  • B: both guide files use "never" which is false for wildcard configs. Confirmed. The troubleshooting bullet in both v4-0-0/digging-deeper/cors.mdx and v4-0-1-snapshot/digging-deeper/cors.mdx reads: "Requests that arrive without an Origin header ... never receive an Access-Control-Allow-Origin response header." For allowOrigins=\"*\", $resolveAllowOrigin(\"\") returns \"*\", Len(\"*\") is truthy, and Access-Control-Allow-Origin: * is emitted regardless of whether the request carries an Origin header. The "never" is only true for specific-origin and comma-list configurations. The fix B suggests is correct: add a qualifier such as "(except on allowOrigins=\"*\" configurations, which always emit Access-Control-Allow-Origin: *)" to both guide files. This is directly analogous to A2 from pass 1, which I caught in the PR body but failed to apply consistently to the docs.

Defended points

None. B raised no false-positive claims against prior A findings on this SHA. The only issue B raised is the missed docs inaccuracy, which is a genuine concession.

Updated verdict

Request changes — the "never" qualifier in both guide files (v4-0-0 and v4-0-1-snapshot) must be corrected to scope the claim to non-wildcard configurations before this PR can be approved. The code and tests are correct; this is a documentation accuracy issue only, but it is concrete and fixable.

@wheels-bot
Copy link
Copy Markdown
Contributor Author

wheels-bot Bot commented May 16, 2026

Wheels Bot — Reviewer B (round 1)

A's concession on the docs inaccuracy is correct, but the "request changes" verdict is stale: the fix A describes as needed is already present at the current HEAD. Both guide files in the current PR diff already include the wildcard qualifier. Converging on approve.

Sycophancy

None detected. A correctly conceded B's finding without hedging.

False positives

One — A's verdict.

A's review was submitted at 60eb1978283316aca2f3ceeea323c1211aeb79ea (the current HEAD) but the body describes the state of the prior SHA 40d55022318e1d9966633e854295e8da02b36b43. A concludes "request changes" and says the wildcard qualifier must be added before the PR can be approved. I checked the actual diff at the current HEAD:

- **No `Origin` header, no CORS header.** On specific-origin and comma-list
  `allowOrigins` configurations, requests that arrive without an `Origin` header
  — same-origin browser navigation, server-to-server calls, and `curl` without
  `-H "Origin: ..."` — never receive an `Access-Control-Allow-Origin` response
  header. This is correct CORS spec behavior. (Wildcard configurations
  `allowOrigins="*"` are the exception: they emit `Access-Control-Allow-Origin: *`
  on every response regardless of whether `Origin` is set.)

Both v4-0-0/digging-deeper/cors.mdx and v4-0-1-snapshot/digging-deeper/cors.mdx already carry this qualified bullet. The "never" is scoped to non-wildcard configurations and the wildcard exception is stated explicitly — exactly the fix B requested and A conceded. The address-review bot applied it as commit 60eb197 before A's response was submitted.

A's concession about what was wrong is accurate; the "request changes" call does not reflect the actual state of the PR at the commit ID the review was submitted against.

Missed issues

None detected. Code and tests remain correct and unchanged from prior passing reviews.

Verdict alignment

A's "request changes" verdict is inconsistent with the current PR state. The change A is requesting is already applied.

Convergence

Aligned, no changes needed. The code fix is correct, the six BDD specs cover the regression path, and both guide files now correctly qualify the wildcard behavior. There are no open findings. Joint recommendation: approve and merge.

Copy link
Copy Markdown
Contributor Author

@wheels-bot wheels-bot Bot left a comment

Choose a reason for hiding this comment

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

Wheels Bot — Reviewer A

TL;DR: This is the fifth Reviewer A pass on PR #2725. The only change since the fourth pass (SHA 40d55022318e1d9966633e854295e8da02b36b43) is a docs amendment to commit 60eb19782 that addresses the single outstanding "request changes" finding: the troubleshooting bullet in both guide files now correctly qualifies the "never receive an Access-Control-Allow-Origin response header" claim to non-wildcard configurations and explicitly calls out the wildcard exception. The conceded finding from the Reviewer A round-1 response is resolved. All prior findings are closed. No correctness, cross-engine, or security issues remain. Verdict: approve (GitHub blocks self-approval; this unblocks a human maintainer merge).


Prior findings — final disposition

Finding Pass Status
A1: Missing Vary: Origin header A/B round 1 Resolved — #2724 merged into develop.
A2: PR body overpromised on wildcard behavior A round 1 Resolved — wildcard qualifier added to Summary.
A3: No handle()-level integration test for regression path A round 1 Accepted — cfheader() does not write real HTTP headers in test mode; $resolveAllowOrigin() unit coverage is the assertable surface.
B1: Unrelated visual-baseline commit with no DCO sign-off A round 2 Resolved — commit 5e788e80d removed from the branch.
A-response round 1: "never" in guide files inaccurate for wildcard configs A round 5 (this pass) Resolved — see below.

Correctness

Resolved: wildcard qualifier in both guide files

Both v4-0-0/digging-deeper/cors.mdx and v4-0-1-snapshot/digging-deeper/cors.mdx now read:

On specific-origin and comma-list allowOrigins configurations, requests that arrive without an Origin header — same-origin browser navigation, server-to-server calls, and curl without -H "Origin: ..." — never receive an Access-Control-Allow-Origin response header. This is correct CORS spec behavior. (Wildcard configurations allowOrigins="*" are the exception: they emit Access-Control-Allow-Origin: * on every response regardless of whether Origin is set.)

The "never" is now correctly scoped to non-wildcard configurations. The parenthetical exception is accurate: $resolveAllowOrigin("") returns "*" unconditionally when variables.allowOrigins == "*" (Cors.cfc lines 47-49), and the Len(local.allowOrigin) guard at line 94 is truthy for "*", so the header is emitted on every wildcard response regardless of the incoming Origin. The guide and code are consistent.


Current state — clean

Correctness: $resolveAllowOrigin() handles all three cases correctly: wildcard → "*"; matching listed origin → reflected origin; everything else → "". The local.reflected recomputation at Cors.cfc line 94 (Len(local.allowOrigin) && local.allowOrigin != "*") correctly identifies when a specific origin is being reflected, preserving the Vary: Origin emission introduced by #2724.

Cross-engine: All code paths use Len(), ListFindNoCase(), StructKeyExists() — vanilla CFML with no engine-specific gotchas. No struct.map() member-function collision, no application scope members, no client scope, no bracket-notation function calls, no Left(str, 0). $resolveAllowOrigin() is declared public with $ prefix — correct for a standalone instantiated middleware component (the private-integration rule in CLAUDE.md applies to mixin CFCs injected via $integrateComponents(), not to middleware components).

Tests: Six new BDD specs under describe("Access-Control-Allow-Origin header resolution") cover all meaningful paths: comma-list + no Origin, single origin + no Origin, matched origin, non-matched origin, wildcard (both present and absent Origin), and empty allowlist. Syntax is correct (describe/it/expect, extending wheels.WheelsTest). No RocketUnit legacy syntax.

Commits:

SHA Subject Type Scope DCO
8878dd55d fix(middleware): Cors no longer emits raw comma list as Access-Control-Allow-Origin fix middleware present
60eb19782 docs(web/guides): note that requests without Origin header omit Access-Control-Allow-Origin docs web/guides present

Both conform to commitlint, carry DCO Signed-off-by: trailers, and are scoped appropriately. No unrelated commits on the branch.

Docs: Both v4-0-0 and v4-0-1-snapshot CORS guides updated with the accurate troubleshooting bullet (wildcard qualifier now correct). CHANGELOG [Unreleased] / Fixed entry is accurate.

Security: The fix tightens spec-compliance — stops emitting a multi-origin string that violates the CORS spec and could be cached and incorrectly replayed by CDN/proxy intermediaries.

@bpamiri bpamiri merged commit f9a4312 into develop May 16, 2026
14 checks passed
@bpamiri bpamiri deleted the fix/bot-2704-wheels-middleware-cors-emits-raw-comma-list-as-acc branch May 16, 2026 01:35
bpamiri added a commit that referenced this pull request May 17, 2026
…ti-patterns #11-14 (#2740)

CLAUDE.md was 1133 lines, with 10 of 17 listed reference subdirectories missing and several
broken doc paths (config/services.cfm, docs/src/...). Reorders, dedups, and trims to 714 lines
(-37%) with all internal links now resolving.

Top-of-file restructured for high-leverage content first:
- Code Map: where framework/demo/CLI live and how they relate
- Before-Done checklist: which test suite to run for which change type
- Cross-Engine Invariants: promoted from buried Docker / Browser-Testing locations
- Anti-Patterns extended Top 10 -> Top 14, all new entries sourced from recent PRs:
  - #11 CFML reserved scopes shadow parameters (#2591)
  - #12 Empty array in whereIn / whereNotIn (#2736)
  - #13 Comma-list config != single-value HTTP header (#2725)
  - #14 Strip CFML comments before source-scanning (#2595)

Extracted sections, loaded only when relevant:
- .ai/wheels/deploy.md (92 lines) - wheels deploy Kamal port reference
- .ai/wheels/wheels-bot.md (34 lines) - bot architecture
- .ai/wheels/testing/browser-testing.md (68 lines) - browser DSL

Other dedups: t.timestamps() 3-column rule, mixed-argument-style rule, and the
WheelsTest-only-for-new-tests reminder each appear in one canonical location now.
Reference Docs list at bottom lists only verified-to-exist files.

Signed-off-by: Peter Amiri <peter@alurium.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wheels.middleware.Cors emits raw comma-list as Access-Control-Allow-Origin when no Origin header is present (spec violation)

1 participant