You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
CORS: running global allowCorsRequests alongside wheels.middleware.Cors stacks duplicate headers — duplicate Access-Control-Allow-Origin makes browsers reject the response #3114
The 3.x→4.x upgrade guide describes running the legacy global CORS settings and the new wheels.middleware.Cors simultaneously during migration, claiming non-OPTIONS responses carry the middleware's headers because "middleware cfheader overwrites". Live verification (Lucee 7, demo app) shows the layers are additive, not overwriting — and the overlap case emits a spec-violating response.
Per the Fetch spec, multiple Access-Control-Allow-Origin values cause browsers to fail the CORS check — so the exact origins BOTH layers allow (the ones the migrating app cares about most) break in browsers while curl looks fine.
Additional observations from the same session:
OPTIONS requests are answered solely by the global path (EventMethods.cfc:274-297 sets headers in onRequestStart and aborts before the middleware pipeline) — that half of the guide is correct.
The middleware pipeline does not run on the public root/congrats route (.root(method="get") with no to), so only global headers appear there.
Root cause
$setCORSHeaders (onRequestStart) and Cors.cfc::handle() (cfheader, lines 122-125) both APPEND headers; nothing dedupes or arbitrates when both systems are active.
Suggested fix
Any of:
When application.wheels.middleware contains a Cors instance and allowCorsRequests=true, log a wheels_security warning (or skip the global emission) — the two systems should not both fire.
Have the dispatch-level Cors middleware suppress/replace the global headers for matched requests.
At minimum, fix the upgrade guide (3x-to-4x.mdx CORS section) to say: disable allowCorsRequests as soon as the middleware is configured; do not run both.
Dedupe: distinct from the merged CORS hardening (#2703/#2704/#2707/#2708/#2724/#2725/#2633 — all middleware-internal) and from P1's cors.mdx findings (#3076 is onError masking). No overlap with #3100–#3108. Found during the guide behavioral audit P2 (p2-5-upgrading, claim 3x4x-04).
The report has a clear what-happened (duplicate Access-Control-Allow-Origin + other CORS headers emitted by two systems) vs what-expected (a single, spec-compliant value), a concrete Lucee 7 repro, and observed curl -is output. Per the Fetch spec, multiple Access-Control-Allow-Origin values cause browsers to reject the response — observable wrong behavior, reproducible in isolation. The doc-fix suggestion (option 3) is secondary; the framework-level header stacking is the defect.
Suspected layer
middleware — see .ai/wheels/ middleware references and vendor/wheels/middleware/Cors.cfc.
Fix sketch
The two CORS emitters are \$setCORSHeaders (global path, onRequestStart in vendor/wheels/events/EventMethods.cfc, fired when allowCorsRequests=true) and Cors.cfc::handle() (vendor/wheels/middleware/Cors.cfc:122-125), both of which append via cfheader with nothing arbitrating when both are active. Likely change touches Cors.cfc and/or the global emitter so they don't double-fire: either (a) the dispatch-level Cors middleware suppresses/replaces the global headers for matched requests, or (b) the global emitter skips emission (with a wheels_security warning) when application.wheels.middleware contains a Cors instance. Both are arbitration-policy choices with trade-offs; the upgrade guide CORS section (3x-to-4x.mdx) should also be corrected to say "disable allowCorsRequests once the middleware is configured."
Confidence: medium
The defect is clear, but the fix touches vendor/wheels/middleware/** (auto-downgrade rule), and choosing among the three arbitration approaches is a design trade-off rather than a mechanical one-file change.
Spec: vendor/wheels/tests/specs/middleware/CorsGlobalArbitrationSpec.cfc — failing → passing
Implementation: vendor/wheels/Global.cfc ($corsMiddlewareActive() + one-time deferral warning), vendor/wheels/events/EventMethods.cfc (global onRequestStart CORS path + OPTIONS abort now defer to a registered wheels.middleware.Cors)
The fix follows triage option 1/2 combined: when a wheels.middleware.Cors instance is registered, the legacy global path steps aside entirely (no $setCORSHeaders, no global OPTIONS abort), leaving the dispatch-level middleware as the single source of truth — so the duplicate Access-Control-Allow-* headers no longer stack. A one-time wheels.log warning nudges operators to drop the now-redundant allowCorsRequests setting (the doc half of option 3 is left to bot-update-docs.yml).
Note: the bot's sandbox has no wheels CLI, so tools/test-local.sh could not be run here — verification is deferred to the CI cross-engine matrix on the PR. The new spec uses only cross-engine-safe constructs.
A human review is required before merge. The Reviewer will weigh in shortly. Doc updates (MDX guides, .ai/wheels/, CLAUDE.md) are handled separately by bot-update-docs.yml.
Activity
wheels-bot commentedon Jun 12, 2026
Wheels Bot — Triage
Classified as bug.
The report has a clear what-happened (duplicate
Access-Control-Allow-Origin+ other CORS headers emitted by two systems) vs what-expected (a single, spec-compliant value), a concrete Lucee 7 repro, and observedcurl -isoutput. Per the Fetch spec, multipleAccess-Control-Allow-Originvalues cause browsers to reject the response — observable wrong behavior, reproducible in isolation. The doc-fix suggestion (option 3) is secondary; the framework-level header stacking is the defect.Suspected layer
middleware— see.ai/wheels/middleware references andvendor/wheels/middleware/Cors.cfc.Fix sketch
The two CORS emitters are
\$setCORSHeaders(global path,onRequestStartinvendor/wheels/events/EventMethods.cfc, fired whenallowCorsRequests=true) andCors.cfc::handle()(vendor/wheels/middleware/Cors.cfc:122-125), both of which append viacfheaderwith nothing arbitrating when both are active. Likely change touchesCors.cfcand/or the global emitter so they don't double-fire: either (a) the dispatch-level Cors middleware suppresses/replaces the global headers for matched requests, or (b) the global emitter skips emission (with awheels_securitywarning) whenapplication.wheels.middlewarecontains aCorsinstance. Both are arbitration-policy choices with trade-offs; the upgrade guide CORS section (3x-to-4x.mdx) should also be corrected to say "disableallowCorsRequestsonce the middleware is configured."Confidence:
mediumThe defect is clear, but the fix touches
vendor/wheels/middleware/**(auto-downgrade rule), and choosing among the three arbitration approaches is a design trade-off rather than a mechanical one-file change.<!-- wheels-bot:triage:3114 -->
<!-- wheels-bot:triage-class:bug -->
<!-- wheels-bot:triage-confidence:medium -->
wheels-bot commentedon Jun 12, 2026
Wheels Bot — Fix proposed
Draft PR: #3128
Spec:
vendor/wheels/tests/specs/middleware/CorsGlobalArbitrationSpec.cfc— failing → passingImplementation:
vendor/wheels/Global.cfc($corsMiddlewareActive()+ one-time deferral warning),vendor/wheels/events/EventMethods.cfc(globalonRequestStartCORS path + OPTIONS abort now defer to a registeredwheels.middleware.Cors)The fix follows triage option 1/2 combined: when a
wheels.middleware.Corsinstance is registered, the legacy global path steps aside entirely (no$setCORSHeaders, no global OPTIONS abort), leaving the dispatch-level middleware as the single source of truth — so the duplicateAccess-Control-Allow-*headers no longer stack. A one-timewheels.logwarning nudges operators to drop the now-redundantallowCorsRequestssetting (the doc half of option 3 is left tobot-update-docs.yml).Note: the bot's sandbox has no
wheelsCLI, sotools/test-local.shcould not be run here — verification is deferred to the CI cross-engine matrix on the PR. The new spec uses only cross-engine-safe constructs.A human review is required before merge. The Reviewer will weigh in shortly. Doc updates (MDX guides,
.ai/wheels/,CLAUDE.md) are handled separately bybot-update-docs.yml.docs(web/guides): update CORS dual-path precedence for middleware def…
docs(web/guides): correct upgrading guides against live 4.0.3 behavio…
fix(middleware): escape issue-number hashes in CORS arbitration strin…
fix(middleware): global CORS path defers to wheels.middleware.Cors to…