Skip to content

router: normalize base to always end with trailing slash#2

Merged
nothing-stops-this-train merged 1 commit into
lnd-integrationfrom
router-base-trailing-slash
May 22, 2026
Merged

router: normalize base to always end with trailing slash#2
nothing-stops-this-train merged 1 commit into
lnd-integrationfrom
router-base-trailing-slash

Conversation

@nothing-stops-this-train
Copy link
Copy Markdown

Summary

Hard-to-spot bug: when BITCART_ADMIN_ROOTPATH is set to /admin (no trailing slash), every nested admin URL (/admin/plugins/<x>, /admin/stores/<id>/<x>, etc.) renders Nuxt's "404 Not Found" page on the first paint, even though the route is correctly registered with Vue Router and the page renders fine server-side.

Single-segment URLs (/admin/, /admin/stores, /admin/plugins) survive because their corrupted initial path coincidentally resolves to the home route, which clears the sticky err state on the next Vue Router navigation. Multi-segment URLs never recover.

Root cause

.nuxt/utils.js's getLocation helper does base.slice(0, -1), with the comment "consideration is base is normalized with trailing slash". When base is /admin (no trailing slash), the slice corrupts it to /admi. The subsequent path.startsWith(slicedBase) matches the leading /admi of /admin/<x> but slices the path by 5 chars, producing strings like n/<x> — non-routes. router.resolve returns matched=0, Nuxt's client.js navigation hook sets app.context.error({statusCode: 404, message: "This page could not be found"}), and the err state never gets cleared.

Headless-Chromium reproducer (before patch)

Polling $nuxt.nuxt.err and $route.path from page load (cookies seeded so we're logged in):

/admin/                          → t=510ms  null|/|matched=1                ← OK
/admin/stores                    → t=323ms  null|/stores|matched=1          ← OK
/admin/invoices                  → t=363ms  null|/invoices|matched=1        ← OK
/admin/plugins                   → t=301ms  null|/plugins|matched=1         ← OK
/admin/plugins/liquidityhelper   → t=227ms  null|/|matched=0                ← initial: no match
                                   t=305ms  404|/|matched=0                 ← 404 set
                                   t=365ms  404|/plugins/liquidityhelper|matched=1   ← route resolves but err is sticky

After patch, the same probe shows:

/admin/plugins/liquidityhelper   → t=487ms  null|/plugins/liquidityhelper|matched=1   ← OK from the start

Patch

Normalize config.ROOTPATH to always end with / at the router-construction point. Three reasons to do it here (rather than at the env-var parse site or in publicRuntimeConfig):

  1. Doesn't affect other consumers of config.ROOTPATH — URL-construction code like ${config.ROOTPATH}/api/foo continues to see the unmodified value (e.g. /admin), so it doesn't accidentally produce double slashes like /admin//api/foo.
  2. The bug is specifically about Vue Router's base. The fix lives where the value is consumed.
  3. Single-line, well-localized, easy to revert if Nuxt's own getLocation is ever fixed upstream.

Normalization table

input output
/admin /admin/
/admin/ /admin/
/ /
"" /
undefined /

Test plan

  • Fresh URL-bar navigation to /admin/plugins/<any-plugin> no longer renders "404 Not Found".
  • Existing admin routes (/admin/, /admin/stores, etc.) still load correctly.
  • SPA navigation (clicking sidebar items) still works.
  • Hard reload (Cmd-Shift-R) of nested URLs still works.
  • On a root-path install (BITCART_ADMIN_ROOTPATH unset → "/"), behavior is unchanged — base stays /.

Note re: PR #1

This supersedes the diagnostic in PR #1. That PR addressed a symptom (async chunk 404 from missing /admin/ prefix on publicPath); this PR addresses the underlying cause of the visible "404 Not Found" page. The publicPath issue still happens but turns out to be cosmetic — it produces a single noisy console message for the workbox chunk, and doesn't break route resolution.

Nuxt's getLocation helper (in .nuxt/utils.js) does
`base.slice(0, -1)` on the configured base, with a comment
explicitly noting "consideration is base is normalized with trailing
slash". When BITCART_ADMIN_ROOTPATH is set to "/admin" (no trailing
slash — the form deploy.sh pins, and the form Nuxt accepts
everywhere else), that slice corrupts the base to "/admi". The
initial path strip then misfires: window.location.pathname
"/admin/<route>" startsWith("/admi") matches, but slicing by 5
produces "n/<route>" — a non-route. router.resolve returns
matched=0, Nuxt's navigation logic sets a 404 on the page, and
the err state is sticky for nested URLs.

Single-segment URLs (`/admin/`, `/admin/stores`) happen to recover
because the corrupted-then-resolved path "/" still matches Nuxt's
auto-generated home route, which clears the err state on the
subsequent Vue Router navigation. But multi-segment URLs like
`/admin/plugins/<plugin>` resolve to paths that match no route at
all, so the err is never cleared — the user sees a "404 Not Found"
page on every reload, even though the actual route is correctly
registered and the page renders fine server-side.

This was reproducible 100% of the time on `/admin/plugins/liquidityhelper`
with a headless-Chromium test against an `/admin`-rooted deploy.
After this patch, fresh URL-bar navigation lands on the right page
without a 404 flash.

We normalize at the router-construction point (not the env-var
parser or the publicRuntimeConfig binding) so that any other code
reading config.ROOTPATH continues to see the unmodified value —
which keeps URL-construction code like `\${config.ROOTPATH}/api/...`
from producing accidental "/admin//api/..." double slashes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant