Extends: events.EventEmitter
Dispatcher is the core API used to dispatch requests.
Requests are not guaranteed to be dispatched in order of invocation.
Dispatcher.close([callback]): PromiseCloses the dispatcher and gracefully waits for enqueued requests to complete before resolving.
Arguments:
(error: Error | null, data: null) => void (optional)Returns: void | Promise<null> - Only returns a Promise if no callback argument was passed
dispatcher.close() // -> Promise
dispatcher.close(() => {}) // -> void
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('undici')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.setEncoding('utf8')
body.on('data', console.log)
} catch (error) {}
await client.close()
console.log('Client closed')
server.close()
Dispatcher.connect(options[, callback])Starts two-way communications with the requested resource using HTTP CONNECT.
Arguments:
ConnectOptions(err: Error | null, data: ConnectData | null) => void (optional)Returns: void | Promise<ConnectData> - Only returns a Promise if no callback argument was passed
ConnectOptionsstringUndiciHeaders (optional) - Default: nullAbortSignal | events.EventEmitter | null (optional) - Default: nullunknown (optional) - This argument parameter is passed through to ConnectDataConnectDatanumberRecord<string, string | string[] | undefined>stream.Duplexunknownimport { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
throw Error('should never get here')
}).listen()
server.on('connect', (req, socket, head) => {
socket.write('HTTP/1.1 200 Connection established\r\n\r\n')
let data = head.toString()
socket.on('data', (buf) => {
data += buf.toString()
})
socket.on('end', () => {
socket.end(data)
})
})
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { socket } = await client.connect({
path: '/'
})
const wanted = 'Body'
let data = ''
socket.on('data', d => { data += d })
socket.on('end', () => {
console.log(`Data received: ${data.toString()} | Data wanted: ${wanted}`)
client.close()
server.close()
})
socket.write(wanted)
socket.end()
} catch (error) { }
Dispatcher.destroy([error, callback]): PromiseDestroy the dispatcher abruptly with the given error. All the pending and running requests will be asynchronously aborted and error. Since this operation is asynchronously dispatched there might still be some progress on dispatched requests.
Both arguments are optional; the method can be called in four different ways:
Arguments:
Error | null (optional)(error: Error | null, data: null) => void (optional)Returns: void | Promise<void> - Only returns a Promise if no callback argument was passed
dispatcher.destroy() // -> Promise
dispatcher.destroy(new Error()) // -> Promise
dispatcher.destroy(() => {}) // -> void
dispatcher.destroy(new Error(), () => {}) // -> void
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end()
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const request = client.request({
path: '/',
method: 'GET'
})
client.destroy()
.then(() => {
console.log('Client destroyed')
server.close()
})
await request
} catch (error) {
console.error(error)
}
Dispatcher.dispatch(options, handler)This is the low level API which all the preceding APIs are implemented on top of. This API is expected to evolve through semver-major versions and is less stable than the preceding higher level APIs. It is primarily intended for library developers who implement higher level APIs on top of this.
Arguments:
DispatchOptionsDispatchHandlerReturns: Boolean - false if dispatcher is busy and further dispatch calls won't make any progress until the 'drain' event has been emitted.
DispatchOptionsstring | URLstringstringboolean (optional) - Default: false - If false, the request will attempt to create a long-living connection by sending the connection: keep-alive header,otherwise will attempt to close it immediately after response by sending connection: close within the request and closing the socket afterwards.string | Buffer | Uint8Array | stream.Readable | Iterable | AsyncIterable | null (optional) - Default: nullUndiciHeaders (optional) - Default: null.Record<string, any> | null (optional) - Default: null - Query string params to be embedded in the request URL. Note that both keys and values of query are encoded using encodeURIComponent. If for some reason you need to send them unencoded, embed query params into path directly instead.boolean (optional) - Default: true if method is 'HEAD' or 'GET' - Whether the requests can be safely retried or not. If false the request won't be sent until all preceding requests in the pipeline has completed.boolean (optional) - Default: method !== 'HEAD' - Whether the response is expected to take a long time and would end up blocking the pipeline. When this is set to true further pipelining will be avoided on the same connection until headers have been received.string | null (optional) - Default: null - Upgrade the request. Should be used to specify the kind of upgrade i.e. 'Websocket'.number | null (optional) - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use 0 to disable it entirely. Defaults to 300 seconds.number | null (optional) - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.boolean (optional) - Default: false - For H2, it appends the expect: 100-continue header, and halts the request body until a 100-continue is received from the remote serverDispatchHandler(controller: DispatchController, context: object) => void - Invoked before request is dispatched on socket. May be invoked multiple times when a request is retried when the request at the head of the pipeline fails.(controller: DispatchController, statusCode: number, headers: Record<string, string | string[]>, socket: Duplex) => void (optional) - Invoked when request is upgraded. Required if DispatchOptions.upgrade is defined or DispatchOptions.method === 'CONNECT'.(controller: DispatchController, statusCode: number, headers: Record<string, string | string []>, statusMessage?: string) => void - Invoked when statusCode and headers have been received. May be invoked multiple times due to 1xx informational headers. Not required for upgrade requests. Any return value is ignored.(controller: DispatchController, chunk: Buffer) => void - Invoked when response payload data is received. Not required for upgrade requests.(controller: DispatchController, trailers: Record<string, string | string[]>) => void - Invoked when response payload and trailers have been received and the request has completed. Not required for upgrade requests.(controller: DispatchController, error: Error) => void - Invoked when an error has occurred. May not throw.import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const data = []
client.dispatch({
path: '/',
method: 'GET',
headers: {
'x-foo': 'bar'
}
}, {
onConnect: () => {
console.log('Connected!')
},
onError: (error) => {
console.error(error)
},
onHeaders: (statusCode, headers) => {
console.log(`onHeaders | statusCode: ${statusCode} | headers: ${headers}`)
},
onData: (chunk) => {
console.log('onData: chunk received')
data.push(chunk)
},
onComplete: (trailers) => {
console.log(`onComplete | trailers: ${trailers}`)
const res = Buffer.concat(data).toString('utf8')
console.log(`Data: ${res}`)
client.close()
server.close()
}
})
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end()
}).listen()
await once(server, 'listening')
server.on('upgrade', (request, socket, head) => {
console.log('Node.js Server - upgrade event')
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n')
socket.write('Upgrade: WebSocket\r\n')
socket.write('Connection: Upgrade\r\n')
socket.write('\r\n')
socket.end()
})
const client = new Client(`http://localhost:${server.address().port}`)
client.dispatch({
path: '/',
method: 'GET',
upgrade: 'websocket'
}, {
onConnect: () => {
console.log('Undici Client - onConnect')
},
onError: (error) => {
console.log('onError') // shouldn't print
},
onUpgrade: (statusCode, headers, socket) => {
console.log('Undici Client - onUpgrade')
console.log(`onUpgrade Headers: ${headers}`)
socket.on('data', buffer => {
console.log(buffer.toString('utf8'))
})
socket.on('end', () => {
client.close()
server.close()
})
socket.end()
}
})
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
request.on('data', (data) => {
console.log(`Request Data: ${data.toString('utf8')}`)
const body = JSON.parse(data)
body.message = 'World'
response.end(JSON.stringify(body))
})
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const data = []
client.dispatch({
path: '/',
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({ message: 'Hello' })
}, {
onConnect: () => {
console.log('Connected!')
},
onError: (error) => {
console.error(error)
},
onHeaders: (statusCode, headers) => {
console.log(`onHeaders | statusCode: ${statusCode} | headers: ${headers}`)
},
onData: (chunk) => {
console.log('onData: chunk received')
data.push(chunk)
},
onComplete: (trailers) => {
console.log(`onComplete | trailers: ${trailers}`)
const res = Buffer.concat(data).toString('utf8')
console.log(`Response Data: ${res}`)
client.close()
server.close()
}
})
Dispatcher.pipeline(options, handler)For easy use with stream.pipeline. The handler argument should return a Readable from which the result will be read. Usually it should just return the body argument unless some kind of transformation needs to be performed based on e.g. headers or statusCode. The handler should validate the response and save any required state. If there is an error, it should be thrown. The function returns a Duplex which writes to the request and reads from the response.
Arguments:
PipelineOptions(data: PipelineHandlerData) => stream.ReadableReturns: stream.Duplex
Extends: RequestOptions
boolean (optional) - Default: false - Set to true if the handler will return an object stream.numberRecord<string, string | string[] | undefined>unknownstream.Readableobject({statusCode: number, headers: Record<string, string | string[]>}) => void | null (optional) - Default: null - Callback collecting all the info headers (HTTP 100-199) received.import { Readable, Writable, PassThrough, pipeline } from 'stream'
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
request.pipe(response)
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
let res = ''
pipeline(
new Readable({
read () {
this.push(Buffer.from('undici'))
this.push(null)
}
}),
client.pipeline({
path: '/',
method: 'GET'
}, ({ statusCode, headers, body }) => {
console.log(`response received ${statusCode}`)
console.log('headers', headers)
return pipeline(body, new PassThrough(), () => {})
}),
new Writable({
write (chunk, _, callback) {
res += chunk.toString()
callback()
},
final (callback) {
console.log(`Response pipelined to writable: ${res}`)
callback()
}
}),
error => {
if (error) {
console.error(error)
}
client.close()
server.close()
}
)
Dispatcher.request(options[, callback])Performs a HTTP request.
Non-idempotent requests will not be pipelined in order to avoid indirect failures.
Idempotent requests will be automatically retried if they fail due to indirect failure from the request at the head of the pipeline. This does not apply to idempotent requests with a stream request body.
All response bodies must always be fully consumed or destroyed.
Arguments:
RequestOptions(error: Error | null, data: ResponseData) => void (optional)Returns: void | Promise<ResponseData> - Only returns a Promise if no callback argument was passed.
RequestOptionsExtends: DispatchOptions
unknown (optional) - Default: null - Used for passing through context to ResponseData.AbortSignal | events.EventEmitter | null (optional) - Default: null.({statusCode: number, headers: Record<string, string | string[]>}) => void | null (optional) - Default: null - Callback collecting all the info headers (HTTP 100-199) received.The RequestOptions.method property should not be value 'CONNECT'.
ResponseDatanumberstring - The status message from the response (e.g., "OK", "Not Found").Record<string, string | string[]> - Note that all header keys are lower-cased, e.g. content-type.stream.Readable which also implements the body mixin from the Fetch Standard.Record<string, string> - This object starts out
as empty and will be mutated to contain trailers after body has emitted 'end'.unknownobjectbody contains the following additional body mixin methods and properties:
.arrayBuffer().blob().bytes().json().text()bodybodyUsedbody can not be consumed twice. For example, calling text() after json() throws TypeError.
body contains the following additional extensions:
dump({ limit: Integer }), dump the response by reading up to limit bytes without killing the socket (optional) - Default: 262144.Note that body will still be a Readable even if it is empty, but attempting to deserialize it with json() will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and content-type header starts with application/json.
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { body, headers, statusCode, statusText, trailers } = await client.request({
path: '/',
method: 'GET'
})
console.log(`response received ${statusCode}`)
console.log('headers', headers)
body.setEncoding('utf8')
body.on('data', console.log)
body.on('error', console.error)
body.on('end', () => {
console.log('trailers', trailers)
})
client.close()
server.close()
} catch (error) {
console.error(error)
}
Node.js v15+ is required to run this example
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const abortController = new AbortController()
try {
client.request({
path: '/',
method: 'GET',
signal: abortController.signal
})
} catch (error) {
console.error(error) // should print an RequestAbortedError
client.close()
server.close()
}
abortController.abort()
Alternatively, any EventEmitter that emits an 'abort' event may be used as an abort controller:
import { createServer } from 'http'
import { Client } from 'undici'
import EventEmitter, { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const ee = new EventEmitter()
try {
client.request({
path: '/',
method: 'GET',
signal: ee
})
} catch (error) {
console.error(error) // should print an RequestAbortedError
client.close()
server.close()
}
ee.emit('abort')
Destroying the request or response body will have the same effect.
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { body } = await client.request({
path: '/',
method: 'GET'
})
body.destroy()
} catch (error) {
console.error(error) // should print an RequestAbortedError
client.close()
server.close()
}
Remember to fully consume the body even in the case when it is not read.
const { body, statusCode } = await client.request({
path: '/',
method: 'GET'
})
if (statusCode === 200) {
return await body.arrayBuffer()
}
await body.dump()
return null
Dispatcher.stream(options, factory[, callback])A faster version of Dispatcher.request. This method expects the second argument factory to return a stream.Writable stream which the response will be written to. This improves performance by avoiding creating an intermediate stream.Readable stream when the user expects to directly pipe the response body to a stream.Writable stream.
As demonstrated in Example 1 - Basic GET stream request, it is recommended to use the option.opaque property to avoid creating a closure for the factory method. This pattern works well with Node.js Web Frameworks such as Fastify. See Example 2 - Stream to Fastify Response for more details.
Arguments:
RequestOptions(data: StreamFactoryData) => stream.Writable(error: Error | null, data: StreamData) => void (optional)Returns: void | Promise<StreamData> - Only returns a Promise if no callback argument was passed
StreamFactoryDatanumberRecord<string, string | string[] | undefined>unknown({statusCode: number, headers: Record<string, string | string[]>}) => void | null (optional) - Default: null - Callback collecting all the info headers (HTTP 100-199) received.StreamDataunknownRecord<string, string>objectimport { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
import { Writable } from 'stream'
const server = createServer((request, response) => {
response.end('Hello, World!')
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
const bufs = []
try {
await client.stream({
path: '/',
method: 'GET',
opaque: { bufs }
}, ({ statusCode, headers, opaque: { bufs } }) => {
console.log(`response received ${statusCode}`)
console.log('headers', headers)
return new Writable({
write (chunk, encoding, callback) {
bufs.push(chunk)
callback()
}
})
})
console.log(Buffer.concat(bufs).toString('utf-8'))
client.close()
server.close()
} catch (error) {
console.error(error)
}
In this example, a (fake) request is made to the fastify server using fastify.inject(). This request then executes the fastify route handler which makes a subsequent request to the raw Node.js http server using undici.dispatcher.stream(). The fastify response is passed to the opaque option so that undici can tap into the underlying writable stream using response.raw. This methodology demonstrates how one could use undici and fastify together to create fast-as-possible requests from one backend server to another.
import { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
import fastify from 'fastify'
const nodeServer = createServer((request, response) => {
response.end('Hello, World! From Node.js HTTP Server')
}).listen()
await once(nodeServer, 'listening')
console.log('Node Server listening')
const nodeServerUndiciClient = new Client(`http://localhost:${nodeServer.address().port}`)
const fastifyServer = fastify()
fastifyServer.route({
url: '/',
method: 'GET',
handler: (request, response) => {
nodeServerUndiciClient.stream({
path: '/',
method: 'GET',
opaque: response
}, ({ opaque }) => opaque.raw)
}
})
await fastifyServer.listen()
console.log('Fastify Server listening')
const fastifyServerUndiciClient = new Client(`http://localhost:${fastifyServer.server.address().port}`)
try {
const { statusCode, body } = await fastifyServerUndiciClient.request({
path: '/',
method: 'GET'
})
console.log(`response received ${statusCode}`)
body.setEncoding('utf8')
body.on('data', console.log)
nodeServerUndiciClient.close()
fastifyServerUndiciClient.close()
fastifyServer.close()
nodeServer.close()
} catch (error) { }
Dispatcher.upgrade(options[, callback])Upgrade to a different protocol. Visit MDN - HTTP - Protocol upgrade mechanism for more details.
Arguments:
options UpgradeOptions
callback (error: Error | null, data: UpgradeData) => void (optional)
Returns: void | Promise<UpgradeData> - Only returns a Promise if no callback argument was passed
UpgradeOptionsstringstring (optional) - Default: 'GET'UndiciHeaders (optional) - Default: nullstring (optional) - Default: 'Websocket' - A string of comma separated protocols, in descending preference order.AbortSignal | EventEmitter | null (optional) - Default: nullUpgradeDatahttp.IncomingHeadersstream.Duplexunknownimport { createServer } from 'http'
import { Client } from 'undici'
import { once } from 'events'
const server = createServer((request, response) => {
response.statusCode = 101
response.setHeader('connection', 'upgrade')
response.setHeader('upgrade', request.headers.upgrade)
response.end()
}).listen()
await once(server, 'listening')
const client = new Client(`http://localhost:${server.address().port}`)
try {
const { headers, socket } = await client.upgrade({
path: '/',
})
socket.on('end', () => {
console.log(`upgrade: ${headers.upgrade}`) // upgrade: Websocket
client.close()
server.close()
})
socket.end()
} catch (error) {
console.error(error)
client.close()
server.close()
}
Dispatcher.compose(interceptors[, interceptor])Compose a new dispatcher from the current dispatcher and the given interceptors.
Notes:
- The order of the interceptors matters. The last interceptor will be the first to be called.
- It is important to note that the
interceptorfunction should return a function that follows theDispatcher.dispatchsignature.- Any fork of the chain of
interceptorscan lead to unexpected results.Interceptor Stack Visualization:
> compose([interceptor1, interceptor2, interceptor3]) > > Request Flow: > ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ > │ Request │───▶│interceptor3 │───▶│interceptor2 │───▶│interceptor1 │───▶│ dispatcher │ > └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ .dispatch │ > ▲ ▲ ▲ └─────────────┘ > │ │ │ ▲ > (called first) (called second) (called last) │ > │ > ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ > │ Response │◀───│interceptor3 │◀───│interceptor2 │◀───│interceptor1 │◀─────────┘ > └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ > > The interceptors are composed in reverse order due to function composition. > ``` Arguments: * **interceptors** `Interceptor[interceptor[]]`: It is an array of `Interceptor` functions passed as only argument, or several interceptors passed as separate arguments. Returns: `Dispatcher`. #### Parameter: `Interceptor` A function that takes a `dispatch` method and returns a `dispatch`-like function. #### Example 1 - Basic Compose ```js const { Client, RedirectHandler } = require('undici') const redirectInterceptor = dispatch => { return (opts, handler) => { const { maxRedirections } = opts if (!maxRedirections) { return dispatch(opts, handler) } const redirectHandler = new RedirectHandler( dispatch, maxRedirections, opts, handler ) opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting. return dispatch(opts, redirectHandler) } } const client = new Client('http://localhost:3000') .compose(redirectInterceptor) await client.request({ path: '/', method: 'GET' })
const { Client, RedirectHandler, RetryHandler } = require('undici')
const redirectInterceptor = dispatch => {
return (opts, handler) => {
const { maxRedirections } = opts
if (!maxRedirections) {
return dispatch(opts, handler)
}
const redirectHandler = new RedirectHandler(
dispatch,
maxRedirections,
opts,
handler
)
opts = { ...opts, maxRedirections: 0 }
return dispatch(opts, redirectHandler)
}
}
const retryInterceptor = dispatch => {
return function retryInterceptor (opts, handler) {
return dispatch(
opts,
new RetryHandler(opts, {
handler,
dispatch
})
)
}
}
const client = new Client('http://localhost:3000')
.compose(redirectInterceptor)
.compose(retryInterceptor)
await client.request({ path: '/', method: 'GET' })
redirectThe redirect interceptor allows you to customize the way your dispatcher handles redirects.
It accepts the same arguments as the RedirectHandler constructor.
Example - Basic Redirect Interceptor
const { Client, interceptors } = require("undici");
const { redirect } = interceptors;
const client = new Client("http://service.example").compose(
redirect({ maxRedirections: 3, throwOnMaxRedirect: true })
);
client.request({ path: "/" })
retryThe retry interceptor allows you to customize the way your dispatcher handles retries.
It accepts the same arguments as the RetryHandler constructor.
Example - Basic Redirect Interceptor
const { Client, interceptors } = require("undici");
const { retry } = interceptors;
const client = new Client("http://service.example").compose(
retry({
maxRetries: 3,
minTimeout: 1000,
maxTimeout: 10000,
timeoutFactor: 2,
retryAfter: true,
})
);
dumpThe dump interceptor enables you to dump the response body from a request upon a given limit.
Options
maxSize - The maximum size (in bytes) of the response body to dump. If the size of the request's body exceeds this value then the connection will be closed. Default: 1048576.The
Dispatcher#optionsalso gets extended with the optionsdumpMaxSize,abortOnDumped, andwaitForTrailerswhich can be used to configure the interceptor at a request-per-request basis.
Example - Basic Dump Interceptor
const { Client, interceptors } = require("undici");
const { dump } = interceptors;
const client = new Client("http://service.example").compose(
dump({
maxSize: 1024,
})
);
// or
client.dispatch(
{
path: "/",
method: "GET",
dumpMaxSize: 1024,
},
handler
);
dnsThe dns interceptor enables you to cache DNS lookups for a given duration, per origin.
It is well suited for scenarios where you want to cache DNS lookups to avoid the overhead of resolving the same domain multiple times
Options
maxTTL - The maximum time-to-live (in milliseconds) of the DNS cache. It should be a positive integer. Default: 10000.
0 to disable TTL.maxItems - The maximum number of items to cache. It should be a positive integer. Default: Infinity.dualStack - Whether to resolve both IPv4 and IPv6 addresses. Default: true.
affinity - Whether to use IPv4 or IPv6 addresses. Default: 4.
4 or 6.dualStack is false.lookup: (hostname: string, options: LookupOptions, callback: (err: NodeJS.ErrnoException | null, addresses: DNSInterceptorRecord[]) => void) => void - Custom lookup function. Default: dns.lookup.
pick: (origin: URL, records: DNSInterceptorRecords, affinity: 4 | 6) => DNSInterceptorRecord - Custom pick function. Default: RoundRobin.
records property can be mutated to store the state of the balancing algorithm.storage: DNSStorage - Custom storage for resolved DNS recordsThe
Dispatcher#optionsalso gets extended with the optionsdns.affinity,dns.dualStack,dns.lookupanddns.pickwhich can be used to configure the interceptor at a request-per-request basis.
DNSInterceptorRecord It represents a DNS record.
family - (number) The IP family of the address. It can be either 4 or 6.address - (string) The IP address.DNSInterceptorOriginRecords It represents a map of DNS IP addresses records for a single origin.
4.ips - (DNSInterceptorRecord[] | null) The IPv4 addresses.6.ips - (DNSInterceptorRecord[] | null) The IPv6 addresses.DNSStorage It represents a storage object for resolved DNS records.
size - (number) current size of the storage.get - ((origin: string) => DNSInterceptorOriginRecords | null) method to get the records for a given origin.set - ((origin: string, records: DNSInterceptorOriginRecords | null, options: { ttl: number }) => void) method to set the records for a given origin.delete - ((origin: string) => void) method to delete records for a given origin.full - (() => boolean) method to check if the storage is full, if returns true, DNS lookup will be skipped in this interceptor and new records will not be stored.Example - Basic DNS Interceptor
const { Client, interceptors } = require("undici");
const { dns } = interceptors;
const client = new Agent().compose([
dns({ ...opts })
])
const response = await client.request({
origin: `http://localhost:3030`,
...requestOpts
})
Example - DNS Interceptor and LRU cache as a storage
const { Client, interceptors } = require("undici");
const QuickLRU = require("quick-lru");
const { dns } = interceptors;
const lru = new QuickLRU({ maxSize: 100 });
const lruAdapter = {
get size() {
return lru.size;
},
get(origin) {
return lru.get(origin);
},
set(origin, records, { ttl }) {
lru.set(origin, records, { maxAge: ttl });
},
delete(origin) {
lru.delete(origin);
},
full() {
// For LRU cache, we can always store new records,
// old records will be evicted automatically
return false;
}
}
const client = new Agent().compose([
dns({ storage: lruAdapter })
])
const response = await client.request({
origin: `http://localhost:3030`,
...requestOpts
})
responseErrorThe responseError interceptor throws an error for responses with status code errors (>= 400).
Example
const { Client, interceptors } = require("undici");
const { responseError } = interceptors;
const client = new Client("http://service.example").compose(
responseError()
);
// Will throw a ResponseError for status codes >= 400
await client.request({
method: "GET",
path: "/"
});
decompress⚠️ The decompress interceptor is experimental and subject to change.
The decompress interceptor automatically decompresses response bodies that are compressed with gzip, deflate, brotli, or zstd compression. It removes the content-encoding and content-length headers from decompressed responses and supports RFC-9110 compliant multiple encodings.
Options
skipErrorResponses - Whether to skip decompression for error responses (status codes >= 400). Default: true.skipStatusCodes - Array of status codes to skip decompression for. Default: [204, 304].maxSize - Maximum decompressed response size in bytes for each decompression stage. Set to 0 to disable the limit. Default: 0.Example - Basic Decompress Interceptor
const { Client, interceptors } = require("undici");
const { decompress } = interceptors;
const client = new Client("http://service.example").compose(
decompress()
);
// Automatically decompresses gzip/deflate/brotli/zstd responses
const response = await client.request({
method: "GET",
path: "/"
});
Example - Custom Options
const { Client, interceptors } = require("undici");
const { decompress } = interceptors;
const client = new Client("http://service.example").compose(
decompress({
skipErrorResponses: false, // Decompress 5xx responses
skipStatusCodes: [204, 304, 201] // Skip these status codes
})
);
Supported Encodings
gzip / x-gzip - GZIP compressiondeflate / x-compress - DEFLATE compressionbr - Brotli compressionzstd - Zstandard compressiongzip, deflate) are supported per RFC-9110Behavior
content-encoding and content-length headers when decompressingCache InterceptorThe cache interceptor implements client-side response caching as described in
RFC9111.
Options
store - The CacheStore to store and retrieve responses from. Default is MemoryCacheStore.methods - The safe HTTP methods to cache the response of.cacheByDefault - The default expiration time to cache responses by if they don't have an explicit expiration and cannot have an heuristic expiry computed. If this isn't present, responses neither with an explicit expiration nor heuristically cacheable will not be cached. Default undefined.type - The type of cache for Undici to act as. Can be shared or private. Default shared. private implies privately cacheable responses will be cached and potentially shared with other users of your application.Usage with fetch
const { Agent, cacheStores, interceptors, setGlobalDispatcher } = require('undici')
const client = new Agent().compose(interceptors.cache({
store: new cacheStores.MemoryCacheStore({
maxSize: 100 * 1024 * 1024, // 100MB
maxCount: 1000,
maxEntrySize: 5 * 1024 * 1024 // 5MB
})
}))
setGlobalDispatcher(client)
// First request goes to the network and is cached when cache headers allow it.
const first = await fetch('https://example.com/data')
// Second request can be served from cache according to RFC9111 rules.
const second = await fetch('https://example.com/data')
Deduplicate InterceptorThe deduplicate interceptor deduplicates concurrent identical requests. When multiple identical requests are made while one is already in-flight, only one request is sent to the origin server, and all waiting handlers receive the same response. This reduces server load and improves performance.
Options
methods - The safe HTTP methods to deduplicate. Default ['GET'].skipHeaderNames - Header names that, if present in a request, will cause the request to skip deduplication entirely. Useful for headers like idempotency-key where presence indicates unique processing. Header name matching is case-insensitive. Default [].excludeHeaderNames - Header names to exclude from the deduplication key. Requests with different values for these headers will still be deduplicated together. Useful for headers like x-request-id that vary per request but shouldn't affect deduplication. Header name matching is case-insensitive. Default [].maxBufferSize - Maximum bytes buffered per paused waiting deduplicated handler. If a waiting handler remains paused and exceeds this threshold, it is failed with an abort error to prevent unbounded memory growth. Default 5 * 1024 * 1024.Usage
const { Client, interceptors } = require("undici");
const { deduplicate, cache } = interceptors;
// Deduplicate only
const client = new Client("http://service.example").compose(
deduplicate()
);
// Deduplicate with caching
const clientWithCache = new Client("http://service.example").compose(
deduplicate(),
cache()
);
Requests are considered identical if they have the same:
excludeHeaderNames)All deduplicated requests receive the complete response including status code, headers, and body.
For observability, request deduplication events are published to the undici:request:pending-requests diagnostic channel.
'connect'Parameters:
URLArray<Dispatcher>'disconnect'Parameters:
URLArray<Dispatcher>ErrorEmitted when the dispatcher has been disconnected from the origin.
Note: For HTTP/2, this event is also emitted when the dispatcher has received the GOAWAY Frame with an Error with the message
HTTP/2: "GOAWAY" frame receivedand the codeUND_ERR_INFO. Due to nature of the protocol of using binary frames, it is possible that requests gets hanging as a frame can be received between theHEADERandDATAframes. It is recommended to handle this event and close the dispatcher to create a new HTTP/2 session.
'connectionError'Parameters:
URLArray<Dispatcher>ErrorEmitted when dispatcher fails to connect to origin.
'drain'Parameters:
URLEmitted when dispatcher is no longer busy.
UndiciHeadersRecord<string, string | string[] | undefined> | string[] | Iterable<[string, string | string[] | undefined]> | nullHeader arguments such as options.headers in Client.dispatch can be specified in three forms:
Record<string, string | string[] | undefined> (IncomingHttpHeaders) type.InvalidArgumentError will be thrown.Headers, Map, or a custom iterator returning key-value pairs.
Keys are lowercase and values are not modified.Undici validates header syntax at the protocol level (for example, invalid header names and invalid control characters in string values), but it does not sanitize untrusted application input. Validate and sanitize any user-provided header names and values before passing them to Undici to prevent header/body injection vulnerabilities.
When using the array header format (string[]), Undici processes only indexed elements. Additional properties assigned to the array object are ignored.
Response headers will derive a host from the url of the Client instance if no host header was previously specified.
{
'content-length': '123',
'content-type': 'text/plain',
connection: 'keep-alive',
host: 'mysite.com',
accept: '*/*'
}
[
'content-length', '123',
'content-type', 'text/plain',
'connection', 'keep-alive',
'host', 'mysite.com',
'accept', '*/*'
]
new Headers({
'content-length': '123',
'content-type': 'text/plain',
connection: 'keep-alive',
host: 'mysite.com',
accept: '*/*'
})
or
new Map([
['content-length', '123'],
['content-type', 'text/plain'],
['connection', 'keep-alive'],
['host', 'mysite.com'],
['accept', '*/*']
])
or
{
*[Symbol.iterator] () {
yield ['content-length', '123']
yield ['content-type', 'text/plain']
yield ['connection', 'keep-alive']
yield ['host', 'mysite.com']
yield ['accept', '*/*']
}
}