Skip to content

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

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions