more tests and better metrics
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, describe, expect, test } from "bun:test";
|
||||
import { clearRequestRateLimitsForTests } from "../rateLimit";
|
||||
import { createHttpFetchHandler } from "./http-dispatch";
|
||||
import { createHttpFetchHandler, normalizeHttpRoute } from "./http-dispatch";
|
||||
|
||||
const metrics = {
|
||||
withHttpMetrics: (fn: () => Promise<Response>) => fn(),
|
||||
@@ -35,3 +35,22 @@ describe("createHttpFetchHandler rate limiting", () => {
|
||||
expect(limited.headers.get("Retry-After")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("normalizeHttpRoute", () => {
|
||||
test("collapses dynamic route segments", () => {
|
||||
const req = new Request(
|
||||
"https://localhost/api/clients/0a44a8d4d61c7840540b43fd187fe3c43e2c46c9600b2aa1a61a6b1ab03c5b92/rd/ws?cache=1",
|
||||
{ method: "POST" },
|
||||
);
|
||||
|
||||
expect(normalizeHttpRoute(req, new URL(req.url))).toBe(
|
||||
"POST /api/clients/:id/rd/ws",
|
||||
);
|
||||
});
|
||||
|
||||
test("groups static asset files", () => {
|
||||
const req = new Request("https://localhost/assets/metrics.js");
|
||||
|
||||
expect(normalizeHttpRoute(req, new URL(req.url))).toBe("GET /assets/:file");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,9 @@ import { consumeUnauthorizedRateLimit } from "../rateLimit";
|
||||
|
||||
export type RouteHandler = (req: Request, url: URL, server: unknown) => Promise<Response | null>;
|
||||
|
||||
const UUID_SEGMENT = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
const LONG_ID_SEGMENT = /^[0-9a-f]{16,}$/i;
|
||||
|
||||
function tooManyRequestsResponse(retryAfter = 60): Response {
|
||||
return new Response("Too many requests", {
|
||||
status: 429,
|
||||
@@ -13,17 +16,35 @@ function tooManyRequestsResponse(retryAfter = 60): Response {
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeRouteSegment(segment: string): string {
|
||||
if (/^\d+$/.test(segment)) return ":id";
|
||||
if (UUID_SEGMENT.test(segment) || LONG_ID_SEGMENT.test(segment)) return ":id";
|
||||
if (segment.includes(".") && !segment.startsWith(".")) return ":file";
|
||||
if (segment.length >= 24 && /^[a-z0-9_-]+$/i.test(segment)) return ":id";
|
||||
return segment;
|
||||
}
|
||||
|
||||
export function normalizeHttpRoute(req: Request, url: URL): string {
|
||||
const path = url.pathname
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map(normalizeRouteSegment)
|
||||
.join("/");
|
||||
return `${req.method.toUpperCase()} /${path}`;
|
||||
}
|
||||
|
||||
export function createHttpFetchHandler(deps: {
|
||||
metrics: { withHttpMetrics: (fn: () => Promise<Response>) => Promise<Response> };
|
||||
metrics: { withHttpMetrics: (fn: () => Promise<Response>, route?: string) => Promise<Response> };
|
||||
CORS_HEADERS: Record<string, string>;
|
||||
routes: RouteHandler[];
|
||||
}) {
|
||||
return async function fetchHandler(req: Request, server: unknown): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const routeLabel = normalizeHttpRoute(req, url);
|
||||
return deps.metrics.withHttpMetrics(async () => {
|
||||
if (req.method === "OPTIONS") {
|
||||
return new Response("", { headers: deps.CORS_HEADERS });
|
||||
}
|
||||
const url = new URL(req.url);
|
||||
const wrapped = wrapServerWithClientIp(server as RequestServerLike);
|
||||
const ip = wrapped.requestIP(req)?.address || "unknown";
|
||||
|
||||
@@ -38,6 +59,6 @@ export function createHttpFetchHandler(deps: {
|
||||
}
|
||||
}
|
||||
return new Response("Not found", { status: 404 });
|
||||
});
|
||||
}, routeLabel);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -330,7 +330,11 @@ export async function handleMiscRoutes(
|
||||
snapshot.sessions.fileBrowser = deps.getFileBrowserSessionCount();
|
||||
snapshot.sessions.process = deps.getProcessSessionCount();
|
||||
|
||||
const history = metrics.getHistory();
|
||||
const requestedHistoryLimit = Number(url.searchParams.get("historyLimit") || 240);
|
||||
const historyLimit = Number.isFinite(requestedHistoryLimit)
|
||||
? Math.max(1, Math.min(2000, requestedHistoryLimit))
|
||||
: 240;
|
||||
const history = metrics.getHistory().slice(-historyLimit);
|
||||
|
||||
return new Response(JSON.stringify({ snapshot, history }), {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
|
||||
Reference in New Issue
Block a user