Skip to content

fix(express): catch-all 404 covers every HTTP verb, not just GET - #4010

Merged
PierreBrisorgueil merged 3 commits into
masterfrom
fix/3978-catch-all-404-all-verbs
Aug 3, 2026
Merged

fix(express): catch-all 404 covers every HTTP verb, not just GET#4010
PierreBrisorgueil merged 3 commits into
masterfrom
fix/3978-catch-all-404-all-verbs

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Problem

The content-negotiated 404 catch-all introduced for unmatched routes was registered with app.get('/{*path}', ...) only. initErrorRoutes registers a 4-arity error handler, which only fires on next(err), never on an unmatched route. So unmatched POST / PUT / PATCH / DELETE requests fell through to Express's default finalhandler and returned HTML (Cannot POST /api/...) instead of the intended JSON 404 — contradicting the whole point of the change (API consumers get a proper JSON error instead of a false-positive success/wrong-shaped error).

Fix

  • lib/services/express.js — register the catch-all with app.all instead of app.get, so every unmatched verb gets the same content-negotiated 404. The explicit app.get('/') root route is registered before it and is unaffected.
  • app.all also matches OPTIONS. That's safe: the cors middleware is mounted earlier in the middleware chain (initMiddleware, before initModulesServerRoutes) and always answers a preflight request itself without calling next(), so a real preflight never reaches the catch-all. Verified by two tests, not assumed (see below).
  • MIGRATIONS.md — the entry the original catch-all change never got: unmatched paths no longer return an implicit 200, and any readiness/health check must target a real declared route (e.g. GET /api/health).

Tests

lib/services/tests/express.notfound.unit.tests.js:

  • POST / PUT / PATCH / DELETE / OPTIONS (no cors mounted) on both an API path and a non-API path — the 8 pre-existing tests were all GET, so this closes the verb-coverage gap directly.
  • A dedicated test mounts the real cors middleware (not a mock) ahead of the catch-all, in the same order and config production uses, and sends a real preflight (Origin + Access-Control-Request-Method) — asserting cors answers it (200, Access-Control-Allow-Methods set) rather than the catch-all's JSON 404 body.

Full unit suite: 167 suites / 2300 tests passing. Lint clean.

Closes #3978

Summary by CodeRabbit

  • Bug Fixes

    • Unmatched routes now return consistent, content-negotiated 404 responses across all HTTP methods.
    • CORS preflight requests continue to receive proper OPTIONS handling.
    • Undeclared paths no longer return implicit success responses, improving readiness and health-check accuracy.
  • Documentation

    • Added migration guidance for the updated unmatched-route and health-check behavior.

app.get('/{*path}') left unmatched POST/PUT/PATCH/DELETE falling
through to Express's default HTML finalhandler, contradicting the
content-negotiated JSON 404 introduced for GET in #3975. Register the
catch-all with app.all instead so every unmatched verb gets the same
negotiated 404.

Adds verb coverage (POST/PUT/PATCH/DELETE on API and non-API paths)
and two OPTIONS tests proving, by execution rather than assumption,
that a real preflight is still answered by cors and never reaches the
catch-all. Also adds the MIGRATIONS.md entry the original #3975 change
was missing.

Closes #3978
Merge getApp/getAppWithCors into one getApp({ withCors }) builder and
fold the OPTIONS-without-cors case into the existing verb matrix
instead of a near-duplicate standalone test. No behavior change —
same 19 assertions, less repetition.
@coderabbitai

coderabbitai Bot commented Aug 3, 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: 45 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 Plus

Run ID: b3fac5c5-8b3a-4399-8489-515231c7ff22

📥 Commits

Reviewing files that changed from the base of the PR and between 4ff91d9 and 442f41a.

📒 Files selected for processing (3)
  • MIGRATIONS.md
  • lib/services/express.js
  • lib/services/tests/express.notfound.unit.tests.js

Walkthrough

