Skip to content

Debugging & observability

openFetch gives you two complementary ways to see what happens around HTTP: a built-in debug pipeline on createClient (structured lifecycle events), and a debug() middleware for request/response/error snapshots. The package also ships redaction utilities and typed errors so diagnostics stay useful without leaking secrets.

Built-in debug pipeline (createClient)

Enable structured logging on the client defaults (or per request). When debug is set, each call emits OpenFetchDebugEvent objects with at least:

  • stage — Where you are in the lifecycle (see tables below).
  • timestampDate.now() when the event was emitted.
  • Additional fields — Depends on the stage (url, status, durationMs, reason, …).

Enablement

ts
import { createClient } from "@hamdymohamedak/openfetch";

const client = createClient({
  debug: true, // same as "verbose"
  logger: (log) => {
    // Optional. Omit to use console.debug with an [OpenFetch] prefix.
    console.debug(log.stage, log);
  },
});
debug valueBehavior
true or "verbose"Full pipeline: merge/init, outgoing request, each fetch attempt, parse, schema, retries, final response, errors.
"basic"Only request, final response, and error (good for production-style tracing with less noise).
false / omittedNo pipeline events.

Per-request overrides merge like the rest of OpenFetchConfig: you can set debug: "basic" on the client and debug: "verbose" on a single client.get(url, { debug: "verbose" }) when investigating one call.

Typical development setup

ts
const client = createClient({
  debug: import.meta.env.DEV ? "verbose" : false,
  logger: (e) => {
    if (import.meta.env.DEV) console.debug("[API]", e);
  },
});

On Node you can use process.env.NODE_ENV === "development" the same way.

Custom sink (DevTools, APM, tests)

The logger function receives one structured object per event. You can forward to OpenTelemetry, a test spy, or a small in-memory ring buffer:

ts
const events: import("@hamdymohamedak/openfetch").OpenFetchDebugEvent[] = [];

const client = createClient({
  debug: "verbose",
  logger: (log) => {
    events.push(log);
  },
});

The exported type OpenFetchDebugEvent is { stage: string; timestamp: number } & Record<string, unknown> — treat unknown keys as optional metadata.

Stages (verbose)

These stages are emitted when debug is true or "verbose" (not for "basic" except where noted):

StageWhenUseful fields (examples)
configAfter merge, before initmethod, url (redacted path), hasJsonSchema, retryMaxAttempts, …
initAfter all init[] callbackshooksRun
requestAfter request interceptors, before middlewarebasic toomethod, url (fully built + query redacted), optional headers (masked)
attempt_startStart of each retry loop iterationattempt, maxAttempts
fetchBefore fetch() for that attemptattempt, method, url, headers (masked)
fetch_completeAfter headers receivedattempt, status, statusText, durationMs, contentLength (if server sent Content-Length)
parseAfter body read / parseattempt, ok, responseType or skipped + reason for rawResponse
schemaAfter Standard Schema validation (if jsonSchema set)attempt, ok, issueCount on failure
hook_after_responseWhen onAfterResponse throws OpenFetchForceRetryhook, action
retryWhen scheduling another attempt after a failurefailedAttempt, nextAttempt, reason, delayMs
responseEnd of successful client call (basic too)status, statusText, durationMs (wall time for the whole call, including retries)
errorUncaught failure from the client run (basic too)name, message, code for OpenFetchError, stack (truncated), durationMs

Retry reasons (on the retry stage) include values such as forceRetry, timeout, http_503, network, parse, retryBudget, canceled, unknown — derived from the error about to trigger backoff.

Privacy defaults

  • URLs in debug events use the same sensitive query redaction as redactSensitiveUrlQuery (tokens, passwords, code, etc. → [REDACTED] by default).
  • Headers on request and fetch are passed through maskHeaderValues (authorization, cookies, API keys, …).

Do not log raw tokens in your own logger implementation; treat events as trusted dev output only.


debug() middleware (@hamdymohamedak/openfetch/plugins)

The debug(options?) export is a middleware that wraps next() and logs three phases: request, response, error. It does not know about merge, retry attempt indices, or schema validation — it is lighter and stack-local.

ts
import { createClient } from "@hamdymohamedak/openfetch";
import { debug } from "@hamdymohamedak/openfetch/plugins";

const client = createClient({
  middlewares: [
    debug({
      includeRequestHeaders: true,
      maskStrategy: "partial",
      maskUrlQuery: true,
      log: (phase, payload) => console.debug("[openfetch]", phase, payload),
    }),
  ],
});

Options include maskHeaders, maskStrategy, sensitiveQueryParamNames, sensitiveQueryParamReplacement, and enabled. See Plugins & fluent API.

When to use which

NeedUse
Full lifecycle, retries, parse/schema, one switch on the clientcreateClient({ debug, logger })
Quick middleware-only traces, or logs only around fetchdebug() plugin
BothPossible; avoid duplicate noise by using "basic" on the client or disabling the plugin’s log when the pipeline is verbose.

Typed errors and safe shapes

For user-facing or cross-service errors, serialize with OpenFetchError.toShape() / toJSON() instead of logging the Error instance directly. By default, shapes omit sensitive fields and can redact URLs (see Errors & security).

Use isOpenFetchError, isHTTPError, isTimeoutError, and isSchemaValidationError to branch in logger or global handlers.


ExportUse in debugging
maskHeaderValuesBuild your own logs or UI with the same masking rules as the pipeline.
redactSensitiveUrlQueryRedact query params in arbitrary URL strings.
DEFAULT_SENSITIVE_QUERY_PARAM_NAMESExtend or document what the package treats as sensitive.
cloneResponseClone a native Response if you read the body in middleware and still want OpenFetch or the browser to consume it.
OpenFetchForceRetry, isOpenFetchForceRetryUnderstand forced retries in retry.onAfterResponse.

Hooks and interceptors

For custom spans (e.g. “before business mapper”), combine the debug pipeline with:

The built-in pipeline shows when the stack runs; your hooks carry domain context.


Fluent client

createFluentClient is built on createClient, so debug and logger on defaults apply to fluent chains as well.


See also

MIT · @hamdymohamedak/openfetch