Automated scanners probing for WordPress paths (/blog/wp/v2/posts/*, POST /) generate a React Router 404 or 405 on every hit, and handleError forwarded all of them to Sentry, exhausting the quota. The existing defence was a list of ignoreErrors regexes that needed a new entry for each scanner pattern. Filter on what the error is instead: shouldReportServerError drops 4xx responses React Router generated itself (internal: true) for requests that matched nothing. Responses the app throws deliberately still report, as do all 5xx and real exceptions. A request carrying a same-origin Referer is still reported, so a broken internal link remains visible in Sentry -- only cold scanner hits are dropped. No HTTP response changes; every URL returns the status and page it did before. Also adds /blog to the Express bot-probe filter so the highest-volume path 404s without an SSR render, and removes the now-redundant route-404 ignoreErrors entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ESrxBMZMx2BD9rcKTCgLe4
49 lines
1.9 KiB
TypeScript
49 lines
1.9 KiB
TypeScript
/**
|
|
* Decides which server-side errors are worth sending to Sentry.
|
|
*
|
|
* Automated scanners probe for CMS paths that have never existed here
|
|
* (`/blog/wp/v2/posts/999999`, `/wp-login.php`, a bare `POST /`). React Router
|
|
* throws for each one — a 404 when no route matches, a 405 when a route has no
|
|
* `action` — and every throw reaches `handleError` in `app/entry.server.tsx`.
|
|
* Reporting those burns the Sentry quota without ever describing a real bug.
|
|
*/
|
|
import { isRouteErrorResponse } from "react-router";
|
|
|
|
/** React Router stamps `internal: true` on the errors it generates itself. */
|
|
function isInternalRouterError(error: unknown): boolean {
|
|
return (error as { internal?: unknown }).internal === true;
|
|
}
|
|
|
|
/** True when the request was linked from a page on this same origin. */
|
|
function hasSameOriginReferer(request: Request): boolean {
|
|
const referer = request.headers.get("referer");
|
|
if (!referer) return false;
|
|
try {
|
|
return new URL(referer).origin === new URL(request.url).origin;
|
|
} catch {
|
|
// Scanners send garbage in this header; a referer we can't parse isn't ours.
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether `error` should be reported to Sentry.
|
|
*
|
|
* Drops only the 4xx responses React Router generated for a request that
|
|
* matched nothing — that is, unrecognised URLs and methods. Everything else is
|
|
* reported, including responses the app threw deliberately (`internal: false`),
|
|
* so a 403 from an ownership check still shows up.
|
|
*
|
|
* The exception is a request carrying a same-origin `Referer`: a 404 reached
|
|
* from one of our own pages is a broken internal link, not a scanner, and stays
|
|
* visible in Sentry.
|
|
*/
|
|
export function shouldReportServerError(
|
|
error: unknown,
|
|
request: Request,
|
|
): boolean {
|
|
if (!isRouteErrorResponse(error)) return true;
|
|
if (!isInternalRouterError(error)) return true;
|
|
if (error.status < 400 || error.status > 499) return true;
|
|
return hasSameOriginReferer(request);
|
|
}
|