# `httpRequest` helper _(no submodules)_ ## Purpose Universal HTTP helper used by every provider to issue typed, cancellable JSON requests. The helper is a thin wrapper over an injectable `FetchLike` (defaulting to `globalThis.fetch`) that JSON-serialises non-string bodies, applies a per-call timeout via `AbortSignal`, composes a caller-supplied `signal` with the internal timeout signal, and converts every failure path into a typed `PaymentProviderError` so callers can branch on `error.code` instead of parsing strings. ## Features - Injectable `FetchLike` (`fetch?: FetchLike`) — defaults to `globalThis.fetch` - JSON serialisation of object bodies (string bodies pass through unchanged, enabling `application/x-www-form-urlencoded` flows) - Per-call `timeoutMs` (default `30_000`) via internal `AbortController` - External `signal?: AbortSignal` composed with the internal timeout - Typed errors: `HTTP_ERROR`, `TIMEOUT`, `PARSE_ERROR` - Empty-body handling: 204 / empty responses resolve to `data: {}` - `provider?: string` discriminant surfaced on every raised error - `composeAbortSignal(internal, external)` public helper — reusable by sibling provider packages that issue out-of-band HTTP calls (OAuth token fetches, webhook signature fetches) and want the same caller-signal composition without re-implementing the wiring ## Usage: Basic ```ts import { httpRequest } from "@suaveplan/payment-providers"; const { status, data } = await httpRequest<{ id: string }>({ method: "POST", url: "https://api.example.com/charges", headers: { Authorization: "Bearer secret" }, body: { amount: 1000 }, timeoutMs: 10_000, provider: "example", }); ``` ## Usage: Advanced ```ts import { type FetchLike, httpRequest } from "@suaveplan/payment-providers"; // Inject a recording FetchLike for tests — no globalThis monkey-patching. const calls: Array<{ url: string; init?: RequestInit }> = []; const fetch: FetchLike = async (url, init) => { calls.push({ url, init }); return new Response(JSON.stringify({ ok: true }), { status: 200 }); }; const external = new AbortController(); const promise = httpRequest({ method: "GET", url: "https://api.example.com/intent/1", fetch, signal: external.signal, timeoutMs: 5_000, }); // Cancel externally — either signal firing aborts the request. external.abort(); await promise.catch((err) => console.log(err.code)); ``` ## API Reference | Symbol | Kind | Description | |---|---|---| | `FetchLike` | `type` | `(input: string, init?: RequestInit) => Promise` | | `httpRequest(options: HttpOptions): Promise>` | `function` | issues the HTTP call, returns parsed JSON | | `composeAbortSignal(internal, external?): AbortSignal` | `function` | returns `internal` when `external` is undefined; otherwise returns a fresh signal that aborts when either input aborts, with already-aborted external propagating synchronously | ### `HttpOptions` | Field | Type | Default | Description | |---|---|---|---| | `method` | `"GET" \| "POST" \| "DELETE" \| "PATCH"` | — | HTTP verb | | `url` | `string` | — | request URL | | `headers` | `Record` | `{}` | extra headers (override the JSON defaults if set) | | `body` | `unknown` | — | JSON-serialised unless already a `string` | | `timeoutMs` | `number` | `30_000` | per-call timeout | | `fetch` | `FetchLike` | `globalThis.fetch` | injectable HTTP client | | `signal` | `AbortSignal` | — | external cancellation | | `provider` | `string` | `"unknown"` | discriminant placed on raised errors | ## Implementation Notes The internal `AbortController` always wins on timeout — its `abort()` is called from a `setTimeout` whose handle is cleared in `finally`, so no listener leaks past the call. The composed `AbortSignal` is created inline via a third `AbortController`; both source signals (`timeoutController.signal` and the caller's `signal`) propagate to it via `addEventListener("abort", …, { once: true })`, plus an upfront check for the already-aborted case. Non-`Error` throws from the injected `FetchLike` (e.g. raw strings) become `HTTP_ERROR` with a stringified body excerpt — the helper never lets an untyped failure escape.