Skip to content

[API] Reject unknown routes, non-GET SPA fallback, and lenient integer parsing - #168

Merged
LarsLaskowski merged 2 commits into
mainfrom
claude/issue-137-ut1epb
Aug 16, 2026
Merged

[API] Reject unknown routes, non-GET SPA fallback, and lenient integer parsing#168
LarsLaskowski merged 2 commits into
mainfrom
claude/issue-137-ut1epb

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Aug 16, 2026

Copy link
Copy Markdown
Owner

📖 Description

Three small, related hygiene gaps in the server:

  1. Unknown /api/* routes returned SPA HTML with 200. A request like GET /api/nope fell through the API router, missed express.static, and landed in the catch-all sendFile(index.html), so a caller expecting JSON got a 200 text/html response instead of a 404.
  2. The SPA fallback answered every HTTP method. Because it was registered with app.use, POST /, PUT /whatever, DELETE /x all received the HTML shell with 200 instead of an error status.
  3. Lenient integer parsing. Number.parseInt('50abc', 10) returns 50, so limit=50abc / afterId=7xyz were silently accepted by /api/bootstrap and /api/resync, and PORT=9001x was silently accepted by clampInteger in config.ts.

Fixes:

  • createApiRouter (src/server/routes.ts) now ends with a terminal router.use(...) handler that returns a JSON 404 { "error": "Not found" } for any method/path under /api that no route above matched.
  • The SPA fallback middleware is extracted into a new createSpaFallback() (src/server/spaFallback.ts) so it is independently unit-testable; it now answers only GET/HEAD and returns a plain 404 (no body) for other methods. src/server/index.ts now wires this in instead of an inline handler.
  • A new parseStrictNonNegativeInt() helper in routes.ts requires the raw query value to match /^\d+$/ before parsing, and is used by parseSyncLimit/parseAfterId. clampInteger() in src/server/config.ts applies the same pre-check (with a [config] warning) before falling back to the default, so an invalid env value is rejected rather than truncated.

🎫 Issues

Closes #137

👩‍💻 Reviewer Notes

  • The changes are intentionally narrow parsing/routing hygiene fixes with no change to valid request/response shapes.
  • createSpaFallback() mirrors the existing createHostValidator() pattern (small middleware factory extracted into its own module) so the previously-untested inline catch-all in index.ts can be exercised directly.
  • Worth double-checking: POST /api/health now hits the new terminal 404 handler (covered by a new test) since only GET was ever registered for that route.

📑 Test Plan

✅ Checklist

General

  • I have added unit tests for my changes, per UNIT_TESTS.md.
  • I have run npm test and npm run typecheck locally and they pass.
  • I have updated the project documentation (README, ARCHITECTURE, etc.) to reflect my changes, where applicable.
  • I have read the CONTRIBUTING documentation and followed the project's code style and conventions.

Server-specific (src/server)

  • I have added or updated a module in src/server (tailer, parser, buffer, routes, SSE hub, config, etc.).
  • I have kept src/server/types.ts in sync with src/client/state.ts for any shared payload change (see ARCHITECTURE.md) — not applicable, no shared payload shapes changed.
  • I have preserved existing server-side bounds and defaults (buffer/SSE limits, poll interval clamps) unless the change explicitly targets them.

⏭ Next Steps

None.

…ient integer parsing (#137)

- createApiRouter now ends with a terminal handler returning a JSON 404
  for any method/path under /api that no route matched, instead of
  falling through to the SPA HTML.
- Extract the SPA fallback into createSpaFallback() so it can be unit
  tested; it now answers only GET/HEAD and returns a plain 404 for
  other methods (POST/PUT/DELETE/...).
- parseSyncLimit/parseAfterId in routes.ts and clampInteger in
  config.ts now reject values with trailing garbage (e.g. "50abc",
  "9001x") instead of silently truncating them with parseInt.
Comment thread src/server/spaFallback.test.ts Fixed
Mirror the production wiring in index.ts, where a rate limiter always
sits in front of the SPA fallback's sendFile() call, so the test app
does not expose an unbounded filesystem-reading handler.
@sonarqubecloud

Copy link
Copy Markdown

@LarsLaskowski
LarsLaskowski merged commit 9ba92e8 into main Aug 16, 2026
10 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/issue-137-ut1epb branch August 16, 2026 13:18
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.

API hygiene: unknown /api/* routes return SPA HTML with 200, fallback answers all HTTP methods, numeric params accept trailing garbage

3 participants