skip to content

In Angular, how can one HttpClient call tell an interceptor to skip adding the bearer token, using an HttpContextToken instead of URL matching?

level: middleimportance: nice to knowfreq 30%

answer

  1. metadata that never leaves the browser
  2. a typed key with a default
  3. set on the call, read in the interceptor
  4. the one mutable part of a request

basics

~20 s

Define a key such as new HttpContextToken<boolean>(() => false), pass context: new HttpContext().set(SKIP_AUTH, true) on that call, and have the interceptor check req.context.get(SKIP_AUTH). The context travels with the request to interceptors but is never sent to the server.

solid answer

~40 s

Every `HttpRequest` carries a **`context`**, an `HttpContext`: a typed map for metadata meant for interceptors, not for the backend. Keys are `HttpContextToken<T>` instances created with a factory for the default value, such as `export const SKIP_AUTH = new HttpContextToken<boolean>(() => false)`. A caller sets a value per request with `http.post(url, body, { context: new HttpContext().set(SKIP_AUTH, true) })`, and the interceptor reads `req.context.get(SKIP_AUTH)`, getting the default when the caller set nothing. This beats URL matching, which breaks when paths change, and beats a custom header, which leaks to the server and may trigger a CORS preflight. Two details matter: unlike the rest of the request, the context is **mutable** and shared by clones and retries, and the default factory runs per request, so object defaults are not shared.

code

ts · 26 lines
ts
import { Injectable, inject, signal } from '@angular/core';
import { HttpClient, HttpContext, HttpContextToken, HttpInterceptorFn } from '@angular/common/http';
import { Observable } from 'rxjs';

export const SKIP_AUTH = new HttpContextToken<boolean>(() => false);

@Injectable({ providedIn: 'root' })
export class AuthService {
  private readonly http = inject(HttpClient);
  readonly accessToken = signal<string | null>(null);

  // The refresh call must not carry the (expired) bearer token.
  refresh(): Observable<{ accessToken: string }> {
    return this.http.post<{ accessToken: string }>('/api/auth/refresh', null, {
      context: new HttpContext().set(SKIP_AUTH, true),
    });
  }
}

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  if (req.context.get(SKIP_AUTH)) {
    return next(req);
  }
  const token = inject(AuthService).accessToken();
  return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req);
};

go deeper

for a junior

Recognise HttpContextToken and HttpContext as the way a call passes a flag to an interceptor, and that the server never sees it.

for a middle

Define a token with a default factory, set it with the context option, read it with req.context.get, and explain why it beats URL matching or marker headers.

for a senior

Use the context's mutability deliberately for per-request state across retries and replays, and avoid shared contexts and duplicate token instances.

for a principal

Standardise a small set of exported context tokens as the contract between data services and interceptors, so cross-cutting behaviour stays discoverable.

## The problem it solves An auth interceptor adds `Authorization: Bearer ...` to every API call. A few calls must not get it: the login call, the token-refresh call, or a public endpoint that rejects unexpected credentials. Common workarounds are fragile: - **URL matching** (`if (req.url.endsWith('/refresh'))`) spreads knowledge of endpoints into the interceptor and breaks silently when a path changes. - **A marker header** (`X-Skip-Auth: 1`) is sent to the server, may trigger a CORS preflight on cross-origin calls, and must be stripped again by the interceptor. `HttpContext` gives each request a private channel to interceptors instead. ## The three pieces 1. **The token (the key).** `new HttpContextToken<T>(defaultFactory)` creates a typed key. The factory supplies the value when a request did not set one. Using a function, as the guide explains, means an object or array default is created fresh for each request. 2. **Setting it on a call.** Every `HttpClient` method accepts a `context` option: `{ context: new HttpContext().set(SKIP_AUTH, true) }`. `HttpContext.set()` returns the same context, so calls chain. 3. **Reading it in an interceptor.** `req.context.get(SKIP_AUTH)` returns the stored value or the default. `has()`, `delete()` and `keys()` exist for the rarer cases. ```ts export const SKIP_AUTH = new HttpContextToken<boolean>(() => false); export const authInterceptor: HttpInterceptorFn = (req, next) => { if (req.context.get(SKIP_AUTH)) { return next(req); } const token = inject(AuthService).accessToken(); return next(token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req); }; ``` ## What makes `HttpContext` different from the rest of the request | Aspect | Headers, params, body | `context` | |---|---|---| | Mutability | Immutable; changed only through `req.clone()` | **Mutable** map | | Sent to the server | Yes | Never; the backend does not serialise it | | Typed | Strings (headers, params) | Each token carries its own type `T` | | Shared by clones | Copied or replaced per clone | The same `HttpContext` object is passed on unless the clone supplies another | | Default when unset | None | The token's factory value | The guide calls out the mutability deliberately: if an interceptor changes the context of a request that is later retried, the same interceptor sees that change when it runs again. That is useful for carrying state across attempts, such as a retry counter or a flag that a request has already been replayed after a token refresh. ## Pitfalls - **Reusing one `HttpContext` object for several calls.** Because it is mutable, a value written by an interceptor for one request becomes visible to the others. Create a new context per call. - **Expecting the server to see it.** Context is client-side only; if the server needs a signal, that is a header or a parameter. - **Tokens compared by identity.** Two `new HttpContextToken(...)` calls create two different keys, even with the same default; export one constant and import it everywhere. - **Missing the default.** `get()` on an unset token returns the factory's value (and stores it), so write defaults that describe the normal case. ## Where the context comes from When a call passes no `context` option, the `HttpRequest` constructor creates an empty `HttpContext` for it, so every request has its own map. When a call does pass one, that exact object is used, and `req.clone()` hands the same object on to the next interceptor unless the clone is given a different context. An outer retry of the `HttpClient` Observable re-runs the chain with the same `HttpRequest`, so values an interceptor wrote on the first pass are visible on the second. ## Typical uses - Skipping authentication for login, refresh and public endpoints. - Turning caching on or off per request, as in the guide's `CACHING_ENABLED` example. - Passing a per-request retry budget or a "silent" flag that tells an error interceptor not to show a toast. - Tagging requests for timing or tracing without adding headers.

  • Why does HttpContextToken take a factory function for its default rather than a plain value?
    The factory runs when a request reads a token it never set, so every request gets its own default. With a plain object or array as the default, all requests would share and mutate one instance. The Angular guide gives exactly this reason: using a function ensures that if the value is an object or array, each request gets its own instance.
  • Can an interceptor use the HttpContext to remember that a request was already replayed after a token refresh?
    Yes. The context is the one mutable part of a request, and clones share it unless a new one is supplied. A retry applied to the `HttpClient` Observable re-runs the chain with the same request, so a flag such as `REPLAYED` set before a replay is still there when the interceptor sees that request again, and it can refuse to refresh twice. Keep such flags in exported tokens with a `false` default.

saying these in an interview costs you the question

  • HttpContext values are sent to the server as request headers.
  • HttpContext is immutable like HttpHeaders, so set() must be reassigned.
  • Two HttpContextToken instances with the same default are the same key.
  • A custom marker header is the standard way to signal interceptors.
  • Sharing one HttpContext object between calls is safe because each request copies it.