# Migrating from Undici 7 to 8

<!--type=misc-->

This guide covers the changes you are most likely to hit when upgrading an
application or library from Undici v7 to v8.

## Before you upgrade

- Make sure your runtime is Node.js `>= 22.19.0`.
- If you have custom dispatchers, interceptors, or handlers, review the
  handler API changes before updating.
- If you rely on HTTP/1.1-only behavior, plan to set `allowH2: false`
  explicitly.

## 1. Update your Node.js version

Undici v8 requires Node.js `>= 22.19.0`.

If you are still on Node.js 20 or an older Node.js 22 release, upgrade Node.js
first:

```bash
node -v
```

If that command prints a version lower than `v22.19.0`, upgrade Node.js before
installing Undici v8.

## 2. Migrate custom dispatcher handlers to the v2 API

Undici v8 uses the newer dispatcher handler API consistently.

If you implemented custom dispatchers, interceptors, or wrappers around
`dispatch()`, update legacy callbacks such as `onConnect`, `onHeaders`, and
`onComplete` to the newer callback names.

### Old handler callbacks vs. v8 callbacks

| Undici 7 style | Undici 8 style |
|---|---|
| `onConnect(abort, context)` | `onRequestStart(controller, context)` |
| `onHeaders(statusCode, rawHeaders, resume, statusText)` | `onResponseStart(controller, statusCode, headers, statusText)` |
| `onData(chunk)` | `onResponseData(controller, chunk)` |
| `onComplete(trailers)` | `onResponseEnd(controller, trailers)` |
| `onError(err)` | `onResponseError(controller, err)` |
| `onUpgrade(statusCode, rawHeaders, socket)` | `onRequestUpgrade(controller, statusCode, headers, socket)` |

### Example

Before:

```js
client.dispatch(options, {
  onConnect (abort) {
    this.abort = abort
  },
  onHeaders (statusCode, headers, resume) {
    this.resume = resume
    return true
  },
  onData (chunk) {
    chunks.push(chunk)
    return true
  },
  onComplete (trailers) {
    console.log(trailers)
  },
  onError (err) {
    console.error(err)
  }
})
```

After:

```js
client.dispatch(options, {
  onRequestStart (controller) {
    this.controller = controller
  },
  onResponseStart (controller, statusCode, headers, statusText) {
    console.log(statusCode, statusText, headers)
  },
  onResponseData (controller, chunk) {
    chunks.push(chunk)
  },
  onResponseEnd (controller, trailers) {
    console.log(trailers)
  },
  onResponseError (controller, err) {
    console.error(err)
  }
})
```

### Pause, resume, and abort now go through the controller

In Undici v7, legacy handlers could return `false` or keep references to
`abort()` and `resume()` callbacks. In Undici v8, use the controller instead:

```js
onRequestStart (controller) {
  this.controller = controller
}

onResponseData (controller, chunk) {
  controller.pause()
  setImmediate(() => controller.resume())
}

onResponseError (controller, err) {
  controller.abort(err)
}
```

### Raw headers and trailers moved to the controller

If you need the raw header arrays, read them from the controller:

- `controller.rawHeaders`
- `controller.rawTrailers`

## 3. Update `onBodySent()` handlers

If you implemented `onBodySent()`, note that its signature changed.

Before, handlers received counters:

```js
onBodySent (chunkSize, totalBytesSent) {}
```

In Undici v8, handlers receive the actual chunk:

```js
onBodySent (chunk) {}
```

If you need a notification that the whole body has been sent, use
`onRequestSent()`:

```js
onRequestSent () {
  console.log('request body fully sent')
}
```

## 4. If you need HTTP/1.1 only, disable HTTP/2 explicitly

Undici v8 enables HTTP/2 by default when a TLS server negotiates it via ALPN.

If your application depends on HTTP/1.1-specific behavior, set `allowH2: false`
explicitly.

Before:

```js
const client = new Client('https://example.com')
```

After, to keep HTTP/1.1 only:

```js
const client = new Client('https://example.com', {
  allowH2: false
})
```

The same applies when you configure an `Agent`:

```js
const agent = new Agent({
  allowH2: false
})
```

## 5. Use real `Blob` and `File` instances

Undici v8 no longer accepts fake Blob-like values that only imitate `Blob` or
`File` via properties such as `Symbol.toStringTag`.

If you were passing custom objects that looked like `Blob`s, replace them with
actual `Blob` or `File` instances:

```js
const body = new Blob(['hello'])
```

## 6. Avoid depending on the internal global dispatcher symbol

`setGlobalDispatcher()` and `getGlobalDispatcher()` remain the public APIs and
should continue to be used.

Internally, Undici v8 stores its dispatcher under
`Symbol.for('undici.globalDispatcher.2')` and mirrors a v1-compatible wrapper
for legacy consumers such as Node.js built-in `fetch`.

If your code was reading or writing `Symbol.for('undici.globalDispatcher.1')`
directly, migrate to the public APIs instead:

```js
import { setGlobalDispatcher, getGlobalDispatcher, Agent } from 'undici'

setGlobalDispatcher(new Agent())
const dispatcher = getGlobalDispatcher()
```

If you must expose a dispatcher to legacy v1 handler consumers, wrap it with
`Dispatcher1Wrapper`:

```js
import { Agent, Dispatcher1Wrapper } from 'undici'

const legacyCompatibleDispatcher = new Dispatcher1Wrapper(new Agent())
```

## 7. Verify the upgrade

After moving to Undici v8, it is worth checking these paths in your test suite:

- requests that use a custom `dispatcher`
- `setGlobalDispatcher()` behavior
- any custom interceptor or retry handler
- uploads that use `Blob`, `File`, or `FormData`
- integrations that depend on HTTP/1.1-only behavior

## Related documentation

- [Dispatcher](../api/Dispatcher.md)
- [Client](../api/Client.md)
- [Global Installation](../api/GlobalInstallation.md)
- [Undici Module vs. Node.js Built-in Fetch](./undici-vs-builtin-fetch.md)
