Functional Weave
Code in TypeScript

monitor.http-check-result

Classify one HTTP probe as up, degraded or down, with a reason and message, from status, body and latency.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 39 tests, run in TypeScript, Python and Rust.

What it does

Turns what one HTTP probe came back with into a `CheckOutcome`: a `CheckStatus` (`up`, `degraded`, `down`, from `monitor.check-status`), a machine-readable `reason`, and a short English `message` for the alert. Store `{ at, status }` as a `Check` and the rest of `monitor.*` (uptime, incidents, status pages) works from it.

The prober itself (making the request, timing it) is the caller's I/O; this is the pure decision afterwards, so every language classifies a probe the same way.

For example

  • classifyHttpCheck(status code 200, latency ms 123, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms —) → status up, reason ok, message HTTP 200 in 123 ms a 200 with no conditions is up
  • classifyHttpCheck(status code 204, latency ms —, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms —) → status up, reason ok, message HTTP 204 a 204 without a latency measurement is up
  • classifyHttpCheck(status code 503, latency ms 40, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms —) → status down, reason unexpected-status, message HTTP 503, expected 2xx a 503 is down against the default any-2xx

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function classifyHttpCheck(probe: HttpProbe, expect: HttpExpectation): CheckOutcome
probeHttpProbewhat one request to the target came back with
expectHttpExpectationwhat counts as healthy
returnsCheckOutcome

The types it declares, generated into your project

export type ProbeError = "timeout" | "dns" | "connection" | "tls" | "protocol";

/** What one HTTP request came back with: a response, or the error that prevented one. */
export interface HttpProbe {
  /** the response status, 100 to 599; null when no response arrived */
  readonly statusCode: number | null;
  /** time to the full response in milliseconds, 0 or more; null when not measured */
  readonly latencyMs: number | null;
  /** the response body, or the part of it the prober kept */
  readonly body: string | null;
  /** why no usable response arrived; wins over everything else */
  readonly error: ProbeError | null;
}

/** What counts as a healthy response. */
export interface HttpExpectation {
  /** accepted status codes; null accepts any 2xx (the Prometheus blackbox_exporter default) */
  readonly statuses: readonly number[] | null;
  /** text the body must contain, case-sensitive */
  readonly bodyContains: string | null;
  /** at or above this the target is degraded, at least 1 */
  readonly degradedLatencyMs: number | null;
  /** at or above this the target is down, at least degradedLatencyMs */
  readonly downLatencyMs: number | null;
}

export type CheckReason = "ok" | "slow" | "too-slow" | "unexpected-status" | "body-mismatch" | "timeout" | "dns" | "connection" | "tls" | "protocol";

/** The status of one probe and why, ready to store as a Check and show in an alert. */
export interface CheckOutcome {
  readonly status: CheckStatus;
  readonly reason: CheckReason;
  /** short fixed English, e.g. "HTTP 503, expected 2xx" */
  readonly message: string;
}

Your code names it in one line, in the file that uses it

