Conclusion

Angular Interceptors are a versatile tool that many developers rely on, often without even realizing it. While you might not find one directly in your own codebase, there's a high likelihood that a third-party library you depend on—particularly for authentication—has one in place.

Think of an interceptor as the main gate at a large event. As you enter, you pass through and collect a badge; when you leave, you pass through again and perhaps grab another souvenir. In Angular, every HTTP request created through the HTTP client flows through your registered interceptors—both just before it’s dispatched to the server and again when the response comes back. This makes interceptors the perfect home for shared, cross-cutting HTTP concerns.

Interceptors can serve many purposes, from acting as a caching layer to managing a loading spinner or standardizing error messages. However, the most common use case is adding authentication headers—like an Authorization token—to all outgoing requests.

Let me show you what a typical implementation looks like.

The official Angular documentation suggests something like this:

import { AuthService } from '../auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private auth: AuthService) {}

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        // Get the auth token from the service.
        const authToken = this.auth.getAuthorizationToken();

        // Clone the request and replace the original headers with
        // cloned headers, updated with the authorization.
        const authReq = req.clone({
            headers: req.headers.set('Authorization', authToken),
        });

        // send cloned request with header to the next handler.
        return next.handle(authReq);
    }
}
Enter fullscreen mode Exit fullscreen mode

A popular blog post that appears high in search results offers this approach:

export class AuthInterceptorService implements HttpInterceptor {
    constructor(private authService: AuthService) {}
    intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        const token = this.authService.getAuthToken();

        if (token) {
            // If we have a token, we set it to the header
            request = request.clone({
                setHeaders: { Authorization: `Authorization token ${token}` },
            });
        }

        return next.handle(request).pipe(
            catchError((err) => {
                if (err instanceof HttpErrorResponse) {
                    if (err.status === 401) {
                        // redirect user to the logout page
                    }
                }
                return throwError(err);
            }),
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Even the top-ranked Stack Overflow answer suggests a similar pattern:

import { Injectable } from '@angular/core';

import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from '@angular/common/http';

import { Observable } from 'rxjs/Observable';


@Injectable()
export class fwcAPIInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    const authReq = req.clone({
      headers: req.headers.set('Content-Type', 'application/json')
    });

    console.log('Intercepted HTTP call', authReq);

    return next.handle(authReq);
  }
}
Enter fullscreen mode Exit fullscreen mode
const authReq = req.clone({
    headers: req.headers.set('Content-Type', 'application/json')
    .set('header2', 'header 2 value')
    .set('header3', 'header 3 value')
});
Enter fullscreen mode Exit fullscreen mode

Now, do you see the potential problem?

Consider this scenario: what if your application also sends requests to a third-party API?

Exactly. In that case, those same headers—possibly including sensitive authentication tokens—are forwarded to that external service. That's not something you'd want, whether it happens by accident or intentionally. You certainly don't want to leak user credentials or other sensitive information to a party you don't control.

So, what can you do about it? Let's explore a few solutions.

The simplest and most reliable fix is to check the target host of the outgoing request. Only attach your custom headers when the request is going to a host you trust.

Applying that to the Angular docs example gives us:

import { AuthService } from '../auth.service';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private auth: AuthService) {}

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        const uri = new URL(req.url);
        if(uri.hostname !== 'trusted-domain.com') {
            return next.handle(req);
        }

        // Get the auth token from the service.
        const authToken = this.auth.getAuthorizationToken();

        // Clone the request and replace the original headers with
        // cloned headers, updated with the authorization.
        const authReq = req.clone({
            headers: req.headers.set('Authorization', authToken),
        });

        // send cloned request with header to the next handler.
        return next.handle(authReq);
    }
}
Enter fullscreen mode Exit fullscreen mode

A more adaptable variation of this approach relies on a configurable list of allowed hostnames. This is a common technique in libraries since it makes the behavior flexible.

For instance, the Angular Auth OIDC Client lets you define secure routes in its configuration. It will only inject the Authorization header into requests that match these configured hosts.

import { AuthService } from '../auth.service';
import { Config } from '../config';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private auth: AuthService, private config: Config) {}

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        const uri = new URL(req.url);
        if(!config.trustedHostNames.includes(uri.hostname)) {
            return next.handle(req);
        }

        // Get the auth token from the service.
        const authToken = this.auth.getAuthorizationToken();

        // Clone the request and replace the original headers with
        // cloned headers, updated with the authorization.
        const authReq = req.clone({
            headers: req.headers.set('Authorization', authToken),
        });

        // send cloned request with header to the next handler.
        return next.handle(authReq);
    }
}
Enter fullscreen mode Exit fullscreen mode

Another option is to use the HttpContext to explicitly opt out of adding headers on a per-request basis. The downside is that it's easy to forget to assign the context, which defeats the purpose.

This strategy works in three steps. First, you define an HttpContextToken:

const IS_UNTRUSTED = new HttpContextToken<boolean>(() => false);
Enter fullscreen mode Exit fullscreen mode

Next, you assign this token to a specific HTTP request:

import { IS_UNTRUSTED } from './token.ts';

@Injectable()
export class Some3rdPartyService {
    constructor(private http: HttpClient) {}

    getData() {
        return this.http.get('3rd-party-api',
            {
                context: new HttpContext().set(IS_UNTRUSTED, true),
            }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, you check for the token inside the interceptor before adding any headers:

import { AuthService } from '../auth.service';
import { IS_UNTRUSTED } from './token.ts';

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
    constructor(private auth: AuthService) {}

    intercept(req: HttpRequest<any>, next: HttpHandler) {
        if (req.context.get(IS_UNTRUSTED)) {
            return next.handle(req);
        }

        // Get the auth token from the service.
        const authToken = this.auth.getAuthorizationToken();

        // Clone the request and replace the original headers with
        // cloned headers, updated with the authorization.
        const authReq = req.clone({
            headers: req.headers.set('Authorization', authToken),
        });

        // send cloned request with header to the next handler.
        return next.handle(authReq);
    }
}
Enter fullscreen mode Exit fullscreen mode

The takeaway is clear. All requests made through the HTTP client are processed by your interceptors—it's a powerful mechanism for centralizing logic. But that power cuts both ways. Because external API calls also pass through your interceptors, you might unintentionally add sensitive data to those requests without realizing it. The most common pitfall is attaching an Authorization header to every request.

As a rule of thumb, before enriching a request with custom headers, always confirm you're sending it to a host you control and trust.