Home / Articles

Moving Angular HttpClient to the Fetch Backend with withFetch()

See why Angular's HttpClient is moving from XMLHttpRequest to fetch, how withFetch() enables edge SSR and streaming, and what it means for your interceptors.

Publisher: REACTAPP.TOP6 min read1286 words

This article is published in English.

Sources: Stop Using XMLHttpRequest: Why Angular 22 Shifted to the Native Fetch API

For most of its life, Angular's HttpClient has been a friendly RxJS layer sitting on top of XMLHttpRequest, a browser API that predates almost everything else in the modern web platform. That choice is increasingly a liability once your application renders on the server, runs on edge runtimes, or consumes streamed responses. This guide walks through why the fetch-based backend matters, how to switch to it with a single provider option, and what does and does not change for your existing interceptors.

Why XMLHttpRequest became a liability

XHR dates back to 1999, when Microsoft created it to power Outlook Web Access. Its design shows its age: it is stateful, event-driven, and awkward to compose, and the wider JavaScript ecosystem has been drifting away from it for close to a decade. Most libraries and runtimes settled on the promise-based fetch() long ago. Angular kept XHR under the hood primarily because of its strong commitment to backward compatibility.

That trade-off looked different once Angular leaned into zoneless change detection and server-side rendering. In that world, XHR is no longer just old code that nobody wants to touch; it actively limits where and how an Angular app can run. A fetch-based backend lets the framework modernize its networking stack, handle streamed data properly, and run on edge platforms.

At the time of writing, recent Angular releases are moving toward fetch as the preferred engine for HttpClient. Defaults and deprecations shift between major versions, so check the release notes for the version you are on rather than assuming which backend is active.

Edge runtimes do not ship XMLHttpRequest

Backend hosting has been shifting from full Node.js processes in Docker containers toward lightweight V8 isolates such as Cloudflare Workers, Deno, and Fastly Compute. If you want a deeper look at that runtime model, see our comparison of edge isolates, WebAssembly, and Node.js runtimes.

These platforms are built around modern web-standard APIs, and XMLHttpRequest is not among them. A traditional Angular SSR build deployed to such a platform fails the moment HttpClient issues its first API call while rendering the page, because the underlying transport simply does not exist.

fetch(), by contrast, is available in browsers, Node.js, Deno, and the edge runtimes alike. Once HttpClient uses it, the same SSR application becomes portable: you can render it in Cloudflare Workers or Deno Deploy and make API calls from locations that sit geographically closer to your users.

Switching the backend with withFetch()

The switch is a configuration change, not a refactor. Components and services keep calling this.http.get() and friends exactly as before; only the provider setup in your application config changes.

The snippet below registers HttpClient through provideHttpClient() and passes two features. withFetch() selects the fetch-based transport, and withInterceptors() registers a functional authentication interceptor that is defined further down.

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './core/auth.interceptor';

export const appConfig: ApplicationConfig = {
  providers: [
    // Injects HttpClient using the modern Fetch API backend
    provideHttpClient(
      withFetch(),
      withInterceptors([authInterceptor])
    )
  ]
};

Internally, withFetch() tells Angular to use FetchBackend instead of HttpXhrBackend as the component that actually performs the network request. Everything above that layer stays the same.

One limitation worth knowing before you flip the switch: the fetch backend does not report upload progress events, because fetch() offers no equivalent of XHR's upload progress. If your app shows a progress bar for file uploads, test that flow explicitly.

Streaming responses without buffering everything

XHR was built around complete, self-contained responses. It copes poorly with large or continuous data: downloading a 50MB file or consuming a chunked response tends to accumulate a large buffer in memory before JavaScript gets to work with it, which leads to memory pressure and a sluggish interface.

fetch() was designed with ReadableStream at its core, so the body can be consumed chunk by chunk as it arrives from the network. With the fetch backend, Angular can process incoming data at the moment it lands instead of waiting for the whole payload.

This matters most for AI-style interfaces. A chat UI that reads Server-Sent Events or chunked LLM output needs tokens to appear as they are generated, and a streaming-capable transport is the foundation for that without piling everything up in browser memory.

Be precise about what you get, though. A regular http.get() observable still emits the final response once the body is complete; incremental access comes through progress events (reportProgress with observe: 'events'), and for long-lived token streams many teams still reach for EventSource or a direct fetch() call. Verify the behavior you need against the documentation for your Angular version.

What happens to your interceptors

Whenever the network engine changes, the first question an experienced engineer asks is about middleware: will authentication and logging interceptors keep working?

With functional interceptors, the answer is yes. Interception operates on Angular's own HttpRequest and HttpResponse abstractions, which sit above the transport. Interceptors transform the Angular request object, and only after the whole chain has run does the final request reach FetchBackend, which translates it into a fetch() call. The interceptor never touches the native request.

The example below is backend-agnostic. It reads a JWT, clones the immutable request with an Authorization header, and passes the copy to next, which forwards it to the following interceptor and eventually to the backend.

import { HttpInterceptorFn } from '@angular/common/http';

// This interceptor works perfectly with both XHR and Fetch backends
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = localStorage.getItem('jwt');

  // Clone and mutate the Angular abstraction, not the native request
  const secureReq = req.clone({
    setHeaders: { Authorization: `Bearer ${token}` }
  });
  // Hand off to the next interceptor, and ultimately, the Fetch backend
  return next(secureReq);
};

Two caveats apply to this particular snippet rather than to the fetch backend. First, localStorage does not exist during server-side rendering, so on SSR or edge deployments you should guard the call or inject the token differently. Second, storing tokens in localStorage exposes them to any XSS vulnerability, and the code sends Bearer null when no token is present; production code should handle both cases.

Class-based interceptors still work

Older interceptors written as classes that implement HttpInterceptor continue to function with the fetch backend. They do, however, feel increasingly out of step with the rest of the framework. Combining withFetch() with functional interceptors registered through withInterceptors() is the approach that matches current Angular conventions, so it is worth migrating class-based ones when you touch them.

Key takeaways

  • Moving HttpClient from XHR to fetch is a quiet but significant upgrade: one provider option, no changes to calling code.
  • Add withFetch() to provideHttpClient() if your version does not already use fetch by default.
  • Fetch is what allows Angular SSR to run on Cloudflare Workers, Deno, and similar edge runtimes that lack XMLHttpRequest.
  • The fetch transport is built on ReadableStream, which helps with large payloads and chunked responses such as LLM output, but check how HttpClient surfaces partial data before building a streaming UI on it.
  • Functional interceptors (HttpInterceptorFn) keep working unchanged; test upload progress and any SSR-only code paths after switching.