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.
API reference
@evanion/nestjs-correlation-id exports one module, one middleware, one
service, one function that configures HttpModule, one configuration interface
and four constants.
CorrelationModule
CorrelationModule provides CorrelationService and the resolved
configuration to the whole application.
class CorrelationModule {
static forRoot(config?: Partial<CorrelationConfig>): DynamicModule;
}forRoot() fills in every unset field of config and returns the module,
registered global: true. It provides and exports CorrelationService, and the
configuration under CORRELATION_CONFIG_TOKEN.
Call it once, in the root module. A second call creates a second
CorrelationService and configuration, and a provider holding the copy the
middleware did not use reads undefined inside every request.
import { MiddlewareConsumer, , NestModule } from '@nestjs/common';
import {
,
,
} from '@evanion/nestjs-correlation-id';
@({
: [.()],
})
export class implements NestModule {
(: MiddlewareConsumer) {
.().('*');
}
}CorrelationIdMiddleware
CorrelationIdMiddleware opens a correlation context around every request it
is applied to.
class CorrelationIdMiddleware implements NestMiddleware {
constructor(
correlationService: CorrelationService,
correlationConfig: CorrelationConfig,
);
use(
req: IncomingMessage,
res: ServerResponse,
next: (error?: unknown) => void,
): void;
}Per request it does five things, in order:
- Reads the configured header, case-insensitively. A repeated header arrives as an array or as one comma-joined string, and both become one comma-joined string, which the default validator rejects.
- Reuses that value when
validateaccepts it, and calls the generator otherwise. - Writes the id onto
req.headerswhen the request carried no header, so a proxy or an access logger reading the raw header sees the same id. A header that arrived stays as it came in, including onevalidaterejected. - Sets the response header, unless something already set it.
- Calls
nextinsideCorrelationService.run(id, next).
req and res are typed against node:http, so the class runs under the
Express and the Fastify adapters. Apply middleware that reads the id after this
one.
import {
,
,
} from '@evanion/nestjs-correlation-id';
const = { : 'X-Correlation-Id', : () => 'orders-0f3a2b' };
const = new ();
// The middleware opens its contexts on the service it is given.
const = new (, );
// The middleware reads the request's headers and writes the id with setHeader.
// It leaves a response header that something earlier already set, so it asks
// getHeader first, and this stub answers that nothing did. `as never` lets the
// stubs stand in for Node's full IncomingMessage and ServerResponse.
const = { : () => , : () => };
const = (: <string, string>) => {
let : string | undefined;
.({ } as never, as never, () => {
= .();
});
return ;
};
({ 'x-correlation-id': 'storefront-4f1c9a' }); // -> 'storefront-4f1c9a'
({}); // -> 'orders-0f3a2b'
.(); // -> undefinedCorrelationService
CorrelationService reads, replaces and generates the correlation id, and opens a
correlation context. A correlation context is the callback passed to run(),
plus everything it calls or awaits.
@Injectable()
class CorrelationService {
constructor(correlationConfig: CorrelationConfig);
run<T>(correlationId: string, callback: () => T): T;
getCorrelationId(): string | undefined;
setCorrelationId(correlationId: string): void;
generate(): string;
}| Method | What it does |
|---|---|
run(id, callback) | runs callback in a correlation context and returns what it returns |
getCorrelationId() | the surrounding context’s id, or undefined outside one; synchronous |
setCorrelationId(id) | replaces the surrounding context’s id; throws outside one |
generate() | a fresh id from the configured generator |
The service is a singleton over AsyncLocalStorage, and injecting it leaves the
scope of the provider holding it unchanged. Call setCorrelationId in a context
opened before the right id was known, such as a queue consumer that wraps every
handler in run(generate(), …) and reads the message’s id afterwards. It leaves
the response header with the id the middleware wrote on arrival. The code below
builds a worker with no HTTP server, whose generator returns orders-7c41d2:
import { } from '@nestjs/core';
import {
,
,
} from '@evanion/nestjs-correlation-id';
const = await .(
.({ : () => 'orders-7c41d2' }),
{ : false },
);
const = .();
const = .(.(), () =>
.(),
);
let : string | undefined;
.('orders-0f3a2b', () => {
.('storefront-4f1c9a');
= .();
});
let = '';
try {
.('storefront-4f1c9a');
} catch () {
= ( as Error).;
}
await .();
; // -> 'orders-7c41d2'
; // -> 'storefront-4f1c9a'
; // -> 'setCorrelationId() was called outside a correlation context. Apply CorrelationIdMiddleware, or wrap the work in CorrelationService.run().'withCorrelation(config?)
withCorrelation() returns options for HttpModule.registerAsync that send the
current correlation id on every outgoing request.
function withCorrelation(config?: HttpModuleOptions): HttpModuleAsyncOptions;config reaches the axios instance unchanged. The id is attached by an axios
request interceptor, registered as an extra provider under
CORRELATION_AXIOS_INTERCEPTOR, which reads
the context as each request goes out. No header is attached outside a context.
It requires CorrelationModule.forRoot() somewhere in the application; without
it Nest fails at boot with
Nest can't resolve dependencies of the @evanion/nestjs-correlation-id:AXIOS_INTERCEPTOR (AXIOS_INSTANCE_TOKEN, ?, @evanion/nestjs-correlation-id:CORRELATION_CONFIG).
The provider reaches the axios instance through 'AXIOS_INSTANCE_TOKEN', the
string token @nestjs/axios registers it under, and the package imports
@nestjs/axios for its types only. An application that never calls
withCorrelation does not need @nestjs/axios installed.
In the code below, StockClient is the application’s own HttpService
consumer, shown on
Forward the id to the next service, and
StockModule registers it:
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { withCorrelation } from '@evanion/nestjs-correlation-id';
import { StockClient } from './stock.client.js';
@Module({
imports: [HttpModule.registerAsync(withCorrelation({ timeout: 5000 }))],
providers: [StockClient],
exports: [StockClient],
})
export class StockModule {}CorrelationConfig
CorrelationConfig is the configuration forRoot() resolves and provides under
CORRELATION_CONFIG_TOKEN.
interface CorrelationConfig {
header: string;
generator: () => string;
validate?: (value: string) => boolean;
}| Field | What it is |
|---|---|
header | the header read from the request and written to the response, in the casing sent on the wire |
generator | generates an id when the request carried none that validate accepts |
validate | decides whether an incoming id is reused; defaults to DEFAULT_CORRELATION_ID_VALIDATOR |
forRoot() takes a Partial<CorrelationConfig> and fills in every field,
validate included, so a provider injecting the token never sees a partial
object. validate is optional in the type because CorrelationIdMiddleware
falls back to DEFAULT_CORRELATION_ID_VALIDATOR for a configuration built
without forRoot(), as a unit test builds one, that has no validate.
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
.?.(.()); // -> trueCORRELATION_ID_HEADER
CORRELATION_ID_HEADER is the header forRoot() uses when config.header is
not set.
const CORRELATION_ID_HEADER = 'X-Correlation-Id';No correlation header is registered with the Internet Assigned Numbers
Authority (IANA). X-Correlation-Id and X-Request-Id are the two in common
use.
import { } from '@evanion/nestjs-correlation-id';
; // -> 'X-Correlation-Id'CORRELATION_CONFIG_TOKEN
CORRELATION_CONFIG_TOKEN is the injection token of the resolved
CorrelationConfig.
const CORRELATION_CONFIG_TOKEN =
'@evanion/nestjs-correlation-id:CORRELATION_CONFIG';The token is namespaced because forRoot() registers it in a global: true
module, where a bare 'CORRELATION_CONFIG' would collide with any other package
that chose the same string. It is a string because a Symbol() is unique per
copy of the module: two copies of the package would mint two tokens, and Nest
would fail to resolve one of them.
import { } from '@evanion/nestjs-correlation-id';
; // -> '@evanion/nestjs-correlation-id:CORRELATION_CONFIG'Inject it with @Inject. RestockJob, a queue consumer in Baize’s orders
service, injects it to check an id a queue message carried with the same
validate the middleware applies. StockClient is the service’s own
HttpService consumer, shown on
Forward the id to the next service.
Carry the id through a job explains the consumer.
import { Inject, Injectable } from '@nestjs/common';
import {
CORRELATION_CONFIG_TOKEN,
CorrelationService,
type CorrelationConfig,
} from '@evanion/nestjs-correlation-id';
import { StockClient } from './stock.client.js';
export interface RestockMessage {
game: string;
correlationId?: string;
}
@Injectable()
export class RestockJob {
constructor(
private readonly correlation: CorrelationService,
@Inject(CORRELATION_CONFIG_TOKEN)
private readonly config: CorrelationConfig,
private readonly stock: StockClient,
) {}
handle(message: RestockMessage) {
const carried = message.correlationId;
const id =
carried && this.config.validate?.(carried)
? carried
: this.correlation.generate();
return this.correlation.run(id, () => this.stock.level(message.game));
}
}CORRELATION_AXIOS_INTERCEPTOR
CORRELATION_AXIOS_INTERCEPTOR is the token of the provider withCorrelation()
registers to attach its interceptor to the axios instance.
const CORRELATION_AXIOS_INTERCEPTOR =
'@evanion/nestjs-correlation-id:AXIOS_INTERCEPTOR';Nothing injects it. The provider exists so that Nest runs the factory that registers the interceptor.
import { } from '@evanion/nestjs-correlation-id';
; // -> '@evanion/nestjs-correlation-id:AXIOS_INTERCEPTOR'DEFAULT_CORRELATION_ID_VALIDATOR
DEFAULT_CORRELATION_ID_VALIDATOR accepts an incoming id of 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, and rejects everything else.
const DEFAULT_CORRELATION_ID_VALIDATOR: (value: string) => boolean;An accepted id reaches the response header and the application’s logs. Node
rejects a carriage return (CR) or line feed (LF) in a header, incoming and
outgoing, so the validator’s CR and LF refusal covers an id from another source,
such as a queue message, that your code passes through it. The 128-character
cap bounds what one request appends to every log line it touches. A universally
unique identifier (UUID) from randomUUID is 36 characters.
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