fix(admin): serve the SPA shell on /admin/ as well as /admin - #528
Conversation
An axum `{*path}` segment must match at least one character, so `/admin/`
matched neither `.route("/admin", ...)` nor `.route("/admin/{*path}", ...)`
and answered a bare 404 on the dashboard's own mount root.
Register `/admin/` beside `/admin` in the base router, pointing at the same
`dashboard()` handler. Registering it there rather than beside the
`--features ui` catch-alls keeps the trailing-slash form's answer identical
to `/admin`'s in both builds -- the shell with the feature, and without it
the 404 whose body names `--features ui` rather than axum's empty one.
The repo already knew this rule: `/admin/api` and `/admin/api/` are
registered separately for exactly the same reason. The JSON namespace root
had both spellings; the mount root had only the bare one.
Closes #527.
There was a problem hiding this comment.
Code Review
This pull request registers the trailing-slash version of the admin mount root (/admin/) alongside the exact route (/admin) to prevent requests with a trailing slash from falling through to a bare 404, as wildcard segments in axum cannot match empty strings. The changes include routing updates in src/admin/mod.rs, comprehensive documentation updates across multiple languages, and corresponding test coverage additions in tests/admin_ui.rs and tests/router_surface.rs. There are no review comments provided, so I have no feedback to evaluate.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
No issues found across 9 files
Architecture diagram
sequenceDiagram
participant Browser as Browser / Proxy
participant Router as Axum Router
participant Dashboard as dashboard handler
participant Shell as UI Shell (index.html)
participant Feature404 as Feature-Naming 404
Note over Browser,Router: GET /admin/ request (trailing slash)
Browser->>Router: GET /admin/
alt --features ui build
Router->>Dashboard: CHANGED: /admin/ route maps to dashboard()
Dashboard-->>Shell: Serve SPA shell (text/html)
Shell-->>Router: index.html with /admin/assets/ links
Router-->>Browser: 200 OK (text/html)
else default build (no ui feature)
Router->>Dashboard: CHANGED: /admin/ route maps to dashboard()
Dashboard-->>Feature404: 404 with "--features ui" body
Feature404-->>Router: explanatory 404
Router-->>Browser: 404 (with feature-naming body)
end
Note over Browser,Router: GET /admin request (exact path)
Browser->>Router: GET /admin
alt --features ui build
Router->>Dashboard: /admin route maps to dashboard()
Dashboard-->>Shell: Serve SPA shell (text/html)
Shell-->>Router: index.html with /admin/assets/ links
Router-->>Browser: 200 OK (text/html)
else default build (no ui feature)
Router->>Dashboard: /admin route maps to dashboard()
Dashboard-->>Feature404: 404 with "--features ui" body
Feature404-->>Router: explanatory 404
Router-->>Browser: 404 (with feature-naming body)
end
Note over Router: Both /admin and /admin/ registered via .route()<br>in admin_router() base router
Note over Browser,Router: Route inventory gates verify counts<br>ADMIN_PATHS: 18 paths, 20 method+path pairs
|
… line The #526 index line still claimed the new cfg(not(ui)) router test never runs in CI. That claim was corrected in the note itself during #526 but not in MEMORY.md, which is the file loaded first -- so the stale version was the one being read. It is run, by the default-build step that PR added. The #528 docs-review note cited a nonexistent `ADMIN_ROUTES` fixture; the constant is `ADMIN_PATHS`.
|
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Re-trigger cubic



Closes #527.
What was wrong
On a
--features uibuild,GET /admin/answered a bare404whileGET /adminandGET /admin/anythingboth served the SPA shell.An axum
{*path}segment must match at least one character, so the trailing-slash spelling matched neither registration that could have served it:.route("/admin", get(dashboard))— an exact path.route("/admin/{*path}", get(ui::shell))— needs a non-empty suffixThe repo already knew this rule:
/admin/apiand/admin/api/are registered separately for exactly the same reason, with a comment saying so. The JSON namespace root got both spellings; the mount root got only the bare one.The fix
Register
/admin/alongside/admin, pointing at the samedashboard()handler.It goes in the base router rather than beside the
--features uicatch-alls, so the trailing-slash form inherits the same build-dependent answer/adminalready has: the shell with the feature, and without it the404whose body names--features ui. A slash appended by a browser or a proxy must not change which of those two an operator gets — including not downgrading the explanatory404to axum's empty-bodied one.Tests
admin_ui::the_mount_root_with_a_trailing_slash_serves_the_spa_shell— asserts the shell, not just a200, since a redirect or an empty page would satisfy a status-only check while still failing the operator who typed the slash.router_surface::the_mount_root_without_the_ui_feature_explains_the_missing_bundlenow runs over both spellings. They are two registrations answering one handler, so/admin/could otherwise regress to an empty404while/adminkept its sentence. This strictly widens the existing test — every assertion it made before, it still makes.Both were confirmed non-vacuous by deleting the new route: each fails with a real assertion (not a compile error) and passes once it is restored.
The route-inventory gates in
router_surface.rscaught the new route on the first run, which is what they are for:ADMIN_PATHS(17 → 18 paths, 19 → 20 method+path pairs) and the literal-registration count (40 → 41). The pair count is verified against the table rather than asserted by hand.No new hardening test for
/admin/: both routes resolve to the same handler, so there is no second code path for headers to drift on.Docs
ui/README.mdasserted the old behavior verbatim ("/admin/itself404s because an axum wildcard must match at least one character") — landed in #526 and falsified by this change, so it is rewritten rather than left to rot.Also updated:
docs/admin-ui-delivery.md's current-surface table, and the endpoints reference in all four locales (the route row, the unauthenticated-route list, and the mount-fallback paragraph).Summary by cubic
GET /admin/now serves the SPA shell in a--features uibuild, and the 404 naming--features uiwithout it, matchingGET /admininstead of answering a bare 404. An axum{*path}segment must match at least one character, so the trailing-slash spelling previously matched neither the exact route nor the fallback.Tests and docs
Written for commit cb905cf. Summary will update on new commits.