
Security News
rv Is a New Rust-Powered Ruby Version Manager Inspired by Python's uv
Ruby maintainers from Bundler and rbenv teams are building rv to bring Python uv's speed and unified tooling approach to Ruby development.
@toxicoder/nestjs-undici
Advanced tools
A lightweight NestJS library built on top of the undici HTTP client. It provides:
This library wraps undici to offer a clean NestJS integration. You can:
pnpm add @toxicoder/nestjs-undici
(or npm/yarn). The undici runtime is included as a dependency of this package; NestJS is a peer dependency.You can use environment variables out of the box by importing the module directly:
// app.module.ts
import { Module } from '@nestjs/common';
import { UndiciModule } from '@toxicoder/nestjs-undici';
@Module({
imports: [UndiciModule],
})
export class AppModule {}
Or provide options explicitly:
import { Module } from '@nestjs/common';
import { UndiciModule } from '@toxicoder/nestjs-undici';
@Module({
imports: [
UndiciModule.forRoot({
baseURL: 'https://api.example.com/',
timeout: 5000,
parse: true,
rawBody: false,
pool: true, // enable per-origin pooling
retry: { retries: 2 }, // wrap dispatcher with RetryAgent
}),
],
})
export class AppModule {}
Async options are supported as well (factory, class, existing):
UndiciModule.forRootAsync({
global: true,
useFactory: async () => ({
baseURL: process.env.API_BASE_URL,
timeout: 3000,
parse: true,
}),
});
Inject UndiciService and call helpers:
import { Injectable } from '@nestjs/common';
import { UndiciService } from '@toxicoder/nestjs-undici';
@Injectable()
export class FooService {
constructor(private readonly http: UndiciService) {}
async getUser(id: string) {
const res = await this.http.get<{ id: string; name: string }>(
`/users/${id}`,
);
if (res.statusCode >= 400) {
// With default error strategy ('throw'), you would not see this branch
}
return res.body; // typed body or null
}
async createUser(dto: { name: string }) {
// JSON is auto-serialized when content-type is JSON or not set
const res = await this.http.post<{ id: string }>(`/users`, dto);
return res.body;
}
}
To stream large payloads set parse to false (globally or per-request) and use the raw readable body:
const res = await this.http.get(`/large-file`, {
/* parse is global; override via service config if needed */
});
// With parse=false at config level → res.body is BodyReadable
// Pipe it to a stream if necessary
@toxicoder/nestjs-undici
.
This re-exports undici's FormData implementation and ensures correct behavior with undici.Content-Type
(or Content-Length
) headers manually when sending FormData.
When you pass undici's FormData as the request body, undici will automatically set
the correct multipart/form-data; boundary=...
header and compute lengths when possible.Example:
import { Injectable } from '@nestjs/common';
import { FormData, UndiciService } from '@toxicoder/nestjs-undici';
@Injectable()
export class UploadService {
constructor(private readonly http: UndiciService) {}
async uploadAvatar(userId: string, bytes: Buffer) {
const fd = new FormData();
fd.append('userId', userId);
// If Blob is available (Node >= 18), wrap the Buffer and optionally set type
const file = new Blob([bytes], { type: 'image/png' });
fd.append('file', file, 'avatar.png');
// Do not set Content-Type manually — undici handles it for FormData
const res = await this.http.post<{ ok: boolean }>(`/upload`, fd);
return res.body;
}
}
Configuration can be provided in three ways:
The full set of options is described by the UndiciConfig interface (see Types and interfaces).
If you rely on UndiciBaseConfig, the following env variables are read:
Variable | Type | Default | Description |
---|---|---|---|
UNDICI_BASE_URL | string | none | Base URL used to resolve relative request paths. If not set, relative paths are not allowed. |
UNDICI_TIMEOUT | number (ms) | none | Default request timeout. Can be overridden per-request via timeout . |
UNDICI_RAW_BODY | boolean | false | When true, include rawBody in the response. |
UNDICI_RAW_PARSE | boolean | true | When false, do not consume/parse the response (enables streaming). |
UNDICI_RAW_RETRY | boolean | false | When true, enables wrapping dispatcher with RetryAgent using default options. |
UNDICI_ERROR_STRATEGY | enum ('throw' | 'pass' | 'intercept') | 'throw' unless response interceptors are present (then 'intercept') | Controls error handling (see Error strategies). |
Notes:
asString
, asNumber
, asBoolean
, asEnum
.parse
defaults to true at runtime unless overridden).{ connections: 10 }
by default.{ throwOnError: false }
by default. Set throwOnError: true
to force throwing regardless of errorStrategy
.Dispatcher selection logic inside the service:
dispatcher
is provided in the service config (module options), it is always used and will be wrapped by RetryAgent
when retry
is enabled.dispatcher
is provided per request (via request options), it is used as-is and is not wrapped. You are responsible for its lifecycle.pool
is false
, a new Agent
is created per request (no connection reuse). This is useful when connecting to the same origin with different TLS configs or when avoiding persistent connections. These transient Agents are automatically closed after the request completes.pool
is true
, a Pool
is created and cached per origin. The TLS config used for the first request to an origin is reused for subsequent requests to the same origin. When pooling is enabled but you don't provide explicit pool options, the default is { connections: 10 }
.Set retry
to:
true
to enable undici's RetryAgent
with default retry optionsUndiciRetryOptions
object to configure retriesfalse
(default) to disable retry wrappingDefaults and behavior:
retry
is enabled without explicit options, the default is { throwOnError: false }
.throwOnError
is false
, whether an error is thrown or handled depends on your errorStrategy
(see below).throwOnError
is true
, the retry agent will throw on request/connection failure regardless of the configured errorStrategy
.You can provide TLS options (ca, key, cert). They are applied to the Agent/Pool creation when no custom dispatcher is supplied:
import { readFileSync } from 'node:fs';
UndiciModule.forRoot({
baseURL: 'https://secure.example.com',
tls: {
ca: readFileSync('ca.pem'),
cert: readFileSync('cert.pem'),
key: readFileSync('key.pem'),
},
});
parse: true
(default): consumes and parses the response body according to the content-type
header.
text/*
→ body and rawBody are stringsapplication/json
and *+json
→ body is JSON (parsed object), rawBody is the original stringArrayBuffer
parse: false
: the service returns the undici BodyReadable
without consumption/parsing (streaming mode). Interceptors receive the raw stream.You can control parse
globally or per request.
Undici in Node.js does not aggressively or deterministically GC unused response bodies. If you leave the body unconsumed, you can leak connections, reduce connection reuse, and even stall when running out of connections.
Therefore, when you use parse: false
in this library, you (the caller) are responsible for consuming or canceling the response body yourself — either inside a response interceptor or in your application code.
Do:
const { body, headers } = await undiciService.get(url, { parse: false });
for await (const _ of body as any) {
// Consume the stream to release the connection
}
Do not:
const { headers } = await undiciService.get(url, { parse: false });
// WRONG: body is left unconsumed → can leak connections
If you need only headers, call the dedicated method:
const headers = await undiciService.headers(url);
throw
(default unless response interceptors exist): throws ResponseStatusCodeError
for HTTP status >= 400. The error body is consumed if parsing would not happen later.pass
: returns the response with an error
property (an instance of ResponseStatusCodeError
); error.body
and body
are parsed according to the parse rules.intercept
: calls response interceptors with the parsed body and the error; if there are no interceptors, the error is thrown.You can set the error strategy globally via module config or per-request via errorStrategy
option.
You can register interceptors globally (via module config) and manage them at runtime, or provide per-request interceptors in request options.
UndiciRequestInterceptor
receive a shallow copy of the request config (headers, query, and plain-object body are shallow-cloned per interceptor) allowing safe mutation without leaking changes across interceptors. Non-plain bodies (Buffer, ArrayBuffer, TypedArray, URLSearchParams, FormData, stream-like) are kept by reference.UndiciResponseInterceptor
run in order and can transform the response and/or inspect the error. They receive the parsed body when parse: true
is enabled, otherwise the raw stream.UndiciService.interceptors
API.Global (module-level) configuration example:
UndiciModule.forRoot({
baseURL: 'https://api.example.com',
requestInterceptors: [
async (cfg) => ({
...cfg,
headers: { ...(cfg.headers ?? {}), Authorization: `Bearer ${token}` },
}),
],
responseInterceptors: [
async (resp, err) => {
if (err) {
// wrap error details
return { ...resp, body: { error: true, data: resp.body } };
}
return resp;
},
],
});
Per-request override (replaces the global interceptors for this call):
await http.get('/users', {
requestInterceptors: [
async (cfg) => ({
...cfg,
headers: { ...(cfg.headers ?? {}), 'x-req-id': crypto.randomUUID() },
}),
],
responseInterceptors: [
async (resp) => ({ ...resp, headers: { ...resp.headers, 'x-doc': 'ok' } }),
],
errorStrategy: 'intercept',
rawBody: true,
parse: true,
});
Managing interceptors at runtime:
import { UndiciService } from '@toxicoder/nestjs-undici';
@Injectable()
class ApiService {
constructor(private readonly http: UndiciService) {
// Add a request interceptor dynamically
const rq = this.http.interceptors.request.add(async (cfg) => {
return { ...cfg, headers: { ...(cfg.headers ?? {}), 'x-runtime': '1' } };
});
// Add a response interceptor dynamically
const rs = this.http.interceptors.response.add(async (res, err) => {
if (err) {
return res; // inspect error if needed
}
return { ...res, headers: { ...res.headers, 'x-seen': 'true' } };
});
// You can later remove them when no longer needed
this.http.interceptors.request.remove(rq);
this.http.interceptors.response.remove(rs);
}
}
Notes
errorStrategy
becomes intercept
when response interceptors are present. You can always set errorStrategy
explicitly per request.Most configuration options can be overridden per request by passing an options
object to the helper methods or to request()
:
Note: retry
is configured at the service level. If you supply a custom dispatcher per request, retry wrapping is bypassed.
UndiciService supports compile-time type-safety for response bodies with a flexible model using the TypeSafety
enum.
import { UndiciService, TypeSafety } from '@toxicoder/nestjs-undici'
Concepts:
UndiciResponse<TBody, TRaw, TSafety>
where:
TBody
— the expected body type after parsing (e.g., a DTO or interface)TRaw
— the raw body type when rawBody: true
(defaults to string | Buffer | ArrayBuffer
)TSafety
— affects whether the return type is a union with a potential errorModes:
{ body: TBody | null; rawBody: TRaw | null }
(rawBody present only when rawBody: true
){ error: Error; body: TBody | BodyReadable | null; rawBody: TRaw | BodyReadable | null }
errorStrategy
is not 'throw'.Using GUARDED (default):
@Injectable()
export class ApiService {
constructor(private readonly http: UndiciService) {}
async getUser(id: string): Promise<{ id: string; name: string } | null> {
const res = await this.http.get<{ id: string; name: string }>(
`/users/${id}`,
);
// res: UndiciResponse<{id: string; name: string}, string|Buffer|ArrayBuffer, TypeSafety.GUARDED>
if ('error' in res) {
// handle error; res.body may be parsed JSON, text, ArrayBuffer or null depending on config
throw res.error;
}
return res.body; // typed as {id: string; name: string} | null
}
}
Specifying the return type for request():
// TRaw defaults to string | Buffer | ArrayBuffer when omitted
const res = await http.request<{ ok: boolean }>({
path: '/ping',
method: 'GET',
});
Opting into UNSAFE at the service type level:
@Injectable()
export class UnsafeApiService {
// Note the generic TypeSafety.UNSAFE on the service type annotation
constructor(private readonly http: UndiciService<TypeSafety.UNSAFE>) {}
async create(dto: any) {
const res = await this.http.post<{ id: string }>(`/items`, dto);
// res.body is typed as { id: string } (no union), which is convenient but potentially unsafe
return res.body;
}
}
Important: When to use TypeSafety.UNSAFE
Strict warning about UNSAFE with 'pass'
errorStrategy = 'pass'
is extremely dangerous: the function will appear to return a successful shape at compile time while, at runtime, the response may actually contain an error. This very likely leads to runtime crashes or invalid assumptions in your code. Avoid this combination.Cheat sheet examples:
// Default GUARDED mode
const a = await http.get<{ ok: true }>(`/ok`);
// a is union (success | error) unless errorStrategy='throw'
// Explicit UNSAFE service annotation
const httpUnsafe: UndiciService<TypeSafety.UNSAFE> = http;
const b = await httpUnsafe.get<{ ok: true }>(`/ok`);
// b is success-only in types, i.e. { ok: true }
UndiciModule
(default provider uses UndiciBaseConfig
):
forRoot(options: UndiciConfig): DynamicModule
forRootAsync(options: UndiciAsyncOptions): DynamicModule
Properties
interceptors: { request: RequestInterceptors; response: ResponseInterceptors }
— allows runtime management of interceptors (add
/remove
).Methods
get<TBody, TRaw = string | Buffer | ArrayBuffer>(url, options?)
post<TBody, TRaw = string | Buffer | ArrayBuffer>(url, body, options?)
put<TBody, TRaw = string | Buffer | ArrayBuffer>(url, body, options?)
patch<TBody, TRaw = string | Buffer | ArrayBuffer>(url, body, options?)
delete<TBody, TRaw = string | Buffer | ArrayBuffer>(url, options?)
headers(input: RequestInfo, options?: Omit<RequestInit, 'method'>): Promise<Headers>
— performs a HEAD request using undici.fetch and returns response headers. Use this when you need only headers without consuming the body.request<TBody, TRaw>(options: UndiciRequestOptions)
— low-level method used by helpers. Provide path
(absolute URL or relative when baseURL
is set) and any per-request overrides: headers
, timeout
, signal
, dispatcher
, rawBody
, parse
, tls
, errorStrategy
, requestInterceptors
, responseInterceptors
.Behavioral notes:
headers['content-type']
is JSON or missing and body is a plain object, the body is auto-serialized to JSON and the header is set to application/json
.url
requires baseURL
to be set; absolute URLs are used as-is.UndiciConfig
: configuration (baseURL, timeout, interceptors, dispatcher, rawBody, parse, tls, pool, retry, errorStrategy)UndiciRequestOptions
: per-request options (path, headers, body, timeout, signal, dispatcher, rawBody, parse, tls, errorStrategy, request/response interceptors). Note: retry
is configured at service level; providing a per-request dispatcher bypasses retry wrapping.UndiciRequestConfig
: internal request configuration (url, headers, body, timeout, tls, signal, and the same override options)UndiciResponse<TBody, TRaw>
: response union with typed body
and optional rawBody
and error
UndiciRequestInterceptor
, UndiciResponseInterceptor
RequestInterceptors
, ResponseInterceptors
, Interceptors
(runtime classes to manage and apply interceptors)UndiciTlsOptions
, UndiciRetryOptions
UndiciAsyncOptions
and UndiciConfigFactory
Unit tests (see tests/*.spec.ts) cover:
parse: false
, including error casesUndiciService.headers()
HEAD helperFrom the repository root:
pnpm exec jest -c jest.config.ts
npx jest -c jest.config.ts
Jest config: jest.config.ts
(rootDir='./', testMatch='/tests/*.spec.ts')
baseURL
or use absolute URLs.import { FormData } from '@toxicoder/nestjs-undici'
). Do not set Content-Type
or Content-Length
manually; undici sets the appropriate multipart/form-data
boundary and lengths automatically when FormData is used.parse=false
to stream the response instead of buffering it into memory.connect
options used for subsequent requests to that origin.intercept
: Ensure at least one response interceptor is configured; otherwise the error will be thrown.retry
, configure it appropriately to avoid retry storms.onModuleDestroy
is called (Nest will do this on application shutdown) to close connections.This project is licensed under the ISC License (see package.json).
FAQs
Undici module for NestJS
The npm package @toxicoder/nestjs-undici receives a total of 9 weekly downloads. As such, @toxicoder/nestjs-undici popularity was classified as not popular.
We found that @toxicoder/nestjs-undici demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Ruby maintainers from Bundler and rbenv teams are building rv to bring Python uv's speed and unified tooling approach to Ruby development.
Security News
Following last week’s supply chain attack, Nx published findings on the GitHub Actions exploit and moved npm publishing to Trusted Publishers.
Security News
AGENTS.md is a fast-growing open format giving AI coding agents a shared, predictable way to understand project setup, style, and workflows.