fix: Local Models settings crash — trailing-slash routing miss (#180 #197 #189)#199
Merged
KB (KB-syntheticsciences) merged 5 commits intoJul 22, 2026
Merged
Conversation
…197/#189) The catch-all only refused SPA-fallback for /api/*, but the JSON API is also mounted under /settings/*, /config, /session, etc. An unmatched request to one of those paths returned 200 + index.html, which the SPA JSON.parses and crashes on. Add wantsJson() to detect API/data calls (JSON content-type, or an Accept header wanting JSON without text/html) and return a JSON 404 for them before falling back to the SPA shell. Browser navigations are unaffected.
If a settings route ever returns a 200 with an HTML body (e.g. the SPA fallback for a missed API route), res.json() throws an opaque "SyntaxError: Unexpected token '<'". Check the content-type before parsing and throw a descriptive error naming the path, status, and received content-type instead.
#197/#189) Hono's default strict routing treats /settings/local and /settings/local/ as distinct routes, so the mounted LocalModelsRoutes sub-app only matched the no-slash form. The Local Models panel requests the trailing-slash form, missed the real route, fell through to the SPA catch-all, and crashed on res.json() parsing HTML. Set strict: false on the top-level Hono app so /x/ resolves like /x for every route (propagates through .route() to mounted sub-apps), and send the canonical no-trailing-slash path from the two LocalModels call sites that reported crashes (load and add).
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Collaborator
Author
|
Admin-merging past the failing The red
Every other check passed. Merging on admin override; CI will recover on its own once GitHub ships a fixed image. Closes #180, #197, #189. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the Local Models settings page crashing with
SyntaxError: Unexpected token '<', "<!doctype "... is not valid JSON, which also wedged the rest of Settings.Closes #180
Closes #197
Closes #189
Root cause
Deterministic and cross-platform (not environment-specific). The Local Models panel fetched its route root with a trailing slash —
call("/")→GET/POST /settings/local/— but the server mountsLocalModelsRoutessuch that Hono's.get("/")/.post("/")match/settings/localbut not/settings/local/. The trailing-slash request matched no route, fell through to the catch-all.all("/*"), which returned the SPAindex.htmlwith HTTP 200 +text/html. The frontend'ssettingsApithen ranres.json()on<!doctype html>→ the crash. Because the panel threw mid-load, the surrounding Settings UI wedged ("can't save settings", #197).It only ever hit Local Models because that was the only panel fetching its route root with a trailing slash; every other settings panel uses the generated SDK or sub-paths. (#189 is the same bug on the add-provider
POST /settings/local/.)Verified live:
GET /settings/local→ 200 JSON, butGET /settings/local/→ miss → (onmain) 200 HTML.The fix (functional fix + two defense-in-depth nets)
Functional fix — make the trailing-slash form resolve:
server.ts: main app is nownew Hono({ strict: false }), so/x/≡/xfor all routes (propagates through.route()to mounted sub-apps).LocalModels.tsx: all root call sitescall("/")→call(""), so the panel sends the canonical/settings/localpath (0call("/")remain).Defense-in-depth (so any future routing miss fails gracefully, never crashes):
server.tscatch-all: unmatched API-shaped requests (detected by a purewantsJson(accept, contentType)predicate) now return a JSON404 {"error":"not_found","path":…}instead of 200 SPA HTML. Browser navigations still getindex.html+ CSP unchanged;/api/*misses still 404.settingsApi: throws a descriptive error if a 2xx response isn't JSON, instead of the opaqueres.json()SyntaxError.Tests
test/web/serve.test.ts—wantsJsontruth table (6 cases).test/server/spa-fallback.test.ts— end-to-end viaServer.internalFetch():/settings/nonexistent→ JSON 404; navigation → 200 HTML + CSP header;/api/*→ 404; and/settings/local/(trailing slash) → 200 with apresetsarray (proves it reached the real handler, not the catch-all or the 404 net).settings/api.test.ts— non-JSON-200 → descriptive error (notUnexpected token); happy-path, 204, and non-ok cases.Verification
/settings/local,/detect,/status) all return JSON, no crash;/settings/local/and/settings/localboth → 200 JSON.Notes / optional follow-ups (non-blocking)
/api/*miss still returnstext/plain"404 Not Found" (pre-existing) rather than the structured JSON 404; could be unified for a consistent API-404 contract./api/short-circuit remains insideserveWebAsset(the catch-all handles/api/first).