Skip to content

fix: attribute redirect-driven content misses to the requested path in the 404 log - #2305

Open
edrpls wants to merge 3 commits into
emdash-cms:mainfrom
edrpls:fix/404-log-error-page
Open

fix: attribute redirect-driven content misses to the requested path in the 404 log#2305
edrpls wants to merge 3 commits into
emdash-cms:mainfrom
edrpls:fix/404-log-error-page

Conversation

@edrpls

@edrpls edrpls commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes 404 logging for content misses — both the noise and a telemetry hole the noise was hiding.

The documented template pattern answers a content miss with return Astro.redirect("/404") (every demo and the site-building docs use it). That flow has two consequences for the 404 log:

  1. The first pass answers 302, which the logger (gated on response.status === 404) never saw — so the real missed path was never recorded.
  2. The browser then requests the literal /404, which Astro serves with status 404, and that got logged — so every content miss collapsed into one aggregate "/404" row. On the audited production deployment (Macabro festival site, emdash 0.31.1), that row carried 41% of 55k logged hits, burying the per-path signal while recording no usable path itself.

The fix logs the miss on the first pass, under the path the visitor actually requested: when a matched route answers with a redirect whose target is the error page (/404 or /404/), that request is a content miss and is logged as such. Direct renders of /404 itself are never logged — the route answers 404 by design and carries no path information. Fully-unmatched paths (bot scans, broken links to non-routes) keep logging exactly as before, since Astro renders the error page for them on the first pass with the real pathname.

Net effect for upgraders: the meaningless aggregate /404 row stops growing, and the log gains something it never had — the actual missed paths for content misses. Ordinary redirects (not targeting the error page) are not logged.

An earlier revision of this PR only excluded /404 from logging, on the assumption the missed path was already recorded on the first pass. Adversarial review of the branch proved that assumption wrong for the redirect flow (the first pass is a 302), which would have silently zeroed 404 telemetry for content misses — hence the current shape, which closes the hole instead. The test suite now models the full production sequence: 302-to-/404 followed by the /404 render asserts exactly one row, keyed by the real path (fails on main, which records "/404" instead), alongside tests for unmatched paths, ordinary redirects, and direct error-page visits.

Known limitations, deliberately out of scope: no base-path handling (consistent with the rest of the middleware, which matches raw pathnames everywhere), and locale-prefixed error pages (/fr/404) are not excluded for direct visits — excluding them properly needs the configured locale list, and the redirect flow is unaffected (templates redirect to the absolute /404). The pre-existing aggregate /404 row on already-deployed sites is retained (deleting it would be a data-deleting migration, worth a separate explicit decision).

Found during a measured database audit of a production deployment.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main. — n/a: no admin UI strings changed
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion — n/a: bug fix

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Fable 5 (Claude Code)

Screenshots / test output

The redirect-flow regression test fails on main (the miss is recorded as "/404" instead of the real path):

× logs a content miss under its real path across the redirect-to-/404 flow
  → expected log404 to be called with path "/posts/deleted-post"

After the fix — middleware suite plus redirect integration suites:

Tests  75 passed (75)

🤖 Generated with Claude Code

The redirect middleware logs any 404 response keyed by pathname, so hits
on the site's own /404 route were recorded as misses. emdash's content
templates answer a miss with Astro.redirect("/404"): the real path is
logged on the first pass, the browser follows the redirect, and the /404
render logs a second row — every real miss counted twice, with the
missed-path signal buried under one giant "/404" entry (41% of 55k rows
on an audited production log).

Skip logging when the pathname is /404 (or /404/ for trailing-slash
sites). The error route returns 404 by design on every visit, so a row
for it carries no information, and the redirect flow already captured
the real missed path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4378733

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jul 31, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@2305

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@2305

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@2305

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@2305

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@2305

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@2305

emdash

npm i https://pkg.pr.new/emdash@2305

create-emdash

npm i https://pkg.pr.new/create-emdash@2305

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@2305

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@2305

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@2305

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@2305

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@2305

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@2305

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@2305

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@2305

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@2305

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@2305

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@2305

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@2305

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@2305

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@2305

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@2305

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@2305

commit: 4378733

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet cla: signed labels Jul 31, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the right fix for a real production issue: EmDash templates answer content misses with Astro.redirect("/404"), so Astro's /404 route renders with status 404 on the follow-up request and was being logged as a missed path, inflating the 404 summary. Filtering on pathname is the correct signal — filtering by routePattern would discard the genuinely valuable first-pass rows.

What I checked:

  • The diff, the full middleware, the redirect repository, the 404-log migration (035), and the test file.
  • Query-impact: the change is a pathname string comparison; it adds no DB round-trips on the logged-out hot path.
  • Test quality: a failing regression test is added and the assertions exercise observable behavior (no log404 call, empty DB table).
  • Conventions: changeset is present and user-facing; no new admin UI strings; no SQL interpolation issues.

