Skip to content

Fix static plugin serving nothing when urlPath is configured#1584

Merged
kriszyp merged 6 commits into
mainfrom
fix/static-urlpath-1583
Jul 6, 2026
Merged

Fix static plugin serving nothing when urlPath is configured#1584
kriszyp merged 6 commits into
mainfrom
fix/static-urlpath-1583

Conversation

@kylebernhardy

@kylebernhardy kylebernhardy commented Jul 3, 2026

Copy link
Copy Markdown
Member

Fixes #1583.

Summary

With static: { files: 'web/**', urlPath: 'assets' }, no file was served at any path. Two stacked defects:

  1. The Scope server proxy passed the raw config value (assets) as the route mount, and matchesRoute compares mounts against leading-slash pathnames — the handler was never invoked for any request.
  2. Even with a leading slash, the routing chain's stripPrefix hands handlers a pathname with the mount prefix removed, while server/static.ts keyed its file maps by the full entry.urlPath (which includes the base) — every lookup missed.

Fix, in three parts: normalizeUrlPath now ensures a leading slash (closing the dead-mount trap for any registrant); the Scope proxy resolves urlPath through resolveBaseURLPath — the same normalization the entry pipeline uses, including '.'/'./x' semantics (verified both sides use the same plugin name); and static.ts keys its maps relative to the mount base. Two review-driven behavior decisions on top:

  • urlPath runtime changes now request a restart instead of pretending to hot-apply: the HTTP route mount is registered once at load and cannot be re-registered live, so recomputing keys against a changed option would drift from the actual mount.
  • The mount root disambiguates the no-slash form: the router strips both /assets and /assets/ to /, so the handler consults the unstripped pathname (exposed by stripPrefix as originalPathname, runtime-agnostic — Bun sets _nodeRequest to null) and 301s /assets/assets/ (relative links on a root index page would otherwise break). Falls back to serving when the property is unavailable.

Where to look

  • Blast radius of the proxy change: every plugin registering via scope.server.http with a config urlPath now mounts at the resolved base. Previously slash-less values produced dead mounts (nothing matched — the reported bug), so this activates broken configs rather than changing working ones; explicit urlPath in registration options still overrides via ...options (MCP's mountPath is unaffected).
  • The new originalPathname property on stripPrefix-wrapped requests is a small contract addition — it is the only way the original pathname survives the strip, and static's mount-root redirect (and any future consumer) depends on it.

Testing

  • Unit: normalizeUrlPath leading-slash cases and slash-less matchesRoute (unitTests/server/middlewareChain.test.js, 59 passing).
  • Integration (integrationTests/components/static-urlpath.test.ts, on the issue's exact fixture): file under mount, no root leak, directory index, directory 301 with prefixed Location, root index at /assets/, and /assets/assets/ 301. Verified 3 of the core 4 fail with the fix stashed.

Reported on 5.1.15 while dogfooding the docs-site replatform — good v5.1 patch candidate once it bakes on main. No docs change needed: this restores the documented urlPath behavior (the restart-on-change nuance may deserve a line in the static-files docs if reviewers agree).

Generated by an LLM (Claude Fable 5), cross-reviewed by Codex (hot-reload drift + mount-root redirect findings addressed) and Gemini (approved; flagged the mount-root case as worth noting, now fixed).

Two stacked defects: the Scope server proxy passed the raw config value
('assets') as the route mount, which can never match leading-slash
pathnames; and the routing chain strips the mount prefix from
req.pathname while the static maps were keyed by the full entry URL
path. normalizeUrlPath now ensures a leading slash, the Scope proxy
resolves urlPath via resolveBaseURLPath (same normalization as the
entry pipeline), and static.ts keys its maps relative to the mount
base captured at registration. urlPath runtime changes now request a
restart (the route mount cannot be re-registered live), and the mount
root disambiguates the no-slash form via the unstripped request so
the root index 301s to the trailing-slash path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request resolves issue #1583 where the static plugin failed to serve files when a custom urlPath was configured. It normalizes slash-less paths by ensuring a leading slash, keys static file maps relative to the base URL path, and requests a server restart if urlPath changes. It also handles redirects to trailing-slash paths and adds comprehensive tests. The review feedback correctly identifies that query parameters are lost during both the mount root and directory redirects, and provides actionable suggestions to preserve them in the Location header.

Comment thread server/static.ts
Comment thread server/static.ts
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Reviewed; no blockers found.

nizzlenitz and others added 2 commits July 2, 2026 21:12
…oot disambiguation

Codex review: BunRequest sets _nodeRequest to null, so the mount-root
redirect silently never fired on Bun. stripPrefix now exposes the
unstripped pathname as an explicit property instead of handlers
reaching into runtime internals. Verified on both runtimes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gemini review: both 301 Location headers dropped the original query
string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@kylebernhardy kylebernhardy marked this pull request as ready for review July 3, 2026 04:23

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed via Claude review-queue. Real fix for the completely-broken urlPath static-serving feature — the three-part fix (mount normalization, resolveBaseURLPath resolution, and mount-relative map keys) is coherent and well-tested end to end, including the mount-root-redirect edge case. (Gemini flagged four "blockers" here that don't hold up on closer read — TypeStrip .ts imports are this repo's existing convention, requestRestart() is a real instance method, and resolveBaseURLPath already guards its inputs.)

One perf nit worth a quick follow-up: query-string parsing in server/static.ts (~line 121) runs unconditionally on the index-serving path, even in the common case where indexEntries resolves immediately to a 200 with no redirect. Since static serving is squarely in the request hot path, worth hoisting that queryIndex/query computation inside the two redirect branches so the default-index case stays allocation-free.

Approving — the core fix is solid, this is a follow-up not a blocker.

Kris Zyp and others added 3 commits July 6, 2026 15:07
Keep the common index-serve path (200, no redirect) allocation-free by
computing queryIndex/query lazily inside each 301 branch instead of
unconditionally on every index request. Static serving is in the request
hot path. Follow-up to review on #1584.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
no-restricted-imports bans the node:assert/strict subpath; node:assert
exports the same strictEqual/ok. Unblocks runLinter on #1584.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kriszyp kriszyp merged commit 5e6d9f1 into main Jul 6, 2026
44 of 47 checks passed
@kriszyp kriszyp deleted the fix/static-urlpath-1583 branch July 6, 2026 21:24
kriszyp added a commit that referenced this pull request Jul 9, 2026
* Fix static plugin serving nothing when urlPath is configured (#1583)

Two stacked defects: the Scope server proxy passed the raw config value
('assets') as the route mount, which can never match leading-slash
pathnames; and the routing chain strips the mount prefix from
req.pathname while the static maps were keyed by the full entry URL
path. normalizeUrlPath now ensures a leading slash, the Scope proxy
resolves urlPath via resolveBaseURLPath (same normalization as the
entry pipeline), and static.ts keys its maps relative to the mount
base captured at registration. urlPath runtime changes now request a
restart (the route mount cannot be re-registered live), and the mount
root disambiguates the no-slash form via the unstripped request so
the root index 301s to the trailing-slash path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Expose originalPathname from stripPrefix for runtime-agnostic mount-root disambiguation

Codex review: BunRequest sets _nodeRequest to null, so the mount-root
redirect silently never fired on Bun. stripPrefix now exposes the
unstripped pathname as an explicit property instead of handlers
reaching into runtime internals. Verified on both runtimes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Preserve query strings on static trailing-slash redirects

Gemini review: both 301 Location headers dropped the original query
string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* perf(static): hoist query-string parse into redirect branches

Keep the common index-serve path (200, no redirect) allocation-free by
computing queryIndex/query lazily inside each 301 branch instead of
unconditionally on every index request. Static serving is in the request
hot path. Follow-up to review on #1584.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(lint): import from node:assert, not node:assert/strict

no-restricted-imports bans the node:assert/strict subpath; node:assert
exports the same strictEqual/ok. Unblocks runLinter on #1584.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Kyle Bernhardy <kyle.bernhardy@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Kris Zyp <kris@harperdb.io>
Co-authored-by: Kris Zyp <kriszyp@gmail.com>
(cherry picked from commit 5e6d9f1)
@hdbjeff hdbjeff added the patch label Jul 9, 2026
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Patch cherry-pick: conflict

Cherry-pick onto v5.1 produced conflicts on commit(s): 21d022b5408705280ffcb829b9c041427b61233b eb6631deb6a5854db1e55a88f8c8c84e0100ef77 aae9a110d129d16c8f7b9d7643ffb2c6474ab23a 24ef8e92d0ce2deba2f6c2be151d6bc08709a280 ebff1f4a0c578b466e796b0dc15653d021354344 a2e9a63f166e923c3b1c757bdfcdbe83963acd9a

The conflict markers are committed on branch cherry-pick/v5.1/pr-1584.
A pull request has been opened to land this patch: #1739

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

static plugin: setting urlPath prevents all files from being served (5.1.15)

4 participants