# 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. - Published: 2026-08-22 - Updated: 2026-08-22 - Author: Anthony Fillion-Maillet - Tags: angular, httpclient, interceptors, typescript, interview - Reading time: 12 min --- 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. > **Functional Interceptors Are Standard** > > 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: ```typescript // main.ts 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: ```typescript // interceptors/logging.interceptor.ts import { HttpInterceptorFn, HttpRequest, HttpHandlerFn, HttpEvent } from '@angular/common/http'; import { Observable, tap } from 'rxjs'; export const loggingInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn ): Observable> => { 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: ```typescript // interceptors/auth.interceptor.ts 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: ```typescript // interceptors/retry.interceptor.ts 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. ## 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: ```typescript // interceptors/cache.interceptor.ts 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(() => false); const cache = new Map>(); 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: ```typescript // services/user.service.ts 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: ```typescript // interceptors/token-refresh.interceptor.ts 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(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](https://angular.dev/guide/http/interceptors) 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](/technologies/angular/interview-questions/http-client). ## 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: ```typescript // interceptors/auth.interceptor.spec.ts 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; 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: ```typescript // main.ts 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](/technologies/angular/interview-questions/rxjs-fundamentals) which covers the operators used in interceptors. ## 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 since `HttpRequest` objects are immutable - `HttpContext` tokens make per-request behavior explicit at the call site rather than hidden in global config - Token refresh interceptors should coordinate concurrent requests with `BehaviorSubject` to prevent multiple refresh calls - Interceptor order matters: authentication before caching, caching before retry, logging last --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/en/blog/angular/angular-httpclient-interceptors-request-handling-interview