Angular HttpClient and Interceptors in 2026: Request Handling and Interview Questions
Master Angular HttpClient with functional interceptors, authentication patterns, error handling, and caching strategies. Includes common interview questions and real-world code examples.

Angular HttpClient interceptors transform how applications handle HTTP requests, from adding authentication headers to implementing retry logic. Angular 20 standardized functional interceptors as the recommended approach, replacing the older class-based pattern with a more predictable and tree-shakable API.
Angular 20 recommends functional interceptors with withInterceptors() over class-based interceptors. Functional interceptors have more predictable execution order and integrate better with standalone components.
HttpClient Setup with provideHttpClient
Before writing interceptors, HttpClient must be configured in the application bootstrap. Angular's standalone architecture uses provideHttpClient() instead of the older HttpClientModule.
The setup happens in the application configuration:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './interceptors/auth.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
// Interceptors execute in array order
withInterceptors([authInterceptor, loggingInterceptor])
),
],
});Interceptors execute in the order specified in the array. The first interceptor processes the outgoing request first, and the last interceptor sees the response first.
Functional Interceptor Anatomy
A functional interceptor receives the request and a next function. Calling next(req) passes the request to the next interceptor in the chain or to the backend if this is the last interceptor.
The basic structure looks like this:
import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from '@angular/common/http';
import { Observable, tap } from 'rxjs';
export const loggingInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
): Observable<HttpEvent<unknown>> => {
const startTime = performance.now();
console.log(`[HTTP] ${req.method} ${req.url}`);
return next(req).pipe(
tap({
next: () => {
const duration = Math.round(performance.now() - startTime);
console.log(`[HTTP] ${req.method} ${req.url} completed in ${duration}ms`);
},
error: (err) => {
console.error(`[HTTP] ${req.method} ${req.url} failed:`, err.message);
},
})
);
};The interceptor logs the request method and URL, then measures the response time. The tap operator observes the response without modifying it.
Authentication Interceptor with Token Injection
Most applications need to attach authentication tokens to outgoing requests. The interceptor clones the request with the Authorization header, since HttpRequest objects are immutable.
This pattern retrieves the token from a service and attaches it:
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
// Inject services using Angular's inject() function
const authService = inject(AuthService);
const token = authService.getAccessToken();
// Skip auth header for public endpoints
if (req.url.includes('/public/') || !token) {
return next(req);
}
// Clone request with Authorization header
const authReq = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`,
},
});
return next(authReq);
};The inject() function works because interceptors execute within Angular's injection context. This eliminates constructor injection boilerplate from class-based interceptors.
Error Handling and Retry Logic
Network failures happen. A retry interceptor can automatically retry failed requests with exponential backoff, improving reliability without requiring changes to every HTTP call.
RxJS operators handle the retry logic:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { retry, timer } from 'rxjs';
export const retryInterceptor: HttpInterceptorFn = (req, next) => {
// Only retry GET requests (idempotent)
if (req.method !== 'GET') {
return next(req);
}
return next(req).pipe(
retry({
count: 3,
delay: (error: HttpErrorResponse, retryCount: number) => {
// Don't retry client errors (4xx)
if (error.status >= 400 && error.status < 500) {
throw error;
}
// Exponential backoff: 1s, 2s, 4s
const delayMs = Math.pow(2, retryCount - 1) * 1000;
console.log(`Retry #${retryCount} in ${delayMs}ms`);
return timer(delayMs);
},
})
);
};The interceptor only retries GET requests because POST, PUT, and DELETE are not idempotent. Client errors (4xx) skip retries since the server explicitly rejected the request.
Ready to ace your Angular interviews?
Practice with our interactive simulators, flashcards, and technical tests.
Response Caching with HttpContext
HttpClient provides HttpContext to pass metadata between interceptors and calling code. A caching interceptor can read context tokens to determine cache behavior.
The cache token and interceptor work together:
import { HttpInterceptorFn, HttpContextToken, HttpResponse } from '@angular/common/http';
import { of, tap } from 'rxjs';
// Context token to control caching per-request
export const CACHE_REQUEST = new HttpContextToken<boolean>(() => false);
const cache = new Map<string, HttpResponse<unknown>>();
export const cacheInterceptor: HttpInterceptorFn = (req, next) => {
// Only cache if explicitly requested via context
if (!req.context.get(CACHE_REQUEST) || req.method !== 'GET') {
return next(req);
}
const cacheKey = req.urlWithParams;
const cachedResponse = cache.get(cacheKey);
if (cachedResponse) {
console.log(`[Cache] HIT: ${cacheKey}`);
return of(cachedResponse.clone());
}
return next(req).pipe(
tap((event) => {
if (event instanceof HttpResponse) {
console.log(`[Cache] STORE: ${cacheKey}`);
cache.set(cacheKey, event.clone());
}
})
);
};Calling code opts into caching with the context token:
import { HttpClient, HttpContext } from '@angular/common/http';
import { CACHE_REQUEST } from '../interceptors/cache.interceptor';
export class UserService {
constructor(private http: HttpClient) {}
getUser(id: string) {
return this.http.get(`/api/users/${id}`, {
context: new HttpContext().set(CACHE_REQUEST, true),
});
}
}This pattern keeps caching behavior explicit at the call site rather than hidden in global configuration.
Token Refresh with 401 Handling
When access tokens expire, the server returns a 401 response. An interceptor can catch this, refresh the token, and retry the original request transparently.
The refresh logic coordinates multiple concurrent requests:
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, switchMap, throwError, BehaviorSubject, filter, take } from 'rxjs';
import { AuthService } from '../services/auth.service';
let isRefreshing = false;
const refreshTokenSubject = new BehaviorSubject<string | null>(null);
export const tokenRefreshInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status !== 401 || req.url.includes('/auth/refresh')) {
return throwError(() => error);
}
if (isRefreshing) {
// Wait for the refresh to complete
return refreshTokenSubject.pipe(
filter((token) => token !== null),
take(1),
switchMap((token) => {
const retryReq = req.clone({
setHeaders: { Authorization: `Bearer ${token}` },
});
return next(retryReq);
})
);
}
isRefreshing = true;
refreshTokenSubject.next(null);
return authService.refreshToken().pipe(
switchMap((response) => {
isRefreshing = false;
refreshTokenSubject.next(response.accessToken);
const retryReq = req.clone({
setHeaders: { Authorization: `Bearer ${response.accessToken}` },
});
return next(retryReq);
}),
catchError((refreshError) => {
isRefreshing = false;
authService.logout();
return throwError(() => refreshError);
})
);
})
);
};The BehaviorSubject prevents multiple refresh calls when several requests fail simultaneously. Once the token refreshes, all queued requests retry with the new token.
Interview Questions: HttpClient and Interceptors
Technical interviews often probe understanding of HTTP handling patterns. These questions appear frequently in Angular positions.
Q: Why are functional interceptors preferred over class-based interceptors?
Functional interceptors have more predictable execution order. The Angular documentation states that functional interceptors with withInterceptors() maintain strict array order, while DI-based interceptors can have ordering issues in complex module hierarchies. Functional interceptors also produce smaller bundles because they tree-shake better.
Q: How do you prevent an interceptor from running on specific requests?
Two approaches exist. First, check the URL pattern in the interceptor and call next(req) without modification for excluded paths. Second, use HttpContext tokens that calling code sets to opt out of interceptor behavior. The context approach keeps the exclusion decision at the call site.
Q: What happens if you forget to call next() in an interceptor?
The request never reaches the server. The Observable returned by next() represents the rest of the interceptor chain and the actual HTTP call. Without calling it, downstream interceptors and the backend never see the request. This is intentional for caching interceptors that return cached responses.
Q: How do you handle concurrent requests during token refresh?
Use a BehaviorSubject to coordinate. When the first 401 triggers a refresh, set a flag and emit null on the subject. Subsequent 401s wait on the subject with filter until the new token arrives. This prevents multiple refresh calls and ensures all pending requests retry with the same fresh token.
Q: Can interceptors modify the response body?
Yes. Use map() on the Observable returned by next() to transform HttpResponse objects. Since responses are immutable, clone them with modified bodies. This pattern works for adding computed fields, unwrapping API envelopes, or normalizing data structures.
For more Angular interview preparation, see the HttpClient interview questions module.
Testing Functional Interceptors
Interceptors require testing with mocked HTTP handlers. Angular's HttpTestingController works with interceptors registered through provideHttpClient().
A test setup looks like this:
import { TestBed } from '@angular/core/testing';
import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
import { authInterceptor } from './auth.interceptor';
import { AuthService } from '../services/auth.service';
describe('authInterceptor', () => {
let httpClient: HttpClient;
let httpTesting: HttpTestingController;
let authServiceSpy: jasmine.SpyObj<AuthService>;
beforeEach(() => {
authServiceSpy = jasmine.createSpyObj('AuthService', ['getAccessToken']);
TestBed.configureTestingModule({
providers: [
provideHttpClient(withInterceptors([authInterceptor])),
provideHttpClientTesting(),
{ provide: AuthService, useValue: authServiceSpy },
],
});
httpClient = TestBed.inject(HttpClient);
httpTesting = TestBed.inject(HttpTestingController);
});
it('should add Authorization header when token exists', () => {
authServiceSpy.getAccessToken.and.returnValue('test-token');
httpClient.get('/api/data').subscribe();
const req = httpTesting.expectOne('/api/data');
expect(req.request.headers.get('Authorization')).toBe('Bearer test-token');
req.flush({ data: 'test' });
});
it('should skip Authorization for public endpoints', () => {
authServiceSpy.getAccessToken.and.returnValue('test-token');
httpClient.get('/public/health').subscribe();
const req = httpTesting.expectOne('/public/health');
expect(req.request.headers.has('Authorization')).toBeFalse();
req.flush({ status: 'ok' });
});
});The test verifies both the header injection and the public endpoint exception without making actual network requests.
Production Patterns: Combining Interceptors
Real applications stack multiple interceptors. Order matters: authentication should run before logging, and caching should run before retry logic.
A production configuration might look like this:
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { AppComponent } from './app/app.component';
import { authInterceptor } from './interceptors/auth.interceptor';
import { tokenRefreshInterceptor } from './interceptors/token-refresh.interceptor';
import { cacheInterceptor } from './interceptors/cache.interceptor';
import { retryInterceptor } from './interceptors/retry.interceptor';
import { loggingInterceptor } from './interceptors/logging.interceptor';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([
// Order: auth -> refresh -> cache -> retry -> logging
authInterceptor,
tokenRefreshInterceptor,
cacheInterceptor,
retryInterceptor,
loggingInterceptor,
])
),
],
});This ordering ensures tokens attach before refresh handling, cache checks happen before retries, and logging captures the final timing.
For broader Angular patterns, see the RxJS fundamentals module which covers the operators used in interceptors.
Start practicing!
Test your knowledge with our interview simulators and technical tests.
Key Takeaways for Angular HttpClient Interceptors
- Functional interceptors with
withInterceptors()are the Angular 20 standard, offering predictable ordering and better tree-shaking than class-based alternatives - Use
inject()inside interceptors to access services without constructor boilerplate - Clone requests with
req.clone()to modify headers sinceHttpRequestobjects are immutable HttpContexttokens make per-request behavior explicit at the call site rather than hidden in global config- Token refresh interceptors should coordinate concurrent requests with
BehaviorSubjectto prevent multiple refresh calls - Interceptor order matters: authentication before caching, caching before retry, logging last
Can you spot the bug in Angular?
One real snippet, one hidden bug, one attempt a day. No account needed to try.

Written by
Anthony Fillion-MailletFounder of SharpSkill
Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.
Updated on August 22, 2026
Tags
Share
Related articles

Advanced Angular Dependency Injection in 2026: Providers, Tokens and Interview Questions
Master Angular dependency injection with InjectionToken, hierarchical injectors, resolution modifiers, and multi-providers. Includes interview questions and production patterns.

Angular Forms in 2026: Reactive Forms, Validation and Technical Interview Questions
Master Angular Reactive Forms with typed FormBuilder, custom validators, async validation, FormArray patterns, and prepare for technical interviews with common form-related questions.

Angular Signals and Computed in 2026: Fine-Grained Reactivity and Interview Questions
Master Angular Signals, computed, effect, and linkedSignal in Angular 20+. Learn fine-grained reactivity patterns, performance optimizations, and prepare for technical interviews.