Skip to content

fix: Local Models settings crash — trailing-slash routing miss (#180 #197 #189)#199

Merged
KB (KB-syntheticsciences) merged 5 commits into
mainfrom
fix/settings-spa-fallback-json-404
Jul 22, 2026
Merged

fix: Local Models settings crash — trailing-slash routing miss (#180 #197 #189)#199
KB (KB-syntheticsciences) merged 5 commits into
mainfrom
fix/settings-spa-fallback-json-404

Conversation

@KB-syntheticsciences

Copy link
Copy Markdown
Collaborator

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 slashcall("/")GET/POST /settings/local/ — but the server mounts LocalModelsRoutes such that Hono's .get("/") / .post("/") match /settings/local but not /settings/local/. The trailing-slash request matched no route, fell through to the catch-all .all("/*"), which returned the SPA index.html with HTTP 200 + text/html. The frontend's settingsApi then ran res.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, but GET /settings/local/ → miss → (on main) 200 HTML.

The fix (functional fix + two defense-in-depth nets)

Functional fix — make the trailing-slash form resolve:

  • server.ts: main app is now new Hono({ strict: false }), so /x//x for all routes (propagates through .route() to mounted sub-apps).
  • LocalModels.tsx: all root call sites call("/")call(""), so the panel sends the canonical /settings/local path (0 call("/") remain).

Defense-in-depth (so any future routing miss fails gracefully, never crashes):

  • server.ts catch-all: unmatched API-shaped requests (detected by a pure wantsJson(accept, contentType) predicate) now return a JSON 404 {"error":"not_found","path":…} instead of 200 SPA HTML. Browser navigations still get index.html + CSP unchanged; /api/* misses still 404.
  • settingsApi: throws a descriptive error if a 2xx response isn't JSON, instead of the opaque res.json() SyntaxError.

Tests

  • test/web/serve.test.tswantsJson truth table (6 cases).
  • test/server/spa-fallback.test.ts — end-to-end via Server.internalFetch(): /settings/nonexistent → JSON 404; navigation → 200 HTML + CSP header; /api/* → 404; and /settings/local/ (trailing slash) → 200 with a presets array (proves it reached the real handler, not the catch-all or the 404 net).
  • settings/api.test.ts — non-JSON-200 → descriptive error (not Unexpected token); happy-path, 204, and non-ok cases.

Verification

  • Backend suite: 1142 pass / 0 fail (1 pre-existing env-gated skip). Frontend settings test 4/4. Typecheck clean.
  • Live on the branch: replaying the panel's three load requests (/settings/local, /detect, /status) all return JSON, no crash; /settings/local/ and /settings/local both → 200 JSON.

Notes / optional follow-ups (non-blocking)

  • The /api/* miss still returns text/plain "404 Not Found" (pre-existing) rather than the structured JSON 404; could be unified for a consistent API-404 contract.
  • A now-redundant /api/ short-circuit remains inside serveWebAsset (the catch-all handles /api/ first).

…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).
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openscience Ready Ready Preview, Comment Jul 21, 2026 6:10pm

Request Review

@KB-syntheticsciences

Copy link
Copy Markdown
Collaborator Author

Admin-merging past the failing Test check — rationale.

The red Test (bun test) check is a known external GitHub-runner regression, not a fault of this PR:

  • This branch's tests are green locally: full backend suite 1162 pass / 0 fail, plus a live before/after reproduction confirming the Local Models crash is fixed.
  • The Test job broke repo-wide on every PR since ~Jul 11, with no repo change — it passed for weeks on the same base commit and same Bun 1.3.5. The only delta is GitHub bumping the ubuntu-24.04 runner image 20260705.23220260714.240 (azure kernel -1018-1020), which breaks Bun's test runner (SIGILL / non-deterministic ESM-resolution failures). An unrelated dependabot PR fails identically.
  • Full diagnosis in ci: pin Test job to ubuntu-24.04 (bun test breaks on ubuntu-latest kernel 6.17) #200 (closed — not fixable from the repo; GitHub serves only the latest image patch, so the good .232 image can't be pinned, and bumping Bun fails differently).

Every other check passed. Merging on admin override; CI will recover on its own once GitHub ships a fixed image. Closes #180, #197, #189.

@KB-syntheticsciences
KB (KB-syntheticsciences) merged commit 083ef91 into main Jul 22, 2026
17 of 19 checks passed
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.

Clicking "Local models" page crashes with SyntaxError Error SyntaxError when clicking "Local Model" in settings

1 participant