Skip to the content

Capsid menu

src/server/http.ts

109 lines and 10 functions in DrDustinEdwards/site-api, at commit d3b340f on 2026-10-24. This file on GitHub.

Functions

The functions of http.ts
FunctionLinesComplexityCoveredCRAPMutation
json32 to 371100%OkCRAP 1No data: no mutants in this code
fail39 to 411100%OkCRAP 1No data: no mutants in this code
constructor44 to 461100%OkCRAP 1No data: no mutants in this code
readBody49 to 65685%WatchCRAP 6.1No data: no mutants in this code
readQuery67 to 712100%OkCRAP 2No data: no mutants in this code
describe73 to 781100%OkCRAP 1No data: no mutants in this code
<anonymous:76>76 to 762100%OkCRAP 2No data: no mutants in this code
checked83 to 872100%OkCRAP 2No data: no mutants in this code
pathParam94 to 1033100%OkCRAP 3No data: no mutants in this code
describeFailure115 to 1278100%WatchCRAP 8No data: no mutants in this code

Imports

It imports src/adapter.ts, src/contract.ts. Imported by src/server.ts, src/server/content.ts, src/server/media.ts, src/server/mentions.ts.

Source

// What every route shares: the JSON answers, the body and query readers, the check on what an adapter
// answers, and the contract's name and words for a failure.

import type { z } from "zod";
import { MediaInUseError, NotFoundError, RefusedError, VersionConflictError } from "../adapter.js";
import type { ErrorBody, ErrorCode, MediaUse } from "../contract.js";
import { readCapped } from "@dustinedwards/site-runtime/body";

/** Four million bytes: two million characters of source can take that much as UTF-8 JSON. */
const MAX_BODY_BYTES = 4_000_000;

const STATUS: Record<ErrorCode, number> = {
  unauthorized: 401,
  "rate-limited": 429,
  "not-configured": 503,
  unavailable: 503,
  "not-found": 404,
  "method-not-allowed": 405,
  invalid: 400,
  "too-large": 413,
  "version-conflict": 409,
  refused: 422,
  "not-implemented": 501,
  internal: 500,
};

export const BASE_HEADERS = {
  "cache-control": "no-store",
  "x-content-type-options": "nosniff",
};

export function json(status: number, body: unknown): Response {
  return new Response(JSON.stringify(body), {
    status,
    headers: { ...BASE_HEADERS, "content-type": "application/json; charset=utf-8" },
  });
}

export function fail(code: ErrorCode, message: string, extra: Partial<ErrorBody> = {}): Response {
  return json(STATUS[code], { error: code, message, ...extra });
}

export class BadRequest extends Error {
  constructor(readonly code: ErrorCode, message: string) {
    super(message);
  }
}

export async function readBody<T extends z.ZodType>(request: Request, schema: T): Promise<z.infer<T>> {
  const declared = Number(request.headers.get("content-length") ?? "0");
  if (declared > MAX_BODY_BYTES) throw new BadRequest("too-large", "The body is too large.");
  // Counted as it arrives (site-runtime's one reader), so an understated or missing Content-Length cannot make the
  // Worker hold more than the limit; request.text() read the whole body first.
  const text = await readCapped(request, MAX_BODY_BYTES);
  if (text === null) throw new BadRequest("too-large", "The body is too large.");
  let raw: unknown;
  try {
    raw = JSON.parse(text);
  } catch {
    throw new BadRequest("invalid", "The body is not JSON.");
  }
  const parsed = schema.safeParse(raw);
  if (!parsed.success) throw new BadRequest("invalid", describe(parsed.error));
  return parsed.data;
}

export function readQuery<T extends z.ZodType>(url: URL, schema: T): z.infer<T> {
  const parsed = schema.safeParse(Object.fromEntries(url.searchParams));
  if (!parsed.success) throw new BadRequest("invalid", describe(parsed.error));
  return parsed.data;
}

function describe(error: z.ZodError): string {
  return error.issues
    .slice(0, 5)
    .map((issue) => `${issue.path.join(".") || "body"}: ${issue.message}`)
    .join("; ");
}

/** An adapter that answers outside the contract is a site bug, reported as one and never passed on. */
export class ContractBreach extends Error {}

export function checked<T extends z.ZodType>(schema: T, value: unknown, what: string): z.infer<T> {
  const parsed = schema.safeParse(value);
  if (!parsed.success) throw new ContractBreach(`${what}: ${describe(parsed.error)}`);
  return parsed.data;
}

export type Route = { method: string; pattern: RegExp; run: (m: RegExpMatchArray, url: URL, request: Request) => Promise<Response> };

export const ID = "([^/]+)";

/** A path segment decoded and checked against its shape; what is refused names the thing it should have been. */
export function pathParam(raw: string | undefined, shape: z.ZodType, what: string): string {
  let value: string;
  try {
    value = decodeURIComponent(raw!);
  } catch {
    throw new BadRequest("invalid", `The ${what} is not valid.`);
  }
  if (!shape.safeParse(value).success) throw new BadRequest("invalid", `The ${what} is not valid.`);
  return value;
}

export type Log = (entry: Record<string, unknown>) => void;

export interface Failure {
  code: ErrorCode;
  message: string;
  currentVersion?: string | null;
  usedBy?: MediaUse[];
}

/** The contract's name and words for a failure, for the single routes and for one file of a bulk. */
export function describeFailure(error: unknown, log: Log): Failure {
  if (error instanceof BadRequest) return { code: error.code, message: error.message };
  if (error instanceof VersionConflictError) return { code: "version-conflict", message: error.message, currentVersion: error.currentVersion };
  if (error instanceof NotFoundError) return { code: "not-found", message: error.message };
  if (error instanceof MediaInUseError) return { code: "refused", message: error.message, usedBy: error.usedBy };
  if (error instanceof RefusedError) return { code: "refused", message: error.message };
  if (error instanceof ContractBreach) {
    log({ siteApi: "contract-breach", detail: error.message });
    return { code: "internal", message: "The site answered outside the contract." };
  }
  log({ siteApi: "adapter-failed", error: error instanceof Error ? error.message : String(error) });
  return { code: "internal", message: "The site failed to answer." };
}