2026-09-14 20:11:46 +00:00
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
|
import { createStaticHandler } from "react-router";
|
|
|
|
|
import { shouldReportServerError } from "../error-reporting";
|
|
|
|
|
|
|
|
|
|
const ORIGIN = "https://brackt.com";
|
|
|
|
|
|
|
|
|
|
/** Shaped like the ErrorResponse React Router hands to `handleError`. */
|
|
|
|
|
function routeError(
|
|
|
|
|
status: number,
|
|
|
|
|
internal: boolean,
|
|
|
|
|
statusText = "Not Found",
|
|
|
|
|
) {
|
|
|
|
|
return {
|
|
|
|
|
status,
|
|
|
|
|
statusText,
|
|
|
|
|
internal,
|
|
|
|
|
data: `Error: No route matches URL "/blog/wp/v2/posts/999999"`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function request(path: string, referer?: string, method = "GET") {
|
|
|
|
|
return new Request(`${ORIGIN}${path}`, {
|
|
|
|
|
method,
|
|
|
|
|
headers: referer ? { referer } : {},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("shouldReportServerError", () => {
|
|
|
|
|
it("drops a router 404 for a scanner hitting a URL cold", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(404, true),
|
|
|
|
|
request("/blog/wp/v2/posts/999999"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("drops a router 404 linked from another site", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(404, true),
|
|
|
|
|
request("/blog/", "https://evil.example/"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reports a router 404 linked from one of our own pages", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(404, true),
|
|
|
|
|
request("/leagues/gone", `${ORIGIN}/leagues`),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("drops the 405 from a POST to a route with no action", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(405, true, "Method Not Allowed"),
|
|
|
|
|
request("/", undefined, "POST"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reports a 404 the app threw deliberately", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(404, false),
|
|
|
|
|
request("/leagues/missing"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reports a 403 the app threw from an ownership check", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(403, false, "Forbidden"),
|
|
|
|
|
request("/admin/sports"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reports a router-internal 500", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(500, true, "Internal Server Error"),
|
|
|
|
|
request("/leagues"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reports a plain exception", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(new Error("boom"), request("/leagues")),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reports anything that is not a route error response", () => {
|
|
|
|
|
expect(shouldReportServerError("just a string", request("/leagues"))).toBe(
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
expect(shouldReportServerError(null, request("/leagues"))).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("drops a router 404 whose referer header is not a URL", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(404, true),
|
|
|
|
|
request("/blog/", "not a url"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The unit tests above use hand-written error objects. These drive real requests
|
|
|
|
|
* through React Router so the suite fails if the shape it throws ever changes.
|
|
|
|
|
*/
|
|
|
|
|
describe("shouldReportServerError against real React Router errors", () => {
|
|
|
|
|
const handler = createStaticHandler([
|
|
|
|
|
{
|
|
|
|
|
id: "root",
|
|
|
|
|
path: "/",
|
|
|
|
|
children: [{ id: "home", index: true, loader: () => null }],
|
|
|
|
|
},
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
async function errorFor(req: Request) {
|
|
|
|
|
const ctx = await handler.query(req);
|
|
|
|
|
if (ctx instanceof Response) return null;
|
|
|
|
|
return Object.values(ctx.errors ?? {})[0] ?? null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
it('drops the 404 for an unmatched URL (No route matches URL "...")', async () => {
|
|
|
|
|
const req = request("/blog/wp/v2/posts/999999");
|
|
|
|
|
const error = await errorFor(req);
|
|
|
|
|
expect(error).toMatchObject({ status: 404, internal: true });
|
|
|
|
|
expect(shouldReportServerError(error, req)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reports the same 404 when it came from a link on our own site", async () => {
|
|
|
|
|
const req = request("/nope", `${ORIGIN}/leagues`);
|
|
|
|
|
expect(shouldReportServerError(await errorFor(req), req)).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("drops the 405 from a POST to a route with no action", async () => {
|
|
|
|
|
const req = request("/", undefined, "POST");
|
|
|
|
|
const error = await errorFor(req);
|
|
|
|
|
expect(error).toMatchObject({ status: 405, internal: true });
|
|
|
|
|
expect(shouldReportServerError(error, req)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
2026-09-14 21:18:33 +00:00
|
|
|
|
|
|
|
|
describe("static asset 404s", () => {
|
|
|
|
|
it("drops a stale hashed bundle even with a same-host referer", () => {
|
|
|
|
|
// Every deploy leaves clients requesting the previous build's assets.
|
|
|
|
|
const req = request("/assets/index-OLDHASH.js", `${ORIGIN}/leagues`);
|
|
|
|
|
expect(shouldReportServerError(routeError(404, true), req)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it.each([
|
|
|
|
|
"/assets/app-x1.css",
|
|
|
|
|
"/fonts/inter.woff2",
|
|
|
|
|
"/images/logo.png",
|
|
|
|
|
"/favicon.ico",
|
|
|
|
|
])("drops a 404 for %s", (path) => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(404, true),
|
|
|
|
|
request(path, `${ORIGIN}/`),
|
|
|
|
|
),
|
|
|
|
|
).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("still follows the referer rule for a non-asset path containing a dot", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(404, true),
|
|
|
|
|
request("/leagues/v1.2", `${ORIGIN}/leagues`),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(routeError(404, true), request("/leagues/v1.2")),
|
|
|
|
|
).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("React Router internal statuses that are not 404/405", () => {
|
|
|
|
|
it("reports an internal 400 (route is missing a loader)", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(400, true, "Bad Request"),
|
|
|
|
|
request("/leagues"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("reports an internal 403 (route does not match URL)", () => {
|
|
|
|
|
expect(
|
|
|
|
|
shouldReportServerError(
|
|
|
|
|
routeError(403, true, "Forbidden"),
|
|
|
|
|
request("/leagues"),
|
|
|
|
|
),
|
|
|
|
|
).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("production shape: TLS terminated upstream", () => {
|
|
|
|
|
it("reports a 404 linked from our own site when the proxy strips https", () => {
|
|
|
|
|
// Express builds request.url from req.protocol, which is `http` inside the
|
|
|
|
|
// container. Real browsers send an https referer. Comparing full origins
|
|
|
|
|
// would never match, silencing every broken internal link.
|
|
|
|
|
const req = new Request("http://brackt.com/leagues/gone", {
|
|
|
|
|
headers: { referer: "https://brackt.com/leagues" },
|
|
|
|
|
});
|
|
|
|
|
expect(shouldReportServerError(routeError(404, true), req)).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("still drops a cold scanner hit under that same shape", () => {
|
|
|
|
|
const req = new Request("http://brackt.com/blog/wp/v2/posts/999999");
|
|
|
|
|
expect(shouldReportServerError(routeError(404, true), req)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("still drops a 404 linked from another site under that same shape", () => {
|
|
|
|
|
const req = new Request("http://brackt.com/nope", {
|
|
|
|
|
headers: { referer: "https://evil.example/" },
|
|
|
|
|
});
|
|
|
|
|
expect(shouldReportServerError(routeError(404, true), req)).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|