Skip to content

static: root mount (urlPath: '/') matches only the exact path '/' — all sub-paths 404 (5.1.x) #1766

Description

@heskew

Summary

Apps configuring the built-in static plugin with a root mount (urlPath: '/') serve only the exact path / — every sub-path (/index.html, /assets/*, …) bypasses the static handler and falls through to the REST router's 404. Reported from production instances after auto-upgrade to 5.1.18 from 5.0.x, where this config worked; it broke every deployed app using root-mounted static assets with an explicit urlPath: '/'.

Sibling of #1583 / #1730: #1584 fixed the subpath mounts (urlPath: 'app', '/app') but not the root mount.

Root cause

server/middlewareChain.ts:

  1. normalizeUrlPath('/') returns '/' (truthy), so a root-mounted handler is grouped as a sub-route in resolveRoutedChains instead of joining the default chain.
  2. matchesRoute prefix-matches with a segment boundary:
    if (pathname !== urlPath && !pathname.startsWith(urlPath + '/')) return false;
    For urlPath === '/' the boundary check builds '//'. No real pathname starts with '//', so only the exact request / matches; all sub-paths skip the chain.
  3. Latent second defect: if a '/' route did match a sub-path, stripPrefix(request, '/') would slice one character off the pathname ('/index.html' → 'index.html'), producing a slash-less path.

The mount value comes from the Scope server proxy (components/Scope.ts): urlPath: scopeConfig.urlPath ? resolveBaseURLPath(pluginName, scopeConfig.urlPath) : undefined, and resolveBaseURLPath(name, '/') → '/'.

Affected versions

Broken since v5.1.0 (9dcf7b4, "Add urlPath/host routing with per-route middleware chains" — the Scope proxy began passing the configured urlPath as a route constraint). Present on main and the v5.1 line including 5.1.18. Worked in 5.0.x, where static registered its handler unconditionally and matched the full request path against its own maps. Not covered by #1584's integration tests (subpath mount only).

Repro

static:
  files: 'web/**'
  urlPath: '/'
  • GET / → 200 (index)
  • GET /index.html → 404
  • GET /<anything> → 404

Removing urlPath (unset) works — the breakage is specifically the explicit root value.

Fix direction

normalizeUrlPath should treat the root mount as "no path constraint" (return undefined for '/'): a '/' mount constrains nothing, so root-mounted handlers join the default chain — restoring 5.0.x semantics — and no prefix strip occurs (which also neutralizes the stripPrefix slice hazard, since normalizeUrlPath(prefix) ?? '' becomes ''). A host-constrained root mount ({ host, urlPath: '/' }) degrades correctly to a host-only sub-route.

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpatch

Type

No type

Fields

Priority

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions