Middleware
The middleware pipeline runs around every RPC invocation — auth, logging, timing, error transformation, CSRF, custom hooks. Three stages, prioritised within each.
Middleware in @omnitron-dev/netron-browser are factory
functions that return a MiddlewareFunction — there are no
middleware classes. You register them on an HttpClient or
WebSocketClient (not on NetronClient — see
Registering middleware).
Three-stage pipeline
| Stage | Constant | Runs | Typical uses |
|---|---|---|---|
pre-request | MiddlewareStage.PRE_REQUEST | Before transport call | Attach auth header, attach CSRF token, start timing |
post-response | MiddlewareStage.POST_RESPONSE | After successful transport call | Log/measure the response, read response headers |
error | MiddlewareStage.ERROR | After transport throws | Refresh-token-and-retry (auth), transform errors |
Within a stage, lower priority runs first (default 100). The
error stage runs your error middleware; on the HTTP transport, a
middleware that signals an auth retry triggers exactly one
re-send of the request (see
createAuthErrorMiddleware).
Retry, caching, and circuit-breaking are not middleware in this package — see Not middleware.
Middleware shape
A middleware is a plain function: (ctx, next) => …. Call
await next() to run the rest of the chain.
import type {
MiddlewareFunction,
ClientMiddlewareContext,
} from '@omnitron-dev/netron-browser';
// type MiddlewareFunction =
// (ctx: ClientMiddlewareContext, next: () => Promise<void>)
// => Promise<void> | void;
const myMiddleware: MiddlewareFunction = async (ctx, next) => {
// ...before
await next();
// ...after
};
The context (ClientMiddlewareContext):
interface ClientMiddlewareContext {
service: string;
method: string;
args: any[];
request?: {
headers?: Record<string, string>;
timeout?: number;
metadata?: Record<string, any>;
credentials?: RequestCredentials; // cookie/hybrid mode → 'include'
};
response?: {
data?: any;
headers?: Record<string, string>;
metadata?: Record<string, any>;
};
error?: Error; // populated in the error stage
timing: {
start: number;
end?: number;
middlewareTimes: Map<string, number>;
};
metadata: Map<string, any>; // shared state across middleware
skipRemaining?: boolean; // set to short-circuit the chain
transport: 'http' | 'websocket';
}
The per-middleware config (MiddlewareConfig, all optional except
name):
interface MiddlewareConfig {
name: string; // for metrics/debug
priority?: number; // default 100; lower runs first
condition?: (ctx: ClientMiddlewareContext) => boolean;
onError?: (error: Error, ctx: ClientMiddlewareContext) => void;
services?: string[] | RegExp; // restrict to services
methods?: string[] | RegExp; // restrict to methods
}
Registering middleware
client.use(middleware, config?, stage?) registers a middleware
and returns this (chainable). Available on HttpClient and
WebSocketClient. stage defaults to
MiddlewareStage.PRE_REQUEST.
import { HttpClient } from '@omnitron-dev/netron-browser/client';
import {
createAuthMiddleware,
createLoggingMiddleware,
StorageTokenProvider,
MiddlewareStage,
} from '@omnitron-dev/netron-browser';
const client = new HttpClient({ url: 'https://api.example.com' });
client
.use(createAuthMiddleware({
tokenProvider: new StorageTokenProvider(localStorage, 'token'),
}))
.use(createLoggingMiddleware({ logRequestPayload: true }));
Or build a pipeline up front and pass it in. The middleware
option is a single IMiddlewareManager (a MiddlewarePipeline
instance) — not an array:
import {
MiddlewarePipeline,
createLoggingMiddleware,
createTimingMiddleware,
} from '@omnitron-dev/netron-browser';
const pipeline = new MiddlewarePipeline();
pipeline.use(createLoggingMiddleware());
pipeline.use(createTimingMiddleware());
const client = new HttpClient({
url: 'https://api.example.com',
middleware: pipeline,
});
createClient() / NetronClient has no middleware option. The
high-level createClient() / NetronClient API exposes no
middleware option. To register middleware, construct an
HttpClient or WebSocketClient directly and use .use(...) (or
the middleware pipeline option above).
Built-in middleware
Most factories are importable from the package root
(@omnitron-dev/netron-browser). The two auth error helpers
live only on the /middleware subpath — they are noted inline.
createAuthMiddleware
Attaches the auth token to outgoing requests in the pre-request
stage. tokenProvider is required; everything else is optional.
A transport strategy (bearer / cookie / hybrid) can be supplied
for T#176 cookie-mode; if omitted, a bearer transport is
synthesised from the legacy headerName / tokenPrefix options.
import {
createAuthMiddleware,
StorageTokenProvider,
SimpleTokenProvider,
} from '@omnitron-dev/netron-browser';
client.use(createAuthMiddleware({
tokenProvider: new StorageTokenProvider(localStorage, 'access_token'),
// headerName: 'Authorization', // default
// tokenPrefix: 'Bearer ', // default
skipServices: ['public'],
skipMethods: ['auth@1.0.0.signin'],
}));
| Option | Default | Notes |
|---|---|---|
tokenProvider | — | Required. { getToken(): string | null | Promise<string | null>; getTokenType?(): string } |
transport | bearer (synthesised) | IClientTokenTransport strategy; cookie/hybrid set credentials: 'include' |
headerName | 'Authorization' | Legacy bearer-only; ignored when transport is given |
tokenPrefix | 'Bearer ' | Legacy bearer-only; ignored when transport is given |
skipServices | [] | Service names to skip |
skipMethods | [] | service.method pairs to skip |
Token-provider helpers:
SimpleTokenProvider(token | () => string | null)— wrap a static string or getter.StorageTokenProvider(storage, key)— read fromlocalStorage/sessionStorage.
createAuthErrorMiddleware
Import from the
/middlewaresubpath —@omnitron-dev/netron-browser/middleware. These two helpers are not re-exported from the package root.
An error-stage middleware that handles auth failures: on 401
it refreshes the token via the AuthenticationClient and signals
a retry (the HTTP transport re-sends once); 403 and 429 surface
through callbacks. Register it on the ERROR stage.
import { AuthenticationClient } from '@omnitron-dev/netron-browser';
import {
createAuthErrorMiddleware,
MiddlewareStage,
} from '@omnitron-dev/netron-browser/middleware';
const authClient = new AuthenticationClient({ autoRefresh: true });
client.use(
createAuthErrorMiddleware({
authClient,
onSessionExpired: () => { window.location.href = '/login'; },
onAccessDenied: (details) => showNotification(details),
onRateLimited: (retryAfter) => warn(`Retry after ${retryAfter}s`),
maxRetries: 1,
}),
{ name: 'auth-error-handler', priority: 10 },
MiddlewareStage.ERROR,
);
| Option | Default | Notes |
|---|---|---|
authClient | — | Required. AuthenticationClient used for refresh |
onSessionExpired | — | Called on 401 when refresh fails |
onAccessDenied | — | Called on 403 with { service, method, userId?, requiredPermissions?, details? } |
onRateLimited | — | Called on 429 with retryAfter seconds |
maxRetries | 1 | Max refresh-and-retry attempts |
retryStatusCodes | [401] | Codes that trigger refresh+retry |
emitEvents | true | Emit events on the auth client |
There is also a shorthand:
import { createSimpleAuthErrorMiddleware } from '@omnitron-dev/netron-browser/middleware';
client.use(
createSimpleAuthErrorMiddleware(authClient, {
onSessionExpired: () => { window.location.href = '/login'; },
}),
undefined,
MiddlewareStage.ERROR,
);
createLoggingMiddleware
Logs requests, responses, and errors. Wraps all three stages
around next().
import {
createLoggingMiddleware,
ConsoleLogger,
} from '@omnitron-dev/netron-browser';
client.use(createLoggingMiddleware({
logger: new ConsoleLogger('[netron]'),
logRequestPayload: true,
logResponsePayload: false,
}));
| Option | Default | Notes |
|---|---|---|
logger | new ConsoleLogger() | Any Logger (debug/info/warn/error) |
requestLogLevel | 'info' | LogLevel |
responseLogLevel | 'info' | LogLevel |
errorLogLevel | 'error' | LogLevel |
logRequestPayload | false | Include ctx.args — avoid in prod |
logResponsePayload | false | Include ctx.response.data |
skipServices / skipMethods | [] | Skip lists |
Also exported: the Logger interface, ConsoleLogger class, and
LogLevel type.
createTimingMiddleware
Measures call duration via the performance API, records to a
collector, and can flag slow calls.
import {
createTimingMiddleware,
InMemoryMetricsCollector,
} from '@omnitron-dev/netron-browser';
const collector = new InMemoryMetricsCollector(1000);
client.use(createTimingMiddleware({
collector,
slowThreshold: 1000,
onSlowRequest: (m) => console.warn('slow', m.service, m.method, m.duration),
onMeasure: (m) => analytics.record(m),
}));
collector.getAverageDuration('users', 'findById');
collector.getSlowestCalls(10);
| Option | Default | Notes |
|---|---|---|
collector | — | MetricsCollector; InMemoryMetricsCollector provided |
onMeasure | — | (metrics: PerformanceMetrics) => void per call |
slowThreshold | — | ms threshold for the slow-call callback |
onSlowRequest | console.warn | Called when duration > slowThreshold |
skipServices / skipMethods | [] | Skip lists |
Also exported: MetricsCollector / PerformanceMetrics types and
InMemoryMetricsCollector (package root). For observing the emitted
performance.measure entries there is also createPerformanceObserver(cb),
which is exported only from the /middleware subpath
(@omnitron-dev/netron-browser/middleware), not the package root.
createErrorTransformMiddleware
An error-stage middleware that normalises thrown errors into a
consistent NormalizedError shape and (optionally) maps codes to
user-facing messages.
import {
createErrorTransformMiddleware,
CommonErrorMessages,
} from '@omnitron-dev/netron-browser';
client.use(createErrorTransformMiddleware({
errorMessages: CommonErrorMessages,
includeStack: false,
onError: (normalized) => Sentry.captureException(normalized),
}));
| Option | Default | Notes |
|---|---|---|
transformer | defaultErrorTransformer | (error, { service, method, transport }) => NormalizedError | Error |
onError | — | Called with the normalised error |
includeStack | true | Include error.stack |
includeContext | true | Include service / method |
errorMessages | {} | Map of error code → message |
skipServices / skipMethods | [] | Skip lists |
Also exported: defaultErrorTransformer, CommonErrorMessages,
and the guards isRetryableError, isClientError,
isServerError.
createCsrfMiddleware
Pre-request middleware for cookie-mode auth (T#176). Reads the
CSRF cookie and echoes it in the X-CSRF-Token header
(double-submit). No-ops outside the browser or before the cookie
is set.
import { createCsrfMiddleware } from '@omnitron-dev/netron-browser';
client.use(createCsrfMiddleware({
// cookieName: 'omni_csrf', // default
// headerName: 'X-CSRF-Token', // default
skipMethods: ['auth@1.0.0.signin', 'auth@1.0.0.refresh'],
}));
| Option | Default | Notes |
|---|---|---|
cookieName | 'omni_csrf' | CSRF cookie to read |
headerName | 'X-CSRF-Token' | Header to echo it in |
skipMethods | [] | service.method pairs the server has exempted |
Register this alongside createAuthMiddleware when using the
cookie token transport; bearer mode does not need it.
Custom middleware
Any (ctx, next) function works. Read/write ctx.request.headers
before next(); inspect ctx.response / ctx.error after.
import type { MiddlewareFunction } from '@omnitron-dev/netron-browser';
const tenantMiddleware: MiddlewareFunction = async (ctx, next) => {
if (!ctx.request) ctx.request = {};
if (!ctx.request.headers) ctx.request.headers = {};
ctx.request.headers['X-Tenant-ID'] = getCurrentTenant();
await next();
};
client.use(tenantMiddleware, { name: 'tenant', priority: 50 });
Useful patterns
Skip transport on a cache hit (set skipRemaining and
populate ctx.response):
const cacheReadMiddleware: MiddlewareFunction = async (ctx, next) => {
const key = `${ctx.service}.${ctx.method}:${JSON.stringify(ctx.args)}`;
const hit = memoryCache.get(key);
if (hit !== undefined) {
ctx.response = { data: hit };
ctx.skipRemaining = true; // short-circuits the chain
return; // do not call next()
}
await next();
};
Error tagging for Sentry (register on the ERROR stage):
import { MiddlewareStage } from '@omnitron-dev/netron-browser';
const sentryMiddleware: MiddlewareFunction = async (ctx, next) => {
try {
await next();
} catch (error) {
Sentry.withScope((scope) => {
scope.setTag('rpc.service', ctx.service);
scope.setTag('rpc.method', ctx.method);
scope.setContext('rpc', { args: ctx.args });
Sentry.captureException(error);
});
throw error; // re-throw
}
};
client.use(sentryMiddleware, { name: 'sentry', priority: 200 }, MiddlewareStage.ERROR);
Restrict to specific services/methods via config:
client.use(myMiddleware, {
name: 'mutations-only',
methods: /\.(create|update|delete)$/,
});
Ordering rules
Within a stage, lower priority runs first (default 100).
Service- and method-scoped middleware are merged with global
middleware and the whole set is re-sorted by priority. A typical
ordering:
| Stage | Priority | Middleware | Why |
|---|---|---|---|
| pre-request | 10 | Auth | Attach token before transport |
| pre-request | 20 | CSRF | Attach CSRF header (cookie mode) |
| pre-request | 30 | Timing (start) | Bracket the call |
| pre-request | 50 | Tenant / context | Other request attributes |
| post-response | 30 | Logging | Final log |
| post-response | 50 | Timing (record) | Record latency |
| error | 10 | Auth error handler | Refresh + retry on 401 |
| error | 100 | Error transform | Normalise the thrown error |
| error | 200 | Sentry | Report, then re-throw |
Not middleware
Some capabilities that look like middleware are configured elsewhere in this package — do not look for middleware factories for them.
Retry — an HttpClient option
Request retry is not middleware. It is a built-in
HttpClient option: set retry: true and maxRetries. Backoff
is exponential (2^n × 1000ms) and it applies to the HTTP
transport only.
const client = new HttpClient({
url: 'https://api.example.com',
retry: true,
maxRetries: 3, // default 3 when retry is enabled
});
See Browser client for the full client options.
Circuit-breaking & caching — the fluent HTTP interface
Circuit-breaking and response caching live in the advanced
fluent HTTP interface (HttpRemotePeer / FluentInterface,
the same surface as @omnitron-dev/netron-http-core), not in the
middleware pipeline:
- Caching —
HttpCacheManager,CacheOptions(and the per-call.cache({ ... })fluent builder). - Circuit breaking —
CircuitBreakerOptions. - Fluent retry —
RetryOptionswith anattemptsfield (e.g..retry({ attempts: 3 })). Note thisattemptsfield is the fluent retry option — it is unrelated to theHttpClientretry/maxRetriesoptions above, and there is no retry middleware.
These are exported from the package root for advanced use
(HttpCacheManager, RetryManager, FluentInterface,
CircuitBreakerOptions, CacheOptions, RetryOptions). They are
documented with the fluent interface, not here.
Best practices
- Always
await next(). Fire-and-forget breaks ordering and loses the response/error. - Initialise
ctx.request.headersbefore writing. Both may beundefined(ctx.request ??= {}; ctx.request.headers ??= {}). - Re-throw in error middleware unless you genuinely recover — swallowing the error hides failures.
- Name your middleware.
config.nameshows up in the pipeline's per-middleware metrics (getMetrics()). - Don't log payloads in production. Keep
logRequestPayloadoff, or useskipServices/skipMethodsfor sensitive calls.
See also
- Browser client —
HttpClientoptions includingretry/maxRetries - Auth —
AuthenticationClient, token transports, cookie mode - Error handling — error types and codes
- Transports — what middleware wraps