Functional Weave
Code in Rust

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` };
}