Fastify plugin that decorates the Fastify instance with app.fetch and routes requests through internal injection or external fetch according to policy.
This approach is most useful for services that combine in-process route calls with external HTTP(S) calls and need explicit policy control over redirects, trust boundaries, and payload behavior. It is not intended for non-Fastify runtimes, browser-side fetch abstractions, or workflow orchestration concerns such as retries, caching, and circuit breaking.
pnpm add fastify-fetchimport fastify from 'fastify'
import fastifyFetch from 'fastify-fetch'
const app = fastify()
await app.register(fastifyFetch)
app.get('/health', async () => ({ status: 'ok' }))
const response = await app.fetch('https://example.com/health')
console.log(await response.json())Policy choices usually follow a service journey: set a routing baseline, define redirect boundary behavior, set payload limits, and select a response contract default.
This pattern fits when service-local calls should run internally for latency and test parity, while third-party domains delegate externally.
This example focuses on routing defaults and route decisions.
policy: {
defaultTransport: 'external',
route: ({ currentUrl }) =>
currentUrl.hostname === 'api.internal.local' ? 'internal-buffered' : 'external',
}This configuration keeps internal-domain targets internal and delegates other targets externally.
This pattern fits when selected paths should be rejected regardless of other routing defaults.
This example focuses on route-level rejection decisions.
policy: {
defaultTransport: 'internal-buffered',
route: ({ currentUrl }) =>
currentUrl.pathname.startsWith('/admin/') ? 'reject' : 'internal-buffered',
}This configuration rejects restricted paths and keeps other paths on internal transport.
This pattern fits when same-origin redirects can remain internal, cross-origin redirects should delegate externally, and redirect depth must be bounded.
This example focuses on redirects.onBoundary: 'delegate' and redirects.maxHops.
import { sameOrigin } from 'fastify-fetch'
policy: {
redirects: {
onBoundary: 'delegate',
maxHops: 10,
},
route: ({ currentUrl, isRedirect, previousUrl }) => {
if (isRedirect && previousUrl && !sameOrigin(previousUrl, currentUrl)) {
return 'external'
}
return 'internal-buffered'
},
}This configuration keeps same-origin redirects internal, delegates cross-origin redirects externally, and caps redirect chains at ten hops.
This pattern fits when redirects must never cross from internal execution to external fetch.
This example focuses on redirects.onBoundary: 'reject'.
policy: {
redirects: {
onBoundary: 'reject',
maxHops: 10,
},
route: ({ currentUrl }) =>
currentUrl.hostname === 'api.internal.local' ? 'internal-buffered' : 'external',
}This configuration rejects cross-boundary redirects instead of delegating them.
This pattern fits when common calls can remain buffered while large upload and download paths avoid unbounded buffering.
This example focuses on payload limits with overflow fallback.
policy: {
buffering: {
maxRequestBytes: 1 * 1024 * 1024,
maxResponseBytes: 8 * 1024 * 1024,
onOverflow: 'fallback',
},
route: ({ currentUrl }) =>
currentUrl.pathname.startsWith('/downloads/')
? { transport: 'internal-stream', contract: 'wire-stream' }
: 'internal-buffered',
}This configuration keeps standard paths buffered and moves /downloads/* paths to streaming with wire preservation. Overflow fallback applies to buffered paths and is conditional, so calls can still fail when fallback conditions are not met.
This pattern fits when over-limit payloads should fail fast instead of changing transport mode.
This example focuses on buffering.onOverflow: 'reject'.
policy: {
buffering: {
maxRequestBytes: 512 * 1024,
maxResponseBytes: 2 * 1024 * 1024,
onOverflow: 'reject',
},
}This configuration rejects requests or responses that exceed configured buffered limits.
This pattern fits when most responses are forwarded downstream and relay-safe header normalization should be the default.
This example focuses on defaultContract.
policy: {
defaultContract: 'forward-safe',
route: ({ currentUrl }) =>
currentUrl.pathname.startsWith('/client/')
? { transport: 'internal-buffered', contract: 'fetch' }
: 'internal-buffered',
}This configuration applies forward-safe by default and uses fetch only on client-facing paths.
| Contract | Best-fit use case |
|---|---|
fetch |
direct client consumption where decoded payload behavior is preferred |
forward-safe |
downstream forwarding after decode where relay-sensitive headers must be normalized |
wire-stream |
byte-preserving relay and large transfer paths |
This package targets server-side Fetch compatibility for Fastify HTTP(S) workloads. Compatibility depends on transport and policy selection, so strict browser-equivalent behavior is not guaranteed in every mode.
externaltransport inherits request and response semantics fromexternalFetch(undici.fetchby default).internal-bufferedandinternal-streamreturn FetchResponseobjects and apply Fetch-style behavior for redirect handling, abort propagation, origin-sensitive header handling, and fetch-style failure normalization.- Policy resolution order is fixed: call override when enabled, then route decision, then policy defaults.
routecan returnreject, which fails the call withTypeError('fetch failed').redirects.onBoundary: 'reject'blocks internal-to-external redirect transitions.buffering.onOverflow: 'reject'fails over-limit payload paths instead of falling back.forward-safedecodes supported content codings and normalizes relay-sensitive headers by removingcontent-encodingand updating or removingcontent-length.wire-streampreserves encoded payload bytes and wire headers without decode.
redirect: 'manual'follows Undici server-runtime behavior and returns redirect responses instead of browseropaqueredirectresponses.- Automatic redirect following applies to
301,302,303,307, and308. - Redirect targets are restricted to HTTP(S); redirects to other schemes fail.
- Non-HTTP(S) request URLs are routed to external transport unless policy explicitly returns
reject.
function fromNodeHeaders ↗
Converts Node.js outgoing headers into a Fetch Headers instance.
export declare function fromNodeHeaders(nodeHeaders: OutgoingHttpHeaders): Headers| Parameter | Type | Description |
|---|---|---|
nodeHeaders |
OutgoingHttpHeaders |
Node.js outgoing header object. |
Fetch headers containing all defined entries.
Numeric values are stringified and undefined values are skipped.
function sameOrigin ↗
Returns whether two URLs share the same origin.
export declare function sameOrigin(sourceUrl: URL, targetUrl: URL): boolean| Parameter | Type | Description |
|---|---|---|
sourceUrl |
URL |
Source URL in the comparison. |
targetUrl |
URL |
Target URL in the comparison. |
true when both URLs resolve to the same origin; otherwise false.
The comparison matches scheme, host, and port for tuple origins and treats matching opaque origins (origin === 'null') as same-origin. This helper is commonly used in redirect policy decisions to distinguish same-origin transitions from cross-origin transitions.
function splitCookiesString ↗
Splits a potentially comma-joined set-cookie header value into individual cookie values.
export declare function splitCookiesString(cookiesString: string): string[]| Parameter | Type | Description |
|---|---|---|
cookiesString |
string |
Raw set-cookie header value that may contain one or more cookies. |
Cookie header values in original order.
Commas inside cookie attributes such as Expires are preserved, so commas are treated as separators only when they begin a new cookie key-value pair.
function toNodeHeaders ↗
Converts Fetch Headers into a Node.js outgoing header object.
export declare function toNodeHeaders(headers: Headers): OutgoingHttpHeaders| Parameter | Type | Description |
|---|---|---|
headers |
Headers |
Fetch headers to convert. |
Node.js outgoing header object.
set-cookie values are normalized to preserve multiple cookie entries when comma-joined values are encountered.
const fastifyFetch ↗
Decorates a Fastify instance with app.fetch and applies policy-routed internal or external request execution.
fastifyFetch: import('fastify').FastifyPluginCallback<
FastifyFetchOptions,
import('fastify').RawServerDefault,
import('fastify').FastifyTypeProviderDefault,
import('fastify').FastifyBaseLogger
>Routing and response behavior are resolved from plugin policy defaults and optional per-call overrides when FastifyFetchOptions.allowPerCallOverrides is true. Internal execution failures are normalized to TypeError('fetch failed') with nested causes when available. Delegated external execution follows the configured external fetch behavior.
interface FastifyFetchCallOverrides ↗
Defines optional contract and transport overrides for a single app.fetch call.
export interface FastifyFetchCallOverridesCall overrides are applied only when FastifyFetchOptions.allowPerCallOverrides is true. transport intentionally excludes reject, so call-level overrides can choose execution location but cannot force policy rejection.
Contract override for a single call.
readonly contract?: FastifyFetchContract;Transport override for a single call.
readonly transport?: Exclude<FastifyFetchTransport, 'reject'>;interface FastifyFetchInit ↗
Extends Fetch RequestInit with optional fastify-fetch call overrides.
export interface FastifyFetchInit extends RequestInitPer-call policy overrides for transport and contract decisions.
readonly fastifyFetch?: FastifyFetchCallOverrides;This field is applied only when FastifyFetchOptions.allowPerCallOverrides is true; otherwise it is ignored.
interface FastifyFetchOptions ↗
Defines plugin registration options for fastifyFetch.
export interface FastifyFetchOptionsEnables call-level overrides provided through FastifyFetchInit.fastifyFetch.
readonly allowPerCallOverrides?: boolean;When true, FastifyFetchCallOverrides.transport and FastifyFetchCallOverrides.contract can override route and default policy decisions for one call. Call-level transport overrides can choose internal or external execution, but cannot force reject. When false, values in FastifyFetchInit.fastifyFetch are ignored.
External fetch implementation used when policy delegates to external transport.
readonly externalFetch?: typeof fetch;Global policy used to resolve routing, redirects, buffering, and contract defaults.
readonly policy?: FastifyFetchPolicy;interface FastifyFetchPolicy ↗
Defines default routing, redirect, buffering, and contract policy for app.fetch.
export interface FastifyFetchPolicyDecision precedence is: call overrides from FastifyFetchInit.fastifyFetch when FastifyFetchOptions.allowPerCallOverrides is true, then FastifyFetchPolicy.route, then policy defaults.
Configures buffered payload limits and overflow behavior.
Defaults are 1048576 request bytes (1 MiB), 8388608 response bytes (8 MiB), and onOverflow: 'fallback'.
readonly buffering?: {
readonly maxRequestBytes?: number;
readonly maxResponseBytes?: number;
readonly onOverflow?: FastifyFetchOverflowPolicy;
};Default response contract when route and call overrides do not set one.
Default is 'fetch'.
readonly defaultContract?: FastifyFetchContract;Default transport when route and call overrides do not set one.
Default is 'internal-buffered'.
readonly defaultTransport?: Exclude<FastifyFetchTransport, 'reject'>;Configures redirect hop limits and boundary behavior.
Defaults are maxHops: 20 and onBoundary: 'delegate'.
readonly redirects?: {
readonly maxHops?: number;
readonly onBoundary?: FastifyFetchBoundaryPolicy;
};Callback used to resolve transport and optional contract per request evaluation.
readonly route?: FastifyFetchRoute;interface FastifyFetchRouteContext ↗
Provides request and redirect metadata for route policy decisions.
export interface FastifyFetchRouteContextURL currently being evaluated for transport and contract decisions.
readonly currentUrl: URL;Returns whether the redirect transition crosses origins.
readonly isCrossOriginRedirect: () => boolean;Indicates whether the current evaluation occurs after at least one redirect hop.
readonly isRedirect: boolean;Original Fetch request constructed for the app.fetch call.
readonly originalRequest: Request;URL from the previous hop, when evaluation occurs during redirect handling.
readonly previousUrl: URL | undefined;Number of redirects followed before the current evaluation.
readonly redirectCount: number;type FastifyFetch ↗
Fetch-compatible call signature decorated on the Fastify instance.
export type FastifyFetch = (input: RequestInfo | URL, init?: FastifyFetchInit) => Promise<Response>type FastifyFetchBoundaryPolicy ↗
Defines redirect behavior when handling reaches an internal-to-external boundary.
export type FastifyFetchBoundaryPolicy = 'delegate' | 'reject'delegate allows boundary transition to external transport, while reject fails the call when that transition is required.
type FastifyFetchContract ↗
Selects response payload and header handling for internal executions.
export type FastifyFetchContract = 'fetch' | 'forward-safe' | 'wire-stream'fetch decodes supported content codings while preserving wire headers, forward-safe decodes and normalizes relay-sensitive headers, and wire-stream preserves encoded payload bytes.
type FastifyFetchOverflowPolicy ↗
Defines overflow behavior when buffered payload limits are exceeded.
export type FastifyFetchOverflowPolicy = 'fallback' | 'reject'fallback attempts stream transport only when fallback is applicable for the current request and response path. Calls still fail when fallback cannot be applied. reject fails immediately when buffered limits are exceeded.
type FastifyFetchRoute ↗
Computes a route decision from the current routing context.
export type FastifyFetchRoute = (context: FastifyFetchRouteContext) => FastifyFetchRouteDecisiontype FastifyFetchRouteDecision ↗
Represents the result returned by a route policy callback.
export type FastifyFetchRouteDecision =
| FastifyFetchTransport
| {
readonly transport: FastifyFetchTransport
readonly contract?: FastifyFetchContract
}String form selects transport directly, while object form sets transport and optional response contract.
type FastifyFetchTransport ↗
Selects where and how a fetch call is executed.
export type FastifyFetchTransport = 'external' | 'internal-buffered' | 'internal-stream' | 'reject'internal-buffered and internal-stream dispatch through Fastify injection, external delegates to the configured external fetch implementation, and reject fails the call with a fetch-style error. reject can be returned by FastifyFetchPolicy.route, but it is intentionally excluded from FastifyFetchPolicy.defaultTransport and FastifyFetchCallOverrides.transport.
type Fetch ↗
Fetch function type used by FastifyFetchOptions.externalFetch.
export type Fetch = typeof fetch