Skip to Content
Correlation IDConfiguration

Unreleased changes

These pages document main, which has changes 2.1.1 does not. npm install @evanion/nestjs-correlation-id gives you 2.1.1, documented here. Values on these pages are produced by running the current source on every build.

Configuration

To change the header, the id generator or the check an incoming id passes, pass a Partial<CorrelationConfig> to CorrelationModule.forRoot() in your root module. CorrelationConfig has three fields, each with a default.

FieldDefaultWhat it does
header'X-Correlation-Id'read from the request, written to the response
generatorrandomUUID from node:cryptogenerates an id when no id that passes validate arrived
validateDEFAULT_CORRELATION_ID_VALIDATORdecides whether an incoming id is reused

Pass a configuration to CorrelationModule.forRoot

The configuration below gives @evanion/nestjs-correlation-id all three fields for Baize’s orders service. It generates ids of the form <service>-<6 hex digits>, carries them in X-Request-Id, and accepts only that shape back. In your application the object goes where Getting started left forRoot() empty, in the root module’s imports: [CorrelationModule.forRoot({ … })].

To show what forRoot() built, the code below builds the module on its own as a Nest application context and reads the configuration back through CORRELATION_CONFIG_TOKEN (Carry the id through a job):

import { } from '@nestjs/core'; import { , , type CorrelationConfig, } from '@evanion/nestjs-correlation-id'; const = await .( .({ : 'X-Request-Id', : () => `orders-${.().(0, 6)}`, : () => /^(storefront|orders|stock)-[0-9a-f]{6}$/.(), }), { : false }, ); const = .<CorrelationConfig>(); await .(); .; // -> 'X-Request-Id' .?.('storefront-4f1c9a'); // -> true .?.('storefront-4f1c9a, orders-0f3a2b'); // -> false .?.(.()); // -> true

forRoot() fills in every field it is not given, or is given as undefined, so a provider that injects CORRELATION_CONFIG_TOKEN always gets a complete object. CorrelationConfig still types validate as optional, so the code calls it with ?., and ?. never skips the call. The last line checks the generator against the validator: an id the service generates has to pass its own check when the next service sends it back.

Choose the correlation header

@evanion/nestjs-correlation-id reads and writes one header. No correlation header is registered with the Internet Assigned Numbers Authority (IANA), so there is no canonical spelling to defer to. X-Correlation-Id and X-Request-Id are the two in common use; pick the one the rest of your stack already sends. Every service in the chain has to read the header the previous one sends: if storefront sends X-Correlation-Id and orders reads X-Request-Id, orders generates a new id.

The middleware matches the request header case-insensitively, because Node lowercases incoming header names. The casing you configure is the casing that goes out on the response and on calls through withCorrelation().

Choose the id generator

The @evanion/nestjs-correlation-id middleware calls the generator only when no id that passes validate arrived, so a counter-backed generator advances once per request that arrived without a usable id.

Under the default validator, a generator whose ids run over 128 characters, or contain a character the validator refuses, such as the +, / and = of base64, produces ids the same application rejects when they come back in as a header. randomUUID produces 36 characters from hex digits and hyphens.

Decide which incoming ids are reused

@evanion/nestjs-correlation-id puts an incoming id that passes validate into the response header and into whatever the application logs. DEFAULT_CORRELATION_ID_VALIDATOR accepts 1 to 128 characters, each a letter from A to Z in either case, a digit, an underscore, a dot, a colon or a hyphen.

import { } from '@evanion/nestjs-correlation-id'; ('storefront-4f1c9a'); // -> true ('0f3a2b7c-41d2-4e3a-9f55-2c1d4e6a8b90'); // -> true ('storefront-4f1c9a, orders-0f3a2b'); // -> false ('orders-0f3a2b\r\nX-Admin: 1'); // -> false ('a'.(129)); // -> false

What the default check refuses:

  • A carriage return (CR) or a line feed (LF). Node already rejects both in a header, incoming and outgoing. The check also covers an id from another source, such as a queue message, when your consumer passes it through validate the way RestockJob does on Carry the id through a job. A CR or LF in a log line forges a second line.
  • Anything over 128 characters, which bounds what one request can append to every log line it touches.
  • A comma. Node joins a repeated request header into one comma-separated string, so one request that carries the header twice gets a fresh id.

An id that fails is replaced by a generated one, and getCorrelationId() returns the generated id. The middleware also writes a generated id onto req.headers, so code that reads the header rather than CorrelationService sees it. It does so only when the request carried no header, so after a rejection req.headers still holds the rejected value.

Replace DEFAULT_CORRELATION_ID_VALIDATOR

To accept only the shape your services generate, pass your own validate to forRoot(), and @evanion/nestjs-correlation-id applies it in place of the default. Baize’s orders service passes /^(storefront|orders|stock)-[0-9a-f]{6}$/, which accepts no id the default refuses. A caller outside your services then gets its id replaced: a service still on the default generator sends a randomUUID id, and this validator rejects it.

Six hex digits give 16,777,216 values. By the birthday bound, n random ids drawn from N values contain two equal ones with even odds once n reaches about 1.18 × √N, which for these ids is about 4,800. A service that handles more requests than that within the window you search logs over needs a longer suffix.

Call CorrelationModule.forRoot once

forRoot() registers the @evanion/nestjs-correlation-id module global: true, because CorrelationIdMiddleware, withCorrelation() and every provider that reads an id inject CorrelationService or CORRELATION_CONFIG_TOKEN. A global module’s providers resolve in every module of the application. Without that, every consuming module would import CorrelationModule itself.

A second forRoot() creates a second CorrelationService and a second configuration. Nest then injects the second copy into the providers of the module whose imports list that second forRoot(), and the first copy everywhere else. Each copy keeps its own AsyncLocalStorage, so a provider holding a copy the middleware did not use reads undefined inside every request. Nest raises no error and prints no warning.

Read the configuration from the environment

CorrelationModule in @evanion/nestjs-correlation-id has no forRootAsync(). The configuration is a plain object, fixed when the root module is declared, and ConfigService cannot be injected into it. Read an environment value from process.env in the forRoot() call, for example forRoot({ header: process.env['CORRELATION_HEADER'] }). When the variable is unset, header is undefined and forRoot() falls back to X-Correlation-Id.

Last updated on