Skip to content

fix(express): content-negotiated 404 for unmatched routes (#3975)#3977

Merged
PierreBrisorgueil merged 2 commits into
masterfrom
fix/3975-express-404-content-negotiation
Jul 17, 2026
Merged

fix(express): content-negotiated 404 for unmatched routes (#3975)#3977
PierreBrisorgueil merged 2 commits into
masterfrom
fix/3975-express-404-content-negotiation

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What

The Express catch-all route (app.get('/{*path}')) sent the friendly "Devkit Node Api" HTML snippet with an implicit HTTP 200 for every unmatched path — including any /api/* typo. API consumers and automated agents that rely on status codes for error handling saw false-positive success responses with an HTML body.

Why

Unknown routes should return a proper 404, content-negotiated for the caller: JSON for API/JSON-accepting clients, minimal HTML for browsers. A downstream consumer reported this.

Change

In lib/services/express.js (initModulesServerRoutes), after the config.files.routes loop:

  1. Register an explicit app.get('/', ...) first — keeps the friendly root HTML page at 200 (only reached when static hosting served no index.html). This must come before the catch-all since Express 5's /{*path} also matches /.
  2. Replace the old catch-all body: unmatched paths now return 404 { error: 'not_found' } JSON when the path starts with /api or the request explicitly accepts application/json; otherwise a minimal HTML 404.

Non-GET methods are unaffected (Express's default 404 already applied there — out of scope).

Tests

New lib/services/tests/express.notfound.unit.tests.js (supertest + mocked deps, following the existing express.errorroutes.unit.tests.js pattern):

  • GET /api/nope → 404 JSON { error: 'not_found' }
  • GET /api (no trailing slash) → 404 JSON
  • GET /nope with Accept: text/html → 404 HTML, no JSON
  • GET /nope with Accept: application/json → 404 JSON
  • GET / → 200, root page unchanged

Verify

  • npm run lint — clean on changed files
  • npm run test:unit — 165 suites / 2207 tests passing (full unit suite, including the 5 new tests)

Closes #3975

Summary by CodeRabbit

  • Bug Fixes
    • Unknown API routes now return a proper JSON 404 response instead of the landing page.
    • Unmatched browser routes now return an HTML 404 page with the correct status.
    • Requests accepting JSON receive consistent JSON 404 responses.
    • The root landing page continues to load normally.

The catch-all route previously sent the friendly "Devkit Node Api" HTML
snippet with an implicit 200 for every unmatched path, including /api/*
typos. API consumers and automated agents relying on status codes for
error handling saw false-positive success responses.

Register the root route explicitly (keeps the friendly HTML at 200,
only reached when static hosting served no index.html) before the
catch-all, since Express 5's `/{*path}` also matches `/`. The catch-all
now returns 404 with JSON `{ error: 'not_found' }` for /api/* paths or
when the request explicitly accepts application/json, and a minimal
HTML 404 otherwise. Non-GET methods are unaffected (already 404 by
default).

Adds unit tests covering: unknown /api path -> JSON 404, /api without
trailing slash -> JSON 404, unknown page with Accept: text/html -> HTML
404, unknown page with Accept: application/json -> JSON 404, and root
route unchanged at 200.
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 37 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fd807ce4-bf45-4cf6-ac8f-4707db5df6a0

📥 Commits

Reviewing files that changed from the base of the PR and between 2327870 and 446ae2c.

📒 Files selected for processing (4)
  • lib/services/express.js
  • lib/services/tests/express.notfound.unit.tests.js
  • modules/auth/tests/auth.authorization.integration.tests.js
  • modules/core/tests/core.integration.tests.js

Walkthrough

Express routing now serves the landing page only at / and returns HTTP 404 responses for unmatched routes, using JSON for API or JSON-accepting requests and HTML otherwise. Tests cover each response mode and preserve root behavior.

Changes

Not-found routing

Layer / File(s) Summary
Root and catch-all response handling
lib/services/express.js
The root route serves the existing landing page, while unmatched API or JSON requests return JSON not_found responses and other unmatched requests return HTML 404 responses.
Content-negotiated route tests
lib/services/tests/express.notfound.unit.tests.js
A mocked Express test app verifies API 404s, JSON negotiation, HTML 404 output, and the unchanged root response.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: Express now returns content-negotiated 404s for unmatched routes.
Description check ✅ Passed The description is mostly complete, covering what changed, why, tests, and verification, despite not matching the template headings exactly.
Linked Issues check ✅ Passed The code and tests match #3975: unknown routes now return 404 JSON for /api or JSON clients, HTML otherwise, with root behavior preserved.
Out of Scope Changes check ✅ Passed The changes are limited to the Express route behavior and corresponding tests, with no obvious unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3975-express-404-content-negotiation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.12%. Comparing base (319a675) to head (446ae2c).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #3977   +/-   ##
=======================================
  Coverage   93.11%   93.12%           
=======================================
  Files         170      170           
  Lines        5610     5615    +5     
  Branches     1807     1808    +1     
=======================================
+ Hits         5224     5229    +5     
  Misses        311      311           
  Partials       75       75           
Flag Coverage Δ
integration 61.28% <83.33%> (-0.01%) ⬇️
unit 75.12% <100.00%> (+0.07%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d89f05c...446ae2c. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@PierreBrisorgueil

Copy link
Copy Markdown
Contributor Author

The content-negotiated 404 fix is correct, but it exposed 3 integration tests that had baked in the old buggy 200-catch-all behavior. Updated them to assert the new (correct) contract instead of weakening the fix:

modules/core/tests/core.integration.tests.js — "should NOT serve a dedicated Redoc UI on /api/docs (decommissioned)"

  • Old: asserted GET /api/docs → 200 with the generic catch-all landing HTML.
  • New: asserts GET /api/docs → 404 JSON { error: 'not_found' }. This is a stronger "decommissioned" assertion — the route simply doesn't exist anymore, rather than silently falling through to a landing page.

modules/auth/tests/auth.authorization.integration.tests.js — guest public-route checks

  • Old: GET /api/home/releases and GET /api/home/changelogs expected 200 — but neither route exists in modules/home/routes/home.route.js. They only passed via the old implicit 200 catch-all, which is exactly the false positive this PR fixes.
  • New: repointed to real public routes so the "guest can reach a public endpoint" intent still means something: GET /api/health → 200, and GET /api/home/pages/terms → 200 (both confirmed to allow unauthenticated guest access; /api/home/team was already covered by an adjacent test, so it wasn't reused here).

Nit fixes also applied per review:

  • lib/services/express.js: isApiPath regex is now case-insensitive (/^\/api(\/|$)/i), since Express itself routes case-insensitively — /API/nope now correctly gets the JSON 404 instead of falling through to HTML.
  • lib/services/tests/express.notfound.unit.tests.js: added cases for requests with no explicit Accept header (the supertest/curl/agent default) on both an /api/* path and a non-API path, plus a case confirming the case-insensitive /API/* match.

Results: npm run test:unit — 164 suites / 2207 tests green. npm run test:integration — 39 suites / 491 tests green. Lint clean.

@PierreBrisorgueil
PierreBrisorgueil merged commit d34528c into master Jul 17, 2026
6 checks passed
@PierreBrisorgueil
PierreBrisorgueil deleted the fix/3975-express-404-content-negotiation branch July 17, 2026 14:47
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.

🐛 Express catch-all returns 200 HTML for unknown paths — serve content-negotiated 404

1 participant