The unmatched Express route now uses app.all to return negotiated 404 responses for all HTTP verbs. CORS continues to handle OPTIONS preflight requests. Tests and migration notes cover the updated behavior.

Changes

Unmatched route 404 handling

Layer / File(s) Summary
All-verb catch-all route
lib/services/express.js
The unmatched-route handler uses app.all for JSON and HTML 404 responses. CORS continues to handle OPTIONS requests first.
Route coverage and migration notes
lib/services/tests/express.notfound.unit.tests.js, MIGRATIONS.md
Tests cover non-GET verbs, content negotiation, and CORS preflight handling. Migration notes describe the new behavior and the need to use declared routes for readiness and health checks.

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

Possibly related issues

Possibly related PRs

  • pierreb-devkit/Node#3977: This PR extends that PR’s content-negotiated catch-all 404 handling from GET to all HTTP methods.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: extending the Express catch-all 404 handler to every HTTP verb.
Description check ✅ Passed The description clearly explains the problem, fix, affected modules, CORS behavior, tests, lint status, and related issue, but it does not use the repository template headings.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3978-catch-all-404-all-verbs

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 Aug 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.52%. Comparing base (5d41fe2) to head (442f41a).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #4010   +/-   ##
=======================================
  Coverage   93.52%   93.52%           
=======================================
  Files         170      170           
  Lines        5744     5744           
  Branches     1843     1843           
=======================================
  Hits         5372     5372           
  Misses        302      302           
  Partials       70       70           
Flag Coverage Δ
integration 61.68% <50.00%> (ø)
unit 76.06% <100.00%> (ø)

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 c5feb80...442f41a. 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@lib/services/express.js`:
- Around line 291-297: Add a JSDoc header immediately before the catch-all
app.all handler, describing its purpose, documenting the req and res parameters,
and specifying the response return value. Update the handler to return the HTML
response as well as the existing JSON response so its implementation matches the
documented return behavior.

In `@lib/services/tests/express.notfound.unit.tests.js`:
- Around line 21-27: Update the JSDoc headers for both helper functions,
including mockCommonDeps and the helper around the second referenced block, to
use a single concise description line. Move the existing detailed setup
narrative into regular comments above or within each function while preserving
the JSDoc requirement.

In `@MIGRATIONS.md`:
- Around line 9-11: Update the migration note to state that only unmatched GET
requests previously returned the implicit 200, while unmatched POST, PUT, PATCH,
and DELETE requests already returned Express’s 404. Limit the
readiness/health-check guidance to undeclared GET paths, replacing “any
unmatched path” and “regardless of HTTP verb” accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 69bcdf8d-0b97-4618-8ee5-ebccd63e8aa8

📥 Commits

Reviewing files that changed from the base of the PR and between c5feb80 and 4ff91d9.

📒 Files selected for processing (3)
  • MIGRATIONS.md
  • lib/services/express.js
  • lib/services/tests/express.notfound.unit.tests.js

Comment thread lib/services/express.js
Comment thread lib/services/tests/express.notfound.unit.tests.js
Comment thread MIGRATIONS.md Outdated
- Add the required JSDoc header to the catch-all 404 handler
  (documents req/res/return, and returns the HTML response too so
  both branches match the documented return type).
- Trim mockCommonDeps/getApp JSDoc to one-line descriptions per the
  repo's JSDoc convention, moving narrative detail to plain comments.
- Correct MIGRATIONS.md: the implicit-200-to-404 status flip only
  ever applied to unmatched GET (#3975); unmatched POST/PUT/PATCH/
  DELETE already 404'd via Express's default handler before this PR
  — what changes here is the response shape (now content-negotiated),
  not the status code, for those verbs.
@PierreBrisorgueil
PierreBrisorgueil merged commit 257a061 into master Aug 3, 2026
8 checks passed
@PierreBrisorgueil
PierreBrisorgueil deleted the fix/3978-catch-all-404-all-verbs branch August 3, 2026 09:30
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 404 covers GET only — other verbs fall through to Express's default HTML

1 participant