Skip to content

fix(blog-app): Angular SSR not-found route returns 200 instead of 404 #216

Description

@dalenguyen

Context

Follow-up to #210 (PR #213, hotfixed further in commit c6fdad5 on dev).

While verifying #210's fix in production, found that Angular's SSR
"page not found" route (apps/blog-app/src/app/routes/[...page-not-found].ts)
returns HTTP 200 for any unmatched path, not 404. This means:

  • The /api/v1/** catch-all added in chore(blog-app): block Cloud Run env/secret scrapers (low-hanging fruit) #210 now correctly 404s anything under
    our own API namespace (verified live: /api/v1/env, /api/v1/config
    404).
  • But real bot-scanner traffic (sampled from Cloud Run logs, 2026-07-04
    23:04–23:49 UTC) overwhelmingly hits paths outside that namespace:
    /.env, /config.php, /backend/.env, /.env.production,
    /api/.env (one level, not under /v1/), /config.json, /js/config.js,
    etc. — all still return 200 today because they fall through to the SSR
    not-found handler.
  • A file-based catch-all can't fix this from server/routes/**: Nitro
    registers the SSR fallback as the literal wildcard pattern /**, and
    anything we register at that exact same top-level pattern gets silently
    overwritten (confirmed empirically — see chore(blog-app): block Cloud Run env/secret scrapers (low-hanging fruit) #210's PR discussion). The only
    namespaces we can safely claim are ones that don't collide with the
    reserved root wildcard (e.g. /v1/**), which by definition can't cover
    paths outside our own route prefix.

Real fix

Make the Angular SSR not-found route actually set a 404 status instead of
200. This is the same underlying class of issue as #211 (Nitro/Analog SSR
routing not matching plain HTTP semantics) — likely requires setting the
response status in [...page-not-found].ts via Angular's server-side
RESPONSE_INIT / HttpStatusCode injection token (Analog/Angular Universal
supports setting status codes from a route resolver), so the same component
that renders the "not found" UI also emits a 404 status code before Nitro
sends the response.

Acceptance

  • Any unmatched path (bare, /api/* outside v1/, or otherwise) returns 404
    from a live curl -o /dev/null -w '%{http_code}' check.
  • The rendered "not found" page content is unchanged for real users
    (same UI, just a correct status code).
  • Re-run the bot-probe log query from chore(blog-app): block Cloud Run env/secret scrapers (low-hanging fruit) #210 a few days after deploy and
    confirm the paths that used to 200 now show 404 in
    httpRequest.status.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions