undici icon indicating copy to clipboard operation
undici copied to clipboard

feature: Add low-level API for capturing and managing sessions/clients in Agent

Open PandaWorker opened this issue 6 months ago • 1 comments

I am faced with such a problem that I need to close the connection and the client itself according to the condition

How do I get a connection (Client) before it becomes available and the next request goes through it?

My Agent keeps a pool of connections through different proxy servers, and I need to close clients that don't suit me for any reason.

How can I do this without completely rewriting the clients and agent? It turns out that the response has no access to either the client or the connection.

Maybe make an additional implementation with acquire/release clients or proxy wrappers over clients of an existing agent.

Add low-level API for capturing and managing sessions/clients in Dispatcher

Problem Description

Currently, undici does not provide a convenient way to:

  1. Capture an individual HTTP client (or “session”) from the connection pool (Dispatcher) to perform a series of requests.
  2. Close or release specific connections based on response outcome (e.g., on a 403), without affecting the rest of the pool.
  3. Fine-tune the lifecycle of connections: controlling counts, re-opening, and manual shutdown.

The existing Agent/Dispatcher API only offers a global pool (connections), but lacks something like a session() or acquire() method to carve out its own sub-pool of connections that you can explicitly close or return.


Expected Behavior

  • Method dispatcher.session(options?: { connections?: number; }): Dispatcher & SessionControls

    • Returns a Dispatcher-compatible object (a Session) that:
      • Implements the same interface as Dispatcher (so you can pass it anywhere a Dispatcher is expected).
      • Owns its own subset of TCP connections (optionally limited by connections).
      • Exposes additional methods:
        • close(): Promise<void> — force-close all connections in this session.
        • release(): Promise<void> — gracefully return all open connections back to the parent pool.
  • Customizable at creation: allow passing TLS settings, timeouts, proxy options, etc.

  • Backward compatibility: if session() isn’t called, Dispatcher continues working as before.


Usage Example

import { buildHttpProxyConnector } from '@undicijs/proxy';
import { Agent, request, type Dispatcher } from 'undici';

const connector = buildHttpProxyConnector('http://0.0.0.0:8001/', {
  requestTls: { rejectUnauthorized: false },
});

const dispatcher = new Agent({
  connect: (opts, cb) => connector(opts, cb),
  connections: 50,
});

async function callApiWithSession(dispatcher: Dispatcher) {
  // Acquire a session (which is itself a Dispatcher) with two dedicated connections
  const session = dispatcher.session({ connections: 2 });

  try {
    const resp = await session.request('https://api.ipify.org');
    if (resp.statusCode === 403) {
      // Force-close all connections in this session
      await session.close();
      return null;
    }
    return await resp.body.text();
  } finally {
    // On success or after errors (if not closed), return connections to the pool
    await session.release();
  }
}

async function main() {
  const ip = await callApiWithSession(dispatcher);
  console.log(ip);
}

main();

Motivation

  1. Error handling per-session.
    On a 403 or 5xx error, you can tear down only the affected connections instead of the entire Agent.

  2. Batching requests.
    Some workflows need to group multiple requests under a controlled max-connection session.

  3. Flexible optimization.
    Create multiple sessions with different configs (TLS, timeouts, proxies) without instantiating entirely new Agent objects.


Proposed API Sketch

interface Dispatcher {
  /**
   * Create a new session that uses a subset of this Dispatcher’s pool.
   * The returned object implements the Dispatcher interface.
   */
  session(options?: {
    connections?: number;
    // ...other config
  }): Dispatcher & SessionControls;
}

/** Extra control methods on a Session */
interface SessionControls {
  /** Force-close all TCP connections in this session. */
  close(): Promise<void>;
  /** Gracefully return open connections to the parent pool. */
  release(): Promise<void>;
}

Alternatives & Current Workarounds

  • New Agent(...) per use case — heavyweight, as each Agent creates its own pool.
  • Internal hacks — manually closing Client sockets via resp.body.socket.close() or util.hijack(), which is unstable and unsupported.

An official low-level session API will simplify consumer code, make behavior predictable, and eliminate these fragile workarounds. Please consider adding this in the next major release!

PandaWorker avatar Jun 23 '25 13:06 PandaWorker

Agent manages its pool based on the origin used to establish a request/connection, so that's scoped limited to that fact.

Getting the pool that belongs to that specific origin can be helpful (as stated your description).

Tho, that can only belongs to the Agent itself, because a Pool belongs to a single origin, and each Client belongs to a single socket.

metcoder95 avatar Jun 24 '25 06:06 metcoder95