Skip to content

Request Context

RequestContext is ZinTrust’s request-scoped context object, designed for correlation and other cross-cutting concerns.

It does two things at once:

  1. Stores a durable context object on the request (req.context.requestContext).
  2. Mirrors the same context into an async-safe store (via Node AsyncLocalStorage when available).

This enables:

  • Consistent request IDs in logs and error responses
  • Trace correlation via W3C traceparent
  • Late-binding identity (user/tenant) by auth middleware
  • Metrics/tracing enrichment in the kernel without plumbing values through every call

Implementation: src/http/RequestContext.ts.

What’s in the context

The core shape is:

ts
export interface IRequestContext {
  requestId: string;
  traceId?: string;
  userId?: string;
  tenantId?: string;

  startTime: number;
  method: string;
  path: string;
  userAgent?: string;

  // Populated at the end of the request
  status?: number;
  duration?: number;
}

Field sources

  • requestId
    • From x-request-id header when present and string
    • Otherwise generated via generateUuid()
  • traceId
    • Extracted from traceparent (W3C Trace Context) when valid
    • Otherwise, if x-trace-id is present, ZinTrust uses it
    • Otherwise, if microservices tracing has populated req.context.trace.traceId, ZinTrust uses it
    • Otherwise can be supplied via req.context.traceId
  • userId, tenantId
    • Typically populated by auth middleware (JWT/API key/session)
  • method, path, userAgent
    • Read from the request at creation time

How the kernel uses RequestContext

The HTTP kernel creates and installs a context for every request:

  1. RequestContext.create(req) creates a context object and attaches it to req.context.
  2. RequestContext.run(context, async () => { ... }) establishes the async-local store so deeper code can read the current context.
  3. When the request completes, the kernel enriches the context with status and duration, then emits metrics/tracing.

This pattern is important:

  • Identity (userId, tenantId) may not be known at the start.
  • The kernel can still attach identity to spans/metrics at the end, after middleware runs.

AsyncLocalStorage vs fallback mode

ZinTrust attempts to use Node’s AsyncLocalStorage through @node-singletons/async_hooks.

If it cannot import that module (for example in non-Node runtimes), it uses a small in-memory fallback store that behaves like a stack:

  • run(ctx, fn) sets store = ctx, executes fn, then restores the previous store.
  • This preserves correctness for synchronous flows.
  • It does not provide true async context propagation in runtimes without AsyncLocalStorage.

If you rely on async context propagation, prefer Node runtimes.

Common usage patterns

Access the current context

If you’re in a request flow and need the current context:

ts
import { RequestContext } from '@zintrust/core';

const ctx = await RequestContext.current();
if (ctx) {
  Logger.info('Doing work', { requestId: ctx.requestId, traceId: ctx.traceId });
}

Notes:

  • current() is async because the underlying storage is resolved lazily.
  • current() can return undefined if you call it outside a request context.

Prefer passing req for updates

If you have access to the request, use it.

ts
const ctx = RequestContext.get(req);
if (ctx) {
  Logger.info('Start', { requestId: ctx.requestId });
}

Set identity (userId / tenantId)

Auth middleware should update identity using helper setters so:

  • req.context.userId / req.context.tenantId are set
  • The active request context object (if present) is updated too
ts
RequestContext.setUserId(req, userId);
RequestContext.setTenantId(req, tenantId);

Similarly for trace IDs:

ts
RequestContext.setTraceId(req, traceId);

Traceparent parsing

RequestContext.create(req) understands the W3C traceparent header:

Format: version-traceid-spanid-flags

Example:

00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

ZinTrust extracts the 32-hex traceid if valid and non-zero.

Interaction with logging, metrics, tracing

Logging

Logging middleware and application logs can include requestId and traceId to correlate events.

Recommendation:

  • Always include requestId
  • Include traceId when present
  • Avoid logging tokens or secrets

Metrics

The kernel records request metrics using the context’s timing fields (start time and duration), and labels by a route template when available.

Tracing

If OpenTelemetry is enabled (OTEL_ENABLED=true), the kernel creates a server span and later enriches it with:

  • enduser.id from context.userId
  • zintrust.tenant_id from context.tenantId
  • zintrust.trace_id from context.traceId

The “late binding” is intentional: auth middleware runs after the request starts.

Testing

Unit testing code that reads RequestContext

If your code uses RequestContext.current(), wrap the call with RequestContext.run(...) in tests:

ts
import { RequestContext } from '@zintrust/core';

const ctx = {
  requestId: 'test-req-1',
  startTime: Date.now(),
  method: 'GET',
  path: '/test',
} as const;

await RequestContext.run(ctx, async () => {
  const current = await RequestContext.current();
  expect(current?.requestId).toBe('test-req-1');
});

Integration tests

If you’re testing HTTP routes end-to-end, prefer the project’s HTTP test helpers so the kernel sets everything up naturally.

Best practices

  • Treat RequestContext as a cross-cutting concern store (correlation, identity, tenancy), not a general-purpose data bucket.
  • Keep tenantId low-sensitivity and stable; avoid putting PII into trace/metrics attributes.
  • Use the helper setters when writing identity fields.
  • Prefer routePath (template) rather than raw path when generating labels.

Troubleshooting

RequestContext.current() returns undefined

Common causes:

  • Calling it outside a request lifecycle
  • Calling it in a background job without wrapping the job in RequestContext.run(...)

Fix:

  • Pass the context explicitly to background jobs, then re-establish it in the worker.

Trace ID isn’t extracted

Checklist:

  • Ensure traceparent is being forwarded by your ingress/proxy
  • Ensure the header matches W3C format and has a valid, non-zero trace ID

See also

Released under the MIT License.