import { classifyHttpCheck } from "#fune/monitor.http-check-result@^1";
impl/typescript.ts · 66 lines · open · 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` };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add monitor.http-check-result
Download for TypeScript monitor.http-check-result-1.0.0-typescript.fune · 24,449 bytes sha256 8d886555b5f7e7f1a0408d51170372a663b78971c5b8e9ab305276e1eab8799f

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./monitor.http-check-result-1.0.0-typescript.fune, or fetch it from a terminal with fune pull monitor.http-check-result@1.0.0:typescript.

The whole function, every language, is one file too: monitor.http-check-result-1.0.0.fune, 35,051 bytes, sha256 86ccd509db6060458338a7b87a7c3b2049f7bbd1758e551bb3411a8dbea3b456. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before monitor.http-check-result

after — your function gets the result and the arguments, and returns the final result.

// fune: after monitor.http-check-result

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace monitor.check-status in monitor.http-check-result

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show monitor.http-check-result --steps.

// fune: step monitor.http-check-result after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a 200 with no conditions is up status code 200, latency ms 123, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → status up, reason ok, message HTTP 200 in 123 ms
a 204 without a latency measurement is up status code 204, latency ms —, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → status up, reason ok, message HTTP 204
a 503 is down against the default any-2xx status code 503, latency ms 40, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → status down, reason unexpected-status, message HTTP 503, expected 2xx
a redirect is not a 2xx: down unless listed status code 301, latency ms 12, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → status down, reason unexpected-status, message HTTP 301, expected 2xx
299 is still a 2xx status code 299, latency ms 5, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → status up, reason ok, message HTTP 299 in 5 ms
two accepted codes are listed with or status code 404, latency ms 30, body —, error —, statuses 200, 204, body contains —, degraded latency ms —, down latency ms — → status down, reason unexpected-status, message HTTP 404, expected 200 or 204
three accepted codes are listed with commas and or status code 503, latency ms 30, body —, error —, statuses 200, 201, 204, body contains —, degraded latency ms —, down latency ms — → status down, reason unexpected-status, message HTTP 503, expected 200, 201 or 204
a single accepted code status code 201, latency ms 30, body —, error —, statuses 200, body contains —, degraded latency ms —, down latency ms — → status down, reason unexpected-status, message HTTP 201, expected 200
an explicit list replaces 2xx: a 404 can be the healthy answer status code 404, latency ms 50, body —, error —, statuses 404, body contains —, degraded latency ms —, down latency ms — → status up, reason ok, message HTTP 404 in 50 ms
the body must contain the text status code 200, latency ms 80, body status: fail, error —, statuses —, body contains ok, degraded latency ms —, down latency ms — → status down, reason body-mismatch, message body does not contain "ok"
Show the other 29 tests
CaseArgumentsExpected
the body match is case-sensitive status code 200, latency ms 80, body OK, error —, statuses —, body contains ok, degraded latency ms —, down latency ms — → status down, reason body-mismatch, message body does not contain "ok"
no body at all does not contain the text status code 200, latency ms 80, body —, error —, statuses —, body contains ok, degraded latency ms —, down latency ms — → status down, reason body-mismatch, message body does not contain "ok"
a body containing the text is up status code 200, latency ms 80, body {"status":"ok"}, error —, statuses —, body contains "ok", degraded latency ms —, down latency ms — → status up, reason ok, message HTTP 200 in 80 ms
latency just under the degraded threshold is up status code 200, latency ms 999, body —, error —, statuses —, body contains —, degraded latency ms 1,000, down latency ms 3,000 → status up, reason ok, message HTTP 200 in 999 ms
latency exactly at the degraded threshold is degraded status code 200, latency ms 1,000, body —, error —, statuses —, body contains —, degraded latency ms 1,000, down latency ms 3,000 → status degraded, reason slow, message latency 1000 ms at or above 1000 ms
slow but not too slow is degraded status code 200, latency ms 1,500, body —, error —, statuses —, body contains —, degraded latency ms 1,000, down latency ms 3,000 → status degraded, reason slow, message latency 1500 ms at or above 1000 ms
latency at the down threshold is down status code 200, latency ms 3,000, body —, error —, statuses —, body contains —, degraded latency ms 1,000, down latency ms 3,000 → status down, reason too-slow, message latency 3000 ms at or above 3000 ms
only a down threshold: one millisecond under is up status code 200, latency ms 1,999, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms 2,000 → status up, reason ok, message HTTP 200 in 1999 ms
only a down threshold: at it is down status code 200, latency ms 2,000, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms 2,000 → status down, reason too-slow, message latency 2000 ms at or above 2000 ms
a wrong status is reported before the slowness status code 500, latency ms 5,000, body —, error —, statuses —, body contains —, degraded latency ms 1,000, down latency ms 3,000 → status down, reason unexpected-status, message HTTP 500, expected 2xx
a body mismatch is reported before the slowness status code 200, latency ms 5,000, body maintenance, error —, statuses —, body contains ok, degraded latency ms 1,000, down latency ms 3,000 → status down, reason body-mismatch, message body does not contain "ok"
equal thresholds skip degraded status code 200, latency ms 1,000, body —, error —, statuses —, body contains —, degraded latency ms 1,000, down latency ms 1,000 → status down, reason too-slow, message latency 1000 ms at or above 1000 ms
a timeout is down status code —, latency ms 10,000, body —, error timeout, statuses —, body contains —, degraded latency ms 1,000, down latency ms 3,000 → status down, reason timeout, message timed out
a DNS failure is down status code —, latency ms —, body —, error dns, statuses —, body contains —, degraded latency ms —, down latency ms — → status down, reason dns, message DNS lookup failed
a refused connection is down status code —, latency ms 3, body —, error connection, statuses —, body contains —, degraded latency ms —, down latency ms — → status down, reason connection, message connection failed
a TLS failure is down status code —, latency ms 20, body —, error tls, statuses —, body contains —, degraded latency ms —, down latency ms — → status down, reason tls, message TLS handshake failed
an error wins even over a 200 that arrived before it status code 200, latency ms 20, body ok, error protocol, statuses —, body contains ok, degraded latency ms —, down latency ms — → status down, reason protocol, message protocol error
neither a status code nor an error is an error status code —, latency ms 10, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → error: probe must have a statusCode or an error
a status code below 100 is an error status code 99, latency ms 10, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → error: statusCode must be 100 to 599, received 99
a status code above 599 is an error status code 600, latency ms 10, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → error: statusCode must be 100 to 599, received 600
a fractional status code is an error status code 200.5, latency ms 10, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → error: statusCode must be 100 to 599, received 200.5
a negative latency is an error status code 200, latency ms -1, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → error: latencyMs must be a whole number 0 or more, received -1
a fractional latency is an error status code 200, latency ms 1.5, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms — → error: latencyMs must be a whole number 0 or more, received 1.5
an unknown error kind is an error status code —, latency ms —, body —, error refused, statuses —, body contains —, degraded latency ms —, down latency ms — → error: unknown probe error: refused
a down threshold below the degraded one is an error status code 200, latency ms 10, body —, error —, statuses —, body contains —, degraded latency ms 1,000, down latency ms 500 → error: downLatencyMs must not be below degradedLatencyMs: 500 < 1000
a zero degraded threshold is an error status code 200, latency ms 10, body —, error —, statuses —, body contains —, degraded latency ms 0, down latency ms — → error: degradedLatencyMs must be a whole number 1 or more, received 0
a zero down threshold is an error status code 200, latency ms 10, body —, error —, statuses —, body contains —, degraded latency ms —, down latency ms 0 → error: downLatencyMs must be a whole number 1 or more, received 0
an empty list of accepted codes is an error status code 200, latency ms 10, body —, error —, statuses , body contains —, degraded latency ms —, down latency ms — → error: statuses must not be empty; use null to accept any 2xx
an accepted code outside 100 to 599 is an error status code 200, latency ms 10, body —, error —, statuses 200, 42, body contains —, degraded latency ms —, down latency ms — → error: expected statuses must be 100 to 599, received 42

More from the author

## Order of checks

The first thing wrong is the reason, in this order, so the alert names what someone has to fix rather than a symptom of it:

1. `error` set (a transport failure): **down**, reason = the error kind. An error wins even when a status code also arrived (a protocol error after the headers). 2. Status code not accepted: **down**, `unexpected-status`. With `statuses` null any 2xx is accepted, the Prometheus blackbox_exporter default for `valid_status_codes`; a redirect (3xx) is therefore down unless listed, since a health endpoint that redirects is usually a misconfiguration. An explicit list replaces 2xx entirely (`[404]` for a page that should stay gone). 3. `bodyContains` not found: **down**, `body-mismatch`. Case-sensitive substring; a null body never contains anything. (blackbox_exporter's `fail_if_body_not_matches_regexp` is the regex version; this is a plain substring so all three languages agree exactly.) 4. `latencyMs >= downLatencyMs`: **down**, `too-slow`. 5. `latencyMs >= degradedLatencyMs`: **degraded**, `slow`. 6. Otherwise **up**, `ok`.

Latency thresholds are "at or above" and apply only when `latencyMs` is non-null; either threshold may be null to skip that stage, and equal thresholds skip degraded.

## Messages

Fixed English, identical in every language:

| reason | message | | --- | --- | | `ok` | `HTTP 200 in 123 ms`, or `HTTP 200` when latency is null | | `unexpected-status` | `HTTP 503, expected 2xx`; `HTTP 404, expected 200 or 204`; `expected 200, 201 or 204` | | `body-mismatch` | `body does not contain "ok"` | | `slow`, `too-slow` | `latency 1500 ms at or above 1000 ms` (the threshold crossed) | | `timeout` | `timed out` | | `dns` | `DNS lookup failed` | | `connection` | `connection failed` | | `tls` | `TLS handshake failed` | | `protocol` | `protocol error` |

## Errors

- `probe must have a statusCode or an error` - `statusCode must be 100 to 599, received X` - `latencyMs must be a whole number 0 or more, received X` - `unknown probe error: X` - `statuses must not be empty; use null to accept any 2xx` (an empty list would accept nothing and mark every probe down) - `expected statuses must be 100 to 599, received X` - `degradedLatencyMs must be a whole number 1 or more, received X` (and the same for `downLatencyMs`) - `downLatencyMs must not be below degradedLatencyMs: D < G`

## Sources

- Prometheus blackbox_exporter configuration, `http_probe`: `valid_status_codes` "Defaults to 2xx", and `fail_if_body_not_matches_regexp`: https://github.com/prometheus/blackbox_exporter/blob/master/CONFIGURATION.md

Files

PathBytes
README.md3,140
impl/python.py3,857
impl/rust.rs6,275
impl/typescript.ts3,526
vectors.json12,053