Dispatcher is the core abstraction used to dispatch HTTP requests in undici.
It extends EventEmitter and defines the low-level dispatch() contract
along with the higher-level request(), stream(), pipeline(),
connect(), and upgrade() methods that every concrete dispatcher
provides.
Dispatcher itself is abstract: its dispatch(), close(), and
destroy() methods throw Error: not implemented. Concrete dispatchers such
as Client, Pool, BalancedPool, and Agent implement the
contract and are what application code instantiates. Requests are not guaranteed
to be dispatched in the order in which they are invoked.
import { Dispatcher, Agent } from 'undici'
const dispatcher = new Agent()
console.log(dispatcher instanceof Dispatcher) // trueclass Dispatcher extends EventEmitterBase dispatcher class. Concrete dispatchers extend it and implement the
dispatch() method on which the other request helpers are built.
dispatcher.close(callback?): Promise<Promise>Promise
is returned only when
callback
is omitted. It
resolves with
void
once the dispatcher is closed.Closes the dispatcher and gracefully waits for enqueued requests to complete
before resolving or invoking callback.
import { createServer } from 'node:http'
import { Client } from 'undici'
import { once } from 'node: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?): Promise<Object><string><UndiciHeaders>null
.<AbortSignal>
|
<EventEmitter>
|
<null>null
.<any>ConnectData
.<Promise>Promise
is returned only when
callback
is omitted. It
resolves with the
data
object described above.Starts two-way communications with the requested resource using the
HTTP CONNECT method.
import { createServer } from 'node:http'
import { Client } from 'undici'
import { once } from 'node:events'
const server = createServer((request, response) => {
throw new 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?): Promisenull
.<Function><Promise>Promise
is returned only when
callback
is omitted. It
resolves with
void
once the dispatcher is destroyed.Destroys the dispatcher abruptly with the given error. All pending and running
requests are asynchronously aborted and rejected. Because the operation is
dispatched asynchronously, dispatched requests may still make some progress
before being aborted.
Both arguments are optional, so the method can be called in four ways:
dispatcher.destroy() // -> Promise
dispatcher.destroy(new Error()) // -> Promise
dispatcher.destroy(() => {}) // -> void
dispatcher.destroy(new Error(), () => {}) // -> voiddispatch(options, handler): boolean<Object><string><string>'GET'
or
'POST'
.<string>
|
<Buffer>
|
<Uint8Array>
|
<Readable>
|
<FormData>
|
<null>null
.<UndiciHeaders>null
.<Object>encodeURIComponent
.
Default:
null
.<boolean>false
,
the request is not sent until all preceding requests in the pipeline have
completed.
Default:
true
when
method
is
'HEAD'
or
'GET'
.<boolean>true
, further pipelining on the same connection is avoided until
headers have been received.
Default:
method !== 'HEAD'
.0
and
255
.
Default:
0
.'Websocket'
.
Default:
null
, or the request method when it is
'CONNECT'
.0
to disable it entirely. Defaults to 300 seconds.<boolean>false
.<boolean>expect: 100-continue
header and halts the request body until a
100 Continue
is received from the
remote server.
Default:
false
.<Object><Function><DispatchController><any><Function>options.upgrade
is set or
options.method
is
'CONNECT'
.<Function>upgrade
requests.<Function>upgrade
requests.<DispatchController><Buffer><Function>upgrade
requests.<DispatchController><Object><Function><DispatchController><Error><Function><Function><Buffer><Function><boolean>false
when the dispatcher is busy and further
dispatch()
calls will make no progress until the
'drain'
event is emitted.This is the low-level API on which all the higher-level methods are built. This API is expected to evolve through semver-major versions and is less stable than the higher-level methods. It is primarily intended for library developers who implement higher-level APIs on top of it.
The controller passed to each handler is a DispatchController with the
following shape:
Call controller.pause() and controller.resume() to apply backpressure rather
than returning false from a handler.
import { createServer } from 'node:http'
import { Client } from 'undici'
import { once } from 'node: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' }
}, {
onRequestStart: () => {
console.log('Connected!')
},
onResponseError: (_controller, error) => {
console.error(error)
},
onResponseStart: (_controller, statusCode, headers) => {
console.log(`statusCode: ${statusCode} | headers: ${JSON.stringify(headers)}`)
},
onResponseData: (_controller, chunk) => {
data.push(chunk)
},
onResponseEnd: (_controller, trailers) => {
console.log(`trailers: ${JSON.stringify(trailers)}`)
console.log(`Data: ${Buffer.concat(data).toString('utf8')}`)
client.close()
server.close()
}
})dispatcher.pipeline(options, handler): Duplex<Object>dispatcher.request()
's
options
, plus:<boolean>true
when
handler
returns an object stream.
Default:
false
.<Function>Readable
from which the result is read.<Object><number><Object><any>opaque
value passed in
options
.<Readable><Object><Readable><Duplex>For easy use with stream.pipeline(). The handler should return a
Readable from which the result is read. Usually it returns the body directly,
unless a transformation is needed based on, for example, headers or
statusCode. The handler should validate the response and save any required
state; if there is an error, it should be thrown.
import { Readable, Writable, PassThrough, pipeline } from 'node:stream'
import { createServer } from 'node:http'
import { Client } from 'undici'
import { once } from 'node: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?): Promise<Object>dispatcher.dispatch()
's
options
, plus:<any>null
.<AbortSignal>
|
<EventEmitter>
|
<null>null
.<Function>
|
<null>null
.'raw'
to return the response headers
as a raw array.
Default:
null
.<number>65536
(64 KiB).<Function><Object><number><string>'OK'
or
'Not Found'
.<Object><Readable><Object>body
emits
'end'
.<any>opaque
value passed in
options
.<Object><Promise>Promise
is returned only when
callback
is omitted. It
resolves with the
data
object described above.Performs an HTTP request. options.method must not be 'CONNECT'.
Non-idempotent requests are not pipelined, to avoid indirect failures. Idempotent requests are automatically retried if they fail due to an 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.
The body exposes the following body mixin methods and properties:
.arrayBuffer(), .blob(), .bytes(), .json(),
.text(), body, and bodyUsed. A body cannot be consumed twice; for
example, calling text() after json() throws a TypeError. The body also
provides dump({ limit }), which discards up to limit bytes (default
131072) without destroying the socket.
The body is always a Readable, even when empty. Deserializing an empty body
with json() throws. To guard against this, verify the status code is not 204
and the content-type header starts with application/json before calling
json().
import { createServer } from 'node:http'
import { Client } from 'undici'
import { once } from 'node: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)
}A request can be aborted with an AbortSignal, with any EventEmitter that
emits an 'abort' event, or by destroying the response body:
import { Client } from 'undici'
const client = new Client('http://localhost:3000')
const abortController = new AbortController()
try {
client.request({ path: '/', method: 'GET', signal: abortController.signal })
} catch (error) {
console.error(error) // RequestAbortedError
}
abortController.abort()When the response body is conditionally read, always fully consume it otherwise:
const { body, statusCode } = await client.request({ path: '/', method: 'GET' })
if (statusCode === 200) {
return await body.arrayBuffer()
}
await body.dump()
return nulldispatcher.stream(options, factory, callback?): Promise<Object>dispatcher.request()
's
options
.<Function>Writable
to which the response is written.<Promise>Promise
is returned only when
callback
is omitted. It
resolves with the
data
object described above.A faster version of dispatcher.request(). The factory returns a
Writable to which the response is written, which avoids creating an
intermediate Readable when the caller intends to pipe the response body
directly to a Writable.
Use options.opaque to avoid creating a closure for the factory method. This
pattern works well with Node.js web frameworks such as Fastify.
import { createServer } from 'node:http'
import { Client } from 'undici'
import { once } from 'node:events'
import { Writable } from 'node: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)
}dispatcher.upgrade(options, callback?): Promise<Object><string><string>'GET'
.<UndiciHeaders>null
.<string>'Websocket'
.<AbortSignal>
|
<EventEmitter>
|
<null>null
.<Promise>Promise
is returned only when
callback
is omitted. It
resolves with the
data
object described above.Upgrades to a different protocol. See MDN: Protocol upgrade mechanism for more details.
import { createServer } from 'node:http'
import { Client } from 'undici'
import { once } from 'node: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?): Dispatcher<Array>
|
<Function><Function><Dispatcher>dispatch()
method.Composes a new dispatcher from the current dispatcher and the given
interceptors. Each interceptor is a function that takes a dispatch method and
returns a dispatch-like function with the same signature
((options, handler)).
The order of the interceptors matters: the last interceptor in the chain is the first to be called. Forking the chain of interceptors can lead to unexpected results.
compose([interceptor1, interceptor2, interceptor3])
Request flow:
Request -> interceptor3 -> interceptor2 -> interceptor1 -> dispatcher.dispatch
(called first) (called second) (called last)Interceptors can be chained by calling compose() repeatedly:
import { Client } from 'undici'
const client = new Client('http://localhost:3000')
.compose(redirectInterceptor)
.compose(retryInterceptor)
await client.request({ path: '/', method: 'GET' })For the full list of built-in interceptors provided by undici, see Interceptors.
<URL>Emitted when the dispatcher has connected to the origin.
Emitted when the dispatcher has been disconnected from the origin.
For HTTP/2, this event is also emitted when the dispatcher receives a
GOAWAY frame, with an error whose message is HTTP/2: "GOAWAY" frame received
and whose code is UND_ERR_INFO. Because of the binary-framing nature of the
protocol, a request may hang if a frame is received between the HEADER and
DATA frames. It is recommended to handle this event and close the dispatcher to
create a new HTTP/2 session.
Emitted when the dispatcher fails to connect to the origin.
<URL>Emitted when the dispatcher is no longer busy and further dispatch() calls
can make progress.
For the full reference of built-in interceptors (dump, retry, redirect,
decompress, responseError, dns, cache, deduplicate) and their options,
see Interceptors.