tRPC HTTP Client and Node.js Agent Configuration Interoperability
0 reputation · 21 Jun 2021, 01:50 UTC
0 reputation · 21 Jun 2021, 01:50 UTC
Determine whether tRPC should expose a user‑configurable hook for the underlying HTTP Agent (or undici fetch pool) while preserving the generic client abstraction that works across Node.js versions.
The challenge lies in balancing backward compatibility, version‑specific defaults (legacy http.Agent vs undici), and ensuring that any Agent replacement does not affect in‑flight requests or create mixed pool configurations.
Questions:
agentFactory (or similar) option to createHTTPClientOptions that lets users supply a custom Agent or fetch implementation?34775 reputation · 21 Jun 2021, 13:37 UTC
tRPC does not need a dedicated agentFactory option on its HTTP client. The capability you're asking for already exists in a more portable form: httpLink and httpBatchLink accept a custom fetch implementation. Injecting a fetch that carries your Agent (or undici Dispatcher) gives you full control over pooling and keep-alive without tRPC ever learning what an "Agent" is.
The client links are deliberately runtime-agnostic. The same link code runs in browsers, edge workers, React Native, and Node. All it understands is fetch(input, init) → Promise<Response>. Connection pooling, keep-alive, and TLS session reuse are transport concerns that only exist in some of those environments. Adding an agentFactory to the shared options would leak a Node-only concept into a cross-runtime API, forcing every other target to ignore or stub it. The fetch override keeps the abstraction clean: tRPC owns request/response semantics, you own the transport.
On Node 18 and later, the global fetch is undici, and pooling is controlled by a Dispatcher. Wrap undici's fetch with your own Agent and pass the wrapper to the link:
import { fetch as undiciFetch, Agent } from 'undici';
import { httpBatchLink } from '@trpc/client';
const dispatcher = new Agent({ keepAliveTimeout: 10_000 });
const link = httpBatchLink({
url: 'https://api.example.com/trpc',
fetch: (input, init) => undiciFetch(input, { ...init, dispatcher }),
});On older Node versions there is no global fetch, so you already must inject one — typically node-fetch — and that is exactly where a classic http.Agent attaches:
import nodeFetch from 'node-fetch';
import http from 'node:http';
const agent = new http.Agent({ keepAlive: true });
const link = httpBatchLink({
url: 'https://api.example.com/trpc',
fetch: (input, init) => nodeFetch(input, { ...init, agent }),
});No new default is required. The sensible rule is: use whatever fetch the runtime provides, and only override when you need non-default pooling. On Node ≥ 18 that means undici with its default global dispatcher; on earlier versions it means the polyfill the user already supplies. One caveat: undici's keep-alive and dispatcher defaults have shifted across Node 18/20/22, so pin your runtime and verify rather than assuming identical pooling behavior across upgrades.
The fetch implementation is captured when the link is constructed, so swapping agents never mutates in-flight requests — each request runs on the transport its client was built with. If you need two pool configurations, build two clients. Be deliberate about not mixing node-fetch and global undici fetch casually in one process: they maintain separate connection pools, which can double your socket count against the same host.
process.version and typeof globalThis.fetch at startup.ss/netstat for established sockets) to confirm reuse matches your keepAliveTimeout.If maintainers want better ergonomics, a documented recipe for this wrapper pattern is a safer addition than new API surface — it changes no existing option semantics and keeps backward compatibility intact. Note that the createHTTPClientOptions naming in the question appears to be a proposed/internal shape rather than a confirmed public API, so treat that specific name as unverified.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.