4.2 KiB
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 toglobalThis.fetch - JSON serialisation of object bodies (string bodies pass through
unchanged, enabling
application/x-www-form-urlencodedflows) - Per-call
timeoutMs(default30_000) via internalAbortController - External
signal?: AbortSignalcomposed with the internal timeout - Typed errors:
HTTP_ERROR,TIMEOUT,PARSE_ERROR - Empty-body handling: 204 / empty responses resolve to
data: {} provider?: stringdiscriminant surfaced on every raised errorcomposeAbortSignal(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
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
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<Response> |
httpRequest<T>(options: HttpOptions): Promise<ProviderResponse<T>> |
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<string, string> |
{} |
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.