Interview question
Build an Angular HTTP interceptor with correlation and safe retry rules
Implements a functional interceptor that adds correlation metadata, records timing, and retries only explicitly safe transient requests.
TL;DR
Implements a functional interceptor that adds correlation metadata, records timing, and retries only explicitly safe transient requests.
Angular functional interceptors, HttpContextToken, immutable requests, retry policy, correlation, telemetry, and privacy.
Practice the problem like a real interview: restate, reason, implement, and test.
Implement a functional interceptor that adds an X-Correlation-ID when absent, records method, normalized route, duration, and outcome, and retries at most once only when the request opts in and the failure is transient. Do not retry unsafe commands by default, log secrets, or replace the original error.
GET /catalog + RETRY_SAFE=true + 503 -> one delayed retry
POST /orders + default context + 503 -> no retry
Authorization header -> never logged
failure after retry -> original error semantics preserved
finalize without reading sensitive headers or bodies.import {HttpContextToken, HttpErrorResponse, HttpInterceptorFn} from '@angular/common/http';
import {inject} from '@angular/core';
import {retry, throwError, timer, finalize, tap} from 'rxjs';
export const RETRY_SAFE = new HttpContextToken<boolean>(() => false);
export const reliabilityInterceptor: HttpInterceptorFn = (request, next) => {
const telemetry = inject(HttpTelemetry);
const correlationId = request.headers.get('X-Correlation-ID') ?? crypto.randomUUID();
const outgoing = request.headers.has('X-Correlation-ID')
? request
: request.clone({setHeaders: {'X-Correlation-ID': correlationId}});
const started = performance.now();
let outcome = 'success';
return next(outgoing).pipe(
retry({
count: request.context.get(RETRY_SAFE) && request.method === 'GET' ? 1 : 0,
delay: error => {
const transient = error instanceof HttpErrorResponse &&
(error.status === 0 || [502, 503, 504].includes(error.status));
if (!transient) return throwError(() => error);
outcome = 'retried';
return timer(200);
},
}),
tap({error: () => { outcome = 'failure'; }}),
finalize(() => telemetry.record({
method: request.method,
route: normalizeRoute(request.url),
correlationId,
outcome,
durationMs: performance.now() - started,
})),
);
};
The interceptor adds O(1) client work per request. Retry can double one opted-in GET request, so downstream load remains explicitly bounded.
Next in Angular Practical Labs: Test an HTTP response race