Skip to content

Routing: inside .namespace("foo"), redundant to="foo/controller##action" produces opaque Foocontroller lookup; should reject or strip the prefix #2791

@bpamiri

Description

@bpamiri

Summary

Inside a .namespace("foo") block, the to= argument of a route gets concatenated with the namespace name in a confusing way: writing the (intuitive but redundant) to="foo/controller##action" produces a lookup for a controller class literally named Foocontroller instead of Controller under the foo package. The correct form is just to="controller##action" — the namespace adds the package prefix automatically — but the failure mode when you get it wrong is opaque.

Repro

// config/routes.cfm — WRONG (matches the namespace prefix redundantly)
mapper()
    .namespace("datapai")
        .get(name="datapaiDashboard", pattern="dashboard", to="datapai/dashboard##index")
    .end()
.end();

Then GET /datapai/dashboard produces:

Wheels.ViewNotFound — Could not find the view page for the `index` action in the `datapai.Datapaidashboard` controller.

Looked for the view at: app/views/datapai/datapaidashboard/index.cfm

The error is misleading. Wheels concatenated the namespace (datapai) with the (already-prefixed) controller path (datapai/dashboard) to form a class name Datapaidashboard — lowercase concatenation with the first letter capitalized. The user, having written datapai/dashboard expecting an app/controllers/datapai/Dashboard.cfc file, can't find the source of the bizarre Datapaidashboard name in the error.

The fix is to drop the redundant prefix:

// config/routes.cfm — CORRECT
mapper()
    .namespace("datapai")
        .get(name="datapaiDashboard", pattern="dashboard", to="dashboard##index")
    .end()
.end();

Now Wheels routes /datapai/dashboardapp/controllers/datapai/Dashboard.cfc#index as expected.

Why this happens

In vendor/wheels/mapper/scoping.cfc:42-49, the scope() function combines the parent scope's path and package with the current scope's, which is correct. But when a route is later registered with to="datapai/dashboard##index", the slash separator in the to= string is interpreted as a controller-path component and concatenated with the namespace's package, yielding datapai/datapai/dashboard → flattened to Datapaidashboard for the class name and datapai/datapaidashboard for the view directory.

There's no obvious place in the codebase where this concatenation is documented or guarded.

Impact

This is a foot-gun for anyone writing namespace routes. The plain to="dashboard##index" form looks underspecified ("but what namespace is this in?") and the redundant to="datapai/dashboard##index" looks defensively correct — until it isn't. The error message gives no hint that the namespace + to= interaction is the cause.

We hit this exactly once during the DataPAI Phase 0 build. Diagnosis required reading both scoping.cfc and the resulting Wheels.ViewNotFound error several times before realizing the issue. The fix is a one-line route change once you know what's wrong, but the diagnosis took ~30 min.

Suggested fixes (any of)

  1. Reject the redundant form at route-registration time. Inside a .namespace("foo") scope, if a to= starts with the namespace's name followed by /, throw a routing-config error at startup with a clear message:

    Route inside .namespace("foo") should use to="bar##action", not to="foo/bar##action" — the namespace prefix is added automatically. (got: to="foo/bar##action")

  2. Strip the redundant prefix automatically and emit a warning. Less strict than (1) but maintains backward compatibility for anyone whose routes currently work despite the redundant form.

  3. Document the interaction clearly in guides.wheels.dev/v4-0-0/routing/. The current docs explain .namespace() and to= separately but don't show their interaction or the "no redundant prefix" rule.

  4. Improve the ViewNotFound error message to include "Route registered as to='X' inside .namespace('Y')" so the user can trace it back to the route definition rather than the symptom.

Repo / version

  • Wheels Core: 4.0.0-SNAPSHOT+1779
  • File: vendor/wheels/mapper/scoping.cfc:42-49 (path/package combine logic)
  • Likely also in the route-registration path that parses to="controller##action" — needs investigation by someone with deeper Wheels routing knowledge to confirm the exact line.

Where found

Diagnosed during the DataPAI Phase 0 build in Titan when adding /datapai/* routes to land internal SSO users on a separate portal. The wrong form was committed first (taken literally from a draft plan that pre-dated familiarity with the Mapper); a follow-up commit on the same branch fixed it. See paiindustries/titan PR #3337 commit ad61a86cd for the corrected routes.cfm.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No 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