Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 4 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,8 @@ jobs:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['20', '22', '24', '25']
node-version: ['22', '24', '25']
runs-on: ['ubuntu-latest', 'windows-latest', 'macos-latest']
exclude:
- node-version: '20'
runs-on: windows-latest
uses: ./.github/workflows/nodejs.yml
with:
codecov: ${{ (matrix.node-version == '24' || matrix.node-version == '25') && matrix.runs-on == 'ubuntu-latest' }}
Expand All @@ -77,7 +74,7 @@ jobs:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['24', '25']
node-version: ['22', '24', '25']
runs-on: ['ubuntu-latest']
uses: ./.github/workflows/nodejs.yml
with:
Expand All @@ -92,7 +89,7 @@ jobs:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['20', '22', '24', '25']
node-version: ['22', '24', '25']
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
Expand Down Expand Up @@ -178,7 +175,7 @@ jobs:
fail-fast: false
max-parallel: 0
matrix:
node-version: ['20', '22', '24', '25']
node-version: ['22', '24', '25']
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@ jobs:
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}

- name: Test cache-interceptor ${{ inputs.node-version != '20' && 'with' || 'without' }} sqlite
- name: Test cache-interceptor with sqlite
run: npm run test:cache-interceptor
id: test-cache-interceptor
env:
CI: true
NODE_OPTIONS: ${{ inputs.node-version != '20' && '--experimental-sqlite' || '' }}
NODE_OPTIONS: --experimental-sqlite
NODE_V8_COVERAGE: ${{ inputs.codecov == true && './coverage/tmp' || '' }}
UNDICI_NO_WASM_SIMD: ${{ inputs['no-wasm-simd'] }}

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,7 @@ test/request-timeout.10mb.bin
# Claude files
CLAUDE.md
.claude

# Local tooling
.githuman/
.pi/
2 changes: 1 addition & 1 deletion docs/docs/api/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,4 @@ console.log('requests completed')

### Event: `'error'`

Invoked for users errors such as throwing in the `onError` handler.
Invoked for user errors such as throwing in the `onResponseError` handler.
66 changes: 43 additions & 23 deletions docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,26 @@ Returns: `Boolean` - `false` if dispatcher is busy and further dispatch calls wo
* **onResponseEnd** `(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.
* **onResponseError** `(controller: DispatchController, error: Error) => void` - Invoked when an error has occurred. May not throw.

#### Migration from legacy handler API

If you were previously using `onConnect/onHeaders/onData/onComplete/onError`, switch to the new callbacks:

- `onConnect(abort)` → `onRequestStart(controller)` and call `controller.abort(reason)`
- `onHeaders(status, rawHeaders, resume, statusText)` → `onResponseStart(controller, status, headers, statusText)`
- `onData(chunk)` → `onResponseData(controller, chunk)`
- `onComplete(trailers)` → `onResponseEnd(controller, trailers)`
- `onError(err)` → `onResponseError(controller, err)`
- `onUpgrade(status, rawHeaders, socket)` → `onRequestUpgrade(controller, status, headers, socket)`

To access raw header arrays (for preserving duplicates/casing), read them from the controller:

- `controller.rawHeaders` for response headers
- `controller.rawTrailers` for trailers

Pause/resume now uses the controller:

- Call `controller.pause()` and `controller.resume()` instead of returning `false` from handlers.

#### Example 1 - Dispatch GET request

```js
Expand All @@ -236,21 +256,21 @@ client.dispatch({
'x-foo': 'bar'
}
}, {
onConnect: () => {
onRequestStart: () => {
console.log('Connected!')
},
onError: (error) => {
onResponseError: (_controller, error) => {
console.error(error)
},
onHeaders: (statusCode, headers) => {
console.log(`onHeaders | statusCode: ${statusCode} | headers: ${headers}`)
onResponseStart: (_controller, statusCode, headers) => {
console.log(`onResponseStart | statusCode: ${statusCode} | headers: ${JSON.stringify(headers)}`)
},
onData: (chunk) => {
console.log('onData: chunk received')
onResponseData: (_controller, chunk) => {
console.log('onResponseData: chunk received')
data.push(chunk)
},
onComplete: (trailers) => {
console.log(`onComplete | trailers: ${trailers}`)
onResponseEnd: (_controller, trailers) => {
console.log(`onResponseEnd | trailers: ${JSON.stringify(trailers)}`)
const res = Buffer.concat(data).toString('utf8')
console.log(`Data: ${res}`)
client.close()
Expand Down Expand Up @@ -288,15 +308,15 @@ client.dispatch({
method: 'GET',
upgrade: 'websocket'
}, {
onConnect: () => {
console.log('Undici Client - onConnect')
onRequestStart: () => {
console.log('Undici Client - onRequestStart')
},
onError: (error) => {
console.log('onError') // shouldn't print
onResponseError: () => {
console.log('onResponseError') // shouldn't print
},
onUpgrade: (statusCode, headers, socket) => {
console.log('Undici Client - onUpgrade')
console.log(`onUpgrade Headers: ${headers}`)
onRequestUpgrade: (_controller, statusCode, headers, socket) => {
console.log('Undici Client - onRequestUpgrade')
console.log(`onRequestUpgrade Headers: ${JSON.stringify(headers)}`)
socket.on('data', buffer => {
console.log(buffer.toString('utf8'))
})
Expand Down Expand Up @@ -339,21 +359,21 @@ client.dispatch({
},
body: JSON.stringify({ message: 'Hello' })
}, {
onConnect: () => {
onRequestStart: () => {
console.log('Connected!')
},
onError: (error) => {
onResponseError: (_controller, error) => {
console.error(error)
},
onHeaders: (statusCode, headers) => {
console.log(`onHeaders | statusCode: ${statusCode} | headers: ${headers}`)
onResponseStart: (_controller, statusCode, headers) => {
console.log(`onResponseStart | statusCode: ${statusCode} | headers: ${JSON.stringify(headers)}`)
},
onData: (chunk) => {
console.log('onData: chunk received')
onResponseData: (_controller, chunk) => {
console.log('onResponseData: chunk received')
data.push(chunk)
},
onComplete: (trailers) => {
console.log(`onComplete | trailers: ${trailers}`)
onResponseEnd: (_controller, trailers) => {
console.log(`onResponseEnd | trailers: ${JSON.stringify(trailers)}`)
const res = Buffer.concat(data).toString('utf8')
console.log(`Response Data: ${res}`)
client.close()
Expand Down
2 changes: 1 addition & 1 deletion docs/docs/api/H2CClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,4 @@ console.log("requests completed");

### Event: `'error'`

Invoked for users errors such as throwing in the `onError` handler.
Invoked for user errors such as throwing in the `onResponseError` handler.
23 changes: 14 additions & 9 deletions docs/docs/api/RedirectHandler.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,57 +34,62 @@ Returns: `RedirectHandler`

### Methods

#### `onConnect(abort)`
#### `onRequestStart(controller, context)`

Called when the connection is established.
Called when the request starts.

Parameters:

- **abort** `function` - The abort function.
- **controller** `DispatchController` - The request controller.
- **context** `object` - The dispatch context.

#### `onUpgrade(statusCode, headers, socket)`
#### `onRequestUpgrade(controller, statusCode, headers, socket)`

Called when an upgrade is requested.

Parameters:

- **controller** `DispatchController` - The request controller.
- **statusCode** `number` - The HTTP status code.
- **headers** `object` - The headers received in the response.
- **socket** `object` - The socket object.

#### `onError(error)`
#### `onResponseError(controller, error)`

Called when an error occurs.

Parameters:

- **controller** `DispatchController` - The request controller.
- **error** `Error` - The error that occurred.

#### `onHeaders(statusCode, headers, resume, statusText)`
#### `onResponseStart(controller, statusCode, headers, statusText)`

Called when headers are received.

Parameters:

- **controller** `DispatchController` - The request controller.
- **statusCode** `number` - The HTTP status code.
- **headers** `object` - The headers received in the response.
- **resume** `function` - The resume function.
- **statusText** `string` - The status text.

#### `onData(chunk)`
#### `onResponseData(controller, chunk)`

Called when data is received.

Parameters:

- **controller** `DispatchController` - The request controller.
- **chunk** `Buffer` - The data chunk received.

#### `onComplete(trailers)`
#### `onResponseEnd(controller, trailers)`

Called when the request is complete.

Parameters:

- **controller** `DispatchController` - The request controller.
- **trailers** `object` - The trailers received.

#### `onBodySent(chunk)`
Expand Down
25 changes: 12 additions & 13 deletions docs/docs/api/RetryHandler.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,17 +82,16 @@ const handler = new RetryHandler(
return client.dispatch(...args);
},
handler: {
onConnect() {},
onBodySent() {},
onHeaders(status, _rawHeaders, resume, _statusMessage) {
onRequestStart() {},
onBodySent(chunk) {},
onResponseStart(_controller, status, headers) {
// do something with headers
},
onData(chunk) {
onResponseData(_controller, chunk) {
chunks.push(chunk);
return true;
},
onComplete() {},
onError() {
onResponseEnd() {},
onResponseError(_controller, err) {
// handle error properly
},
},
Expand All @@ -107,12 +106,12 @@ const client = new Client(`http://localhost:${server.address().port}`);
const handler = new RetryHandler(dispatchOptions, {
dispatch: client.dispatch.bind(client),
handler: {
onConnect() {},
onBodySent() {},
onHeaders(status, _rawHeaders, resume, _statusMessage) {},
onData(chunk) {},
onComplete() {},
onError(err) {},
onRequestStart() {},
onBodySent(chunk) {},
onResponseStart(_controller, status, headers) {},
onResponseData(_controller, chunk) {},
onResponseEnd() {},
onResponseError(_controller, err) {},
},
});
```
42 changes: 22 additions & 20 deletions docs/examples/proxy/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,42 +49,44 @@ class HTTPHandler {
})
}

onConnect (abort) {
onRequestStart (controller) {
if (this.req.aborted) {
abort()
controller.abort()
} else {
this.abort = abort
this.res.on('close', abort)
this.abort = (reason) => controller.abort(reason)
this.res.on('close', this.abort)
}
}

onHeaders (statusCode, headers, resume) {
onResponseStart (controller, statusCode) {
if (statusCode < 200) {
return
}

this.resume = resume
this.res.on('drain', resume)
this.resume = () => controller.resume()
this.res.on('drain', this.resume)
this.res.writeHead(statusCode, getHeaders({
headers,
headers: controller.rawHeaders ?? [],
proxyName: this.proxyName,
httpVersion: this.httpVersion
}))
}

onData (chunk) {
return this.res.write(chunk)
onResponseData (controller, chunk) {
if (this.res.write(chunk) === false) {
controller.pause()
}
}

onComplete () {
onResponseEnd () {
this.res.off('close', this.abort)
this.res.off('drain', this.resume)

this.res.end()
this.callback()
}

onError (err) {
onResponseError (_controller, err) {
this.res.off('close', this.abort)
this.res.off('drain', this.resume)

Expand All @@ -108,16 +110,16 @@ class WSHandler {
})
}

onConnect (abort) {
onRequestStart (controller) {
if (this.socket.destroyed) {
abort()
controller.abort()
} else {
this.abort = abort
this.socket.on('close', abort)
this.abort = (reason) => controller.abort(reason)
this.socket.on('close', this.abort)
}
}

onUpgrade (statusCode, headers, socket) {
onRequestUpgrade (controller, statusCode, _headers, socket) {
this.socket.off('close', this.abort)

// TODO: Check statusCode?
Expand All @@ -128,8 +130,8 @@ class WSHandler {

setupSocket(socket)

headers = getHeaders({
headers,
const headers = getHeaders({
headers: controller.rawHeaders ?? [],
proxyName: this.proxyName,
httpVersion: this.httpVersion
})
Expand All @@ -144,7 +146,7 @@ class WSHandler {
pipeline(socket, this.socket, socket, this.callback)
}

onError (err) {
onResponseError (_controller, err) {
this.socket.off('close', this.abort)

this.callback(err)
Expand Down
Loading
Loading