monitor.http-check-result@1.0.0
impl/typescript.ts
3,526 bytes · the TypeScript implementation · view raw
import { type CheckOutcome, type HttpExpectation, type HttpProbe } from "./monitor_http_check_result_types.ts";
const ERROR_MESSAGES: Readonly<Record<string, string>> = {
timeout: "timed out",
dns: "DNS lookup failed",
connection: "connection failed",
tls: "TLS handshake failed",
protocol: "protocol error",
};
function expectedText(statuses: readonly number[] | null): string {
if (statuses === null) return "2xx";
if (statuses.length === 1) return String(statuses[0]);
return `${statuses.slice(0, -1).join(", ")} or ${statuses[statuses.length - 1]}`;
}
function threshold(name: string, v: number | null): void {
if (v !== null && (!Number.isSafeInteger(v) || v < 1)) throw new RangeError(`${name} must be a whole number 1 or more, received ${v}`);
}
/**
* One probe, one verdict, checked in a fixed order so the reason is the most
* fundamental thing wrong: a transport error, then the status code, then the
* body, then latency. A 503 that also took ten seconds is reported as the
* 503, because that is what someone has to fix.
*/
export function classifyHttpCheck(probe: HttpProbe, expect: HttpExpectation): CheckOutcome {
const { statusCode, latencyMs, body, error } = probe;
if (statusCode !== null && (!Number.isSafeInteger(statusCode) || statusCode < 100 || statusCode > 599)) {
throw new RangeError(`statusCode must be 100 to 599, received ${statusCode}`);
}
if (error !== null && !(error in ERROR_MESSAGES)) throw new RangeError(`unknown probe error: ${String(error)}`);
if (statusCode === null && error === null) throw new RangeError("probe must have a statusCode or an error");
if (latencyMs !== null && (!Number.isSafeInteger(latencyMs) || latencyMs < 0)) {
throw new RangeError(`latencyMs must be a whole number 0 or more, received ${latencyMs}`);
}
const { statuses, bodyContains, degradedLatencyMs: degraded, downLatencyMs: down } = expect;
if (statuses !== null) {
if (statuses.length === 0) throw new RangeError("statuses must not be empty; use null to accept any 2xx");
for (const s of statuses) {
if (!Number.isSafeInteger(s) || s < 100 || s > 599) throw new RangeError(`expected statuses must be 100 to 599, received ${s}`);
}
}
threshold("degradedLatencyMs", degraded);
threshold("downLatencyMs", down);
if (degraded !== null && down !== null && down < degraded) {
throw new RangeError(`downLatencyMs must not be below degradedLatencyMs: ${down} < ${degraded}`);
}
if (error !== null) return { status: "down", reason: error, message: ERROR_MESSAGES[error] };
const code = statusCode as number;
const accepted = statuses === null ? code >= 200 && code <= 299 : statuses.includes(code);
if (!accepted) return { status: "down", reason: "unexpected-status", message: `HTTP ${code}, expected ${expectedText(statuses)}` };
if (bodyContains !== null && (body === null || !body.includes(bodyContains))) {
return { status: "down", reason: "body-mismatch", message: `body does not contain "${bodyContains}"` };
}
if (latencyMs !== null) {
if (down !== null && latencyMs >= down) {
return { status: "down", reason: "too-slow", message: `latency ${latencyMs} ms at or above ${down} ms` };
}
if (degraded !== null && latencyMs >= degraded) {
return { status: "degraded", reason: "slow", message: `latency ${latencyMs} ms at or above ${degraded} ms` };
}
}
return { status: "up", reason: "ok", message: latencyMs === null ? `HTTP ${code}` : `HTTP ${code} in ${latencyMs} ms` };
}