The implementation is sound. I have two non-blocking suggestions: the new comment is verbose and contains narrative/justification that AGENTS.md discourages, and the exclusion is so narrow that it misses Astro-localized error pages (/fr/404, /es/404) in EmDash's i18n-routed fixtures/templates. The latter doesn't affect the reported redirect-loop bug (templates redirect to the absolute /404), but it's worth recording if direct visits to localized 404 pages should also be suppressed.

Comment on lines +115 to +119
// Log 404s for unmatched paths (fire-and-forget). The site's own error
// page is excluded: its route answers 404 on every render — including
// the follow-up request after a content miss redirects to /404 — so
// logging it counts every real miss twice under one meaningless
// "/404" row while the missed path is already logged on the first pass.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The comment explains a non-obvious "why" (the /404 route answers 404 by design), but it also restates the bug narrative from the PR description and justifies the fix with phrases like "meaningless" and "already logged on the first pass". Per AGENTS.md, comments should not be PR summaries or justification; they should give future readers only what the code can't say itself.

Suggested change
// Log 404s for unmatched paths (fire-and-forget). The site's own error
// page is excluded: its route answers 404 on every render — including
// the follow-up request after a content miss redirects to /404 — so
// logging it counts every real miss twice under one meaningless
// "/404" row while the missed path is already logged on the first pass.
// Log 404s for unmatched paths (fire-and-forget). Skip /404 itself — Astro
// serves the site error page there with status 404 on every render, so logging
// it would double-count the original miss after templates redirect here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Adopted your suggested wording verbatim — pushed in a636351.

// the follow-up request after a content miss redirects to /404 — so
// logging it counts every real miss twice under one meaningless
// "/404" row while the missed path is already logged on the first pass.
if (response.status === 404 && pathname !== "/404" && pathname !== "/404/") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The exclusion only handles /404 and /404/. EmDash's fixtures and demos configure Astro i18n routing with locales: ["en", "fr", "es"] and defaultLocale: "en", so Astro also generates locale-prefixed error pages at /fr/404 and /es/404. A direct visit to one of those still gets logged, which is inconsistent with the rationale for skipping the error page.

This does not affect the reported bug because templates redirect to the absolute path /404, but it's a gap worth considering. If you keep the current scope, consider adding a short comment or test noting the limitation.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed it's a real gap, and deliberately left out of this PR: excluding locale-prefixed error pages needs the configured locale list (the middleware currently does zero i18n/base handling anywhere, so an exact-path check keeps parity with the rest of the file), and a locale-shaped regex would guess. The reported amplification only involves the absolute /404 — templates redirect there regardless of locale. If maintainers want the localized variants excluded too, I'd wire getI18nConfig() into the middleware in a follow-up rather than widen this diff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review and removed review/needs-review No maintainer or bot review yet labels Jul 31, 2026
The template pattern for a content miss is Astro.redirect("/404"): the
first pass answers 302 — which the 404 logger never saw — and only the
follow-up /404 render logged, as one aggregate "/404" row. The real
missed path was never recorded, and excluding /404 alone would have
removed the only signal those misses had.

Log the miss on the first pass instead, keyed by the requested path,
when a route answers with a redirect whose target is the error page.
Direct hits on /404 stay unlogged — the route answers 404 by design and
carries no path information.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@edrpls edrpls changed the title fix: exclude the site error page from 404 logging fix: attribute redirect-driven content misses to the requested path in the 404 log Jul 31, 2026
@edrpls

edrpls commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Heads-up: this PR changed shape in 10d5bce — the original fix was wrong in a way worth being explicit about.

The first revision excluded /404 from logging on the assumption that the real missed path was "already logged on the first pass." Adversarial review of the branch disproved that for the documented content-miss flow: Astro.redirect("/404") answers 302 on the first pass, which the logger (gated on status === 404) never sees. The aggregate /404 row wasn't duplication — it was the only record those misses had. Shipping the exclusion alone would have silently zeroed 404 telemetry for content misses on every site built from the demos/docs.

The current shape closes the underlying hole instead: a redirect whose target is the error page is logged as a miss under the requested path on the first pass, and /404 renders are never logged. Upgraders lose the meaningless aggregate row and gain per-path miss attribution that never existed before. The new test models the full 302→/404 sequence and fails on main (which records "/404" instead of the real path). Title, body, comment, and changeset all restated to the true mechanism.

@edrpls
edrpls force-pushed the fix/404-log-error-page branch from 10d5bce to 4378733 Compare July 31, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core cla: signed review/needs-rereview Author pushed changes since the last review size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant