typescript
65 lines · 8 steps
Refreshing tokens with an Angular HttpInterceptor
An HTTP interceptor attaches a bearer token, catches 401s, and refreshes the token once while queuing concurrent requests.
Explained by
highlit
1import { Injectable } from '@angular/core';
2import {
3 HttpInterceptor,
4 HttpHandler,
5 HttpRequest,
6 HttpEvent,
7 HttpErrorResponse,
8} from '@angular/common/http';
9import { Observable, throwError, BehaviorSubject } from 'rxjs';
10import { catchError, filter, take, switchMap } from 'rxjs/operators';
11import { AuthService } from './auth.service';
12
13@Injectable()
14export class AuthInterceptor implements HttpInterceptor {
15 private refreshing = false;
16 private refreshed$ = new BehaviorSubject<string | null>(null);
17
18 constructor(private auth: AuthService) {}
19
20 intercept(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
21 const token = this.auth.accessToken;
22 const authReq = token ? this.withToken(req, token) : req;
23
24 return next.handle(authReq).pipe(
25 catchError((error) => {
26 if (error instanceof HttpErrorResponse && error.status === 401 && token) {
27 return this.handleUnauthorized(req, next);
28 }
29 return throwError(() => error);
30 })
31 );
32 }
33
34 private withToken(req: HttpRequest<unknown>, token: string): HttpRequest<unknown> {
35 return req.clone({
36 setHeaders: { Authorization: `Bearer ${token}` },
37 });
38 }
39
40 private handleUnauthorized(req: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
41 if (this.refreshing) {
42 return this.refreshed$.pipe(
43 filter((token): token is string => token !== null),
44 take(1),
45 switchMap((token) => next.handle(this.withToken(req, token)))
46 );
47 }
48
49 this.refreshing = true;
50 this.refreshed$.next(null);
51
52 return this.auth.refreshToken().pipe(
53 switchMap((token) => {
54 this.refreshing = false;
55 this.refreshed$.next(token);
56 return next.handle(this.withToken(req, token));
57 }),
58 catchError((error) => {
59 this.refreshing = false;
60 this.auth.logout();
61 return throwError(() => error);
62 })
63 );
64 }
65}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A single BehaviorSubject lets many failed requests share one refresh instead of each triggering its own.
- 2Interceptors are the right place to centralize cross-cutting auth concerns like token injection and 401 recovery.
- 3Guarding a shared operation with a boolean flag turns a stampede of retries into one coordinated wait-and-replay.
Related explainers
go
package ratelimit import ( "context"
A token bucket rate limiter in Go
rate-limiting
concurrency
goroutines
Intermediate
7 steps
typescript
type StorageSchema = Record<string, unknown>; interface StorageOptions { namespace?: string;
A type-safe wrapper around localStorage
generics
type-safety
serialization
Intermediate
9 steps
typescript
type Fetcher<T> = (key: string) => Promise<T>; export class RequestDeduplicator<T> { private inFlight = new Map<string, Promise<T>>();
Deduplicating in-flight requests in TypeScript
deduplication
promises
caching
Intermediate
7 steps
go
package cache import ( "context"
Cache-aside reads with singleflight in Go
caching
singleflight
concurrency
Advanced
8 steps
typescript
import { Injectable, computed, inject, signal } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { firstValueFrom } from 'rxjs';
Optimistic updates with Angular signals
signals
optimistic-updates
state-management
Intermediate
9 steps
python
import os from datetime import datetime, timezone import jwt
JWT bearer auth as a FastAPI dependency
authentication
jwt
dependency-injection
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/refreshing-tokens-with-an-angular-httpinterceptor-explained-typescript-3bd8/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.