HTTP Method Semantics: Safe, Idempotent, Cacheable

Classify HTTP methods as safe, idempotent and cacheable by default, and decide whether a failed request may be retried.

TypescriptResilience

Code

snippet
export type HttpMethod =
  | "GET" | "HEAD" | "OPTIONS" | "TRACE"
  | "POST" | "PUT" | "PATCH" | "DELETE" | "CONNECT";

export interface MethodSemantics {
  method: HttpMethod;
  safe: boolean;
  idempotent: boolean;
  cacheableByDefault: boolean;
}

const SAFE: ReadonlySet<HttpMethod> = new Set(["GET", "HEAD", "OPTIONS", "TRACE"]);
const IDEMPOTENT: ReadonlySet<HttpMethod> = new Set([
  "GET", "HEAD", "OPTIONS", "TRACE", "PUT", "DELETE",
]);
const CACHEABLE: ReadonlySet<HttpMethod> = new Set(["GET", "HEAD"]);
const KNOWN: ReadonlySet<HttpMethod> = new Set([
  "GET", "HEAD", "OPTIONS", "TRACE", "POST", "PUT", "PATCH", "DELETE", "CONNECT",
]);

export function methodSemantics(method: string): MethodSemantics {
  if (typeof method !== "string" || method.length === 0) {
    throw new TypeError("method must be a non-empty string");
  }
  const normalized = method.toUpperCase() as HttpMethod;
  if (!KNOWN.has(normalized)) throw new RangeError("unknown HTTP method: " + method);
  return {
    method: normalized,
    safe: SAFE.has(normalized),
    idempotent: IDEMPOTENT.has(normalized),
    cacheableByDefault: CACHEABLE.has(normalized),
  };
}

export function isRetryable(method: string, status: number): boolean {
  if (!Number.isInteger(status) || status < 100 || status > 599) {
    throw new RangeError("status must be a valid HTTP status code");
  }
  const semantics = methodSemantics(method);
  return status >= 500 && (semantics.idempotent || status === 503);
}