Skip to content

wheels.middleware.Cors cannot short-circuit OPTIONS preflight — middleware runs after route dispatch #2703

@bpamiri

Description

@bpamiri

Severity: high — the new middleware is unusable for the canonical CORS use-case (cross-origin POST/PUT/PATCH/DELETE) without an upstream fix.

Surfaced by: titan Phase 2.6 (Wheels 4.0 upgrade), 2026-05-15. The legacy set(allowCorsRequests=true) global path handled preflight; the new middleware cannot.

Repro

Configure a Wheels 4.0 app with no explicit OPTIONS route on /api/v1/*:

set(middleware = [
    new wheels.middleware.Cors(allowOrigins="https://portal.pai.com")
]);

Then:

OPTIONS /api/v1/jvm HTTP/1.1
Origin: https://portal.pai.com
Access-Control-Request-Method: POST

HTTP/1.1 404 with Wheels.RouteNotFound — Incorrect HTTP Verb for route (from Dispatch.cfc:214).

Expected: 204 No Content with Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers. Browsers will block the actual cross-origin request when preflight fails.

Root cause

In vendor/wheels/Dispatch.cfc:$request():

  • Line 260: $paramParser() runs
  • Line 260+: $findMatchingRoute() 404s when no OPTIONS route matches
  • Line 320: middleware pipeline is built — never reached on the OPTIONS path

wheels.middleware.Cors has explicit preflight-handling code at Cors.cfc:89-95 that is unreachable.

For comparison, the legacy set(allowCorsRequests=true) path aborts OPTIONS at EventMethods.cfc:255-262, BEFORE dispatch route-matching.

Suggested fix

One of:

  1. Run a pre-route middleware pass (CORS middleware specifically — or a designated subset — runs before $findMatchingRoute()).
  2. Auto-register an OPTIONS verb on every route added via .resources() and friends, dispatching to a built-in preflight handler when CORS middleware is present.
  3. Special-case OPTIONS in the dispatcher: if the middleware pipeline contains a wheels.middleware.Cors instance, route OPTIONS to its preflight handler before 404'ing.

Why this matters for v4.0.1

This is the entire reason apps reach for a CORS middleware over the legacy global setting. Without preflight handling, the middleware is strictly less capable than the 3.x setting it's meant to replace.

cc @bpamiri

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions