undici Documentation

repository·main·Indexed 27 days ago

https://github.com/nodejs/undici

A high-performance HTTP/1.1 client for Node.js that powers the built-in fetch implementation. It provides advanced APIs including undici.request(), undici.fetch(), and undici.stream(), as well as specialized agents like ProxyAgent, Socks5Agent, and MockAgent for fine-grained control over connection pooling and HTTP/1.1 pipelining.

Tokens
59.1K
Snippets
159
Records
291
Agent score
92%

What's inside undici

  1. Understand the Dispatcher abstraction

    main

    The Dispatcher class is the core abstraction used to dispatch HTTP requests in undici. It extends EventEmitter and defines the low-level dispatch() contract, as well as higher-level methods like request(), stream(), pipeline(), connect(), and upgrade().

    Note that Dispatcher is an abstract class. You should not instantiate it directly; instead, instantiate concrete implementations such as Client, Pool, BalancedPool, or Agent. Calling dispatch(), close(), or destroy() on a base Dispatcher instance will throw Error: not implemented.

    import { Dispatcher, Agent } from 'undici'
    
    const dispatcher = new Agent()
    console.log(dispatcher instanceof Dispatcher) // true
  2. Use Subresource Integrity (SRI) for web fetch operations

    main
    The subresource-integrity module provides support for Subresource Integrity (SRI) during web fetch operations. SRI is a security feature that enables clients to verify that fetched resources have not been unexpectedly manipulated by checking them against a cryptographic hash.
  3. Use SnapshotAgent for deterministic HTTP testing

    main

    SnapshotAgent (Experimental) records real HTTP responses and replays them on later requests. This allows tests to run against captured data instead of a live network. It extends MockAgent, so it can be used with setGlobalDispatcher() or passed via the dispatcher option. It is compatible with all undici APIs including fetch, request, stream, and pipeline.

    Operating Modes

    • 'record': Performs real requests and writes responses to a snapshot file.
    • 'playback': Serves responses from the snapshot file without network access. Requests with no matching snapshot will reject with an error starting with No snapshot found.
    • 'update': Replays existing snapshots and records any request that has no matching snapshot.
    import { SnapshotAgent, setGlobalDispatcher, fetch } from 'undici'
    
    const agent = new SnapshotAgent({
      mode: 'record',
      snapshotPath: './snapshots/api.json'
    })
    setGlobalDispatcher(agent)
    
    const response = await fetch('https://api.example.com/users')
    const users = await response.json()
    
    await agent.close()
  4. Use RoundRobinPool to distribute requests across connections

    main

    RoundRobinPool is a pool of Client instances connected to the same upstream target that selects clients in a round-robin fashion. Unlike a standard Pool which reuses the first available client, RoundRobinPool cycles through its clients to distribute requests evenly across every open connection.

    When to use: This is ideal when your upstream target is fronted by a load balancer (like a Kubernetes Service) that distributes TCP connections across multiple backend servers. By spreading requests across multiple connections, you ensure the load balancer distributes traffic to different backends.

    Note: RoundRobinPool distributes requests across TCP connections, not backend servers directly. If your load balancer uses sticky sessions or source-IP affinity, you should use BalancedPool instead.

    import { RoundRobinPool } from 'undici'
    
    const pool = new RoundRobinPool('http://localhost:3000', { connections: 10 })
  5. Use EnvHttpProxyAgent to route requests via environment variables

    main

    EnvHttpProxyAgent is a Dispatcher that automatically routes requests through proxies based on the http_proxy, https_proxy, and no_proxy environment variables (or their uppercase variants HTTP_PROXY, HTTPS_PROXY, and NO_PROXY).

    Proxy Selection Logic

    • HTTP requests: Uses http_proxy. If https_proxy is also set, http_proxy is used for HTTP and https_proxy for HTTPS. If only http_proxy is set, it is used for both.
    • HTTPS requests: Uses https_proxy.
    • Bypassing Proxies: The no_proxy variable accepts a comma- or space-separated list of hosts. You can use .example.com or *.example.com to match subdomains, or :port to restrict by port. Setting no_proxy to * bypasses the proxy for all requests.

    Note: If both lowercase and uppercase environment variables are set, the lowercase version takes precedence. The no_proxy value is re-read from the environment on every request unless overridden via the noProxy option.

    Import it from undici:

    import { EnvHttpProxyAgent } from 'undici'
  6. Use MockPool for request interception

    main

    The MockPool class allows you to intercept and mock HTTP requests made to a specific origin. It extends Pool and implements the Interceptable interface. When a request matches a registered interceptor, the MockPool returns a mocked response instead of making a real network request. If no mock matches, a real request is attempted unless network connections are disabled on the MockAgent, in which case a MockNotMatchedError is thrown.

    import { MockAgent, setGlobalDispatcher, request } from 'undici'
    
    const mockAgent = new MockAgent()
    setGlobalDispatcher(mockAgent)
    
    const mockPool = mockAgent.get('http://localhost:3000')
    mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
    
    const { statusCode, body } = await request('http://localhost:3000/foo')
    
    console.log('response received', statusCode) // response received 200
    
    for await (const data of body) {
      console.log('data', data.toString('utf8')) // data foo
    }
  7. Gracefully shut down a Client using close()

    main

    To shut down a Client while ensuring that all currently queued requests are completed, use client.close().

    When client.close() is called while the client is in the pending or processing state with a queue, the client enters the processing.closing sub-state. It will continue to process all outstanding requests and then move gracefully to the destroyed state once finished.

    In contrast, client.destroy() will immediately move the client to the destroyed state, aborting any queued requests.

  8. Prevent connection leaks by consuming or cancelling response bodies

    main

    In Undici, leaving a response body unconsumed can lead to excessive connection usage, reduced performance, and potential deadlocks because Node.js garbage collection is not aggressive enough to release connection resources immediately.

    Best Practices:

    1. Always consume the body: Use a loop or body.dump() to ensure the connection is released.
    2. Use HEAD requests: If you only need headers, use method: 'HEAD' to avoid receiving a body entirely.
    3. Mandatory for request: When using undici.request, you must consume or cancel the body.
    // RECOMMENDED: Consume the body
    const { body, headers } = await fetch(url);
    for await (const chunk of body) { /* consume */ }
    
    // RECOMMENDED: Use HEAD for headers only
    const headers = await fetch(url, { method: 'HEAD' }).then(res => res.headers);
    
    // MANDATORY for undici.request
    const { body, headers } = await request(url);
    await body.dump(); // force consumption
    // Do
    const { body, headers } = await fetch(url);
    for await (const chunk of body) {
      // force consumption of body
    }
    
    // Do not
    const { headers } = await fetch(url);
  9. RedirectHandler behavior and status codes

    main

    The RedirectHandler follows HTTP redirects for the following status codes:

    • 300, 301, 302, 303, 307, and 308.

    Key behaviors:

    • Method Downgrade: For 301/302 responses to a POST request, or for 303 responses with any method other than HEAD, the method is downgraded to GET.
    • Body Handling: Request bodies that have already been consumed are not replayed during redirects.
    • Header Stripping: Headers referring to the original URL (like host) are stripped on each hop. Additional headers specified in stripHeadersOnCrossOriginRedirect are removed on cross-origin redirects.
    • Error Handling: If throwOnMaxRedirect is true, an error is thrown upon reaching the limit. A redirect loop will also trigger an error (e.g., if using a Client or Pool for cross-origin redirects; use an Agent instead).
  10. Handle Undici errors programmatically

    main

    Undici provides typed error objects via the errors namespace. You can distinguish failures using instanceof or by checking the stable error.code string.

    Note: Because the global dispatcher might use a different version of Undici than your local import, it is recommended to match on error.code for maximum reliability across different environments and versions.

    import { errors } from 'undici'
    
    // Recommended: Match on error.code
    if (err.code === 'UND_ERR_CONNECT_TIMEOUT') {
      // handle connect timeout
    }
    
    // Alternative: Match using instanceof
    if (err instanceof errors.ConnectTimeoutError) {
      // handle connect timeout
    }
  11. Guard against unexpected disconnects in tests

    main

    Undici's Client automatically reconnects after socket errors, which can mask bugs like protocol violations or parser errors by allowing a test to pass despite a silent reconnection.

    To catch these, attach a listener to the 'disconnect' event. The guard should only fail if the disconnect occurs while the client is still active (i.e., !client.closed && !client.destroyed).

    When to skip the guard: Do not use the guard if a disconnect is expected behavior, such as:

    • Signal aborts (signal.emit('abort'), ac.abort())
    • Server-side destruction (res.destroy(), req.socket.destroy())
    • Client-side body destruction mid-stream (data.body.destroy())
    • Timeout errors (HeadersTimeoutError, BodyTimeoutError)
    • Successful upgrades (socket is detached from the Client)
    • Retry/reconnect tests where the disconnect triggers the retry
    • HTTP parser errors from malformed responses (HTTPParserError)
    const { Client } = require('undici')
    const { test, after } = require('node:test')
    const { tspl } = require('@matteo.collina/tspl')
    
    test('example with disconnect guard', async (t) => {
      t = tspl(t, { plan: 1 })
    
      const client = new Client('http://localhost:3000')
      after(() => client.close())
    
      client.on('disconnect', () => {
        if (!client.closed && !client.destroyed) {
          t.fail('unexpected disconnect')
        }
      })
    
      // ... test logic ...
    })
  12. Add a new page to the documentation sidebar

    main

    To include a new Markdown file in the documentation navigation:

    1. Create your Markdown file (e.g., api/MyFeature.md).
    2. Open site.json and add a new item to the appropriate group's items array using the format: { "link": "/api/MyFeature", "label": "My Feature" }.