HTTP Client

About

guide/http

Angular has a built-in http request library.

HttpClient dependency is provided using the provideHttpClient helper function in app.config.ts.

export const appConfig: ApplicationConfig = {
    providers: [provideHttpClient()],
};

Then you may inject the HttpClient service as a dependency of the components, services, or other classes.

@Component({ ... })
export class Component {
    constructor(private http: HttpClient) {}
}

Best Practices

  • It is recommended that you create reusable, injectable services which isolate and encapsulate data access logic.

For example, this UserService encapsulates the logic to request data for a user:

@Injectable({ providedIn: "root" })
export class UserService {
    constructor(private http: HttpClient) {}

    getUser(id: string): Observable<User> {
        return this.http.get<User>(`/api/user/${id}`);
    }

    updateUser(id: string, data: UserForm): Observable<T> {
        return this.http.patch(`/api/user/${id}`, data);
    }
}

Within the component, you can combine NgIf and the async pipe to render the UI for the data only after it's finished loading.

@Component({
    template: `
        <ng-container *ngIf="user$ | async as user">
            <p>Name: {{ user.name }}</p>
            <p>Biography: {{ user.biography }}</p>
        </ng-container>
    `,
})
export class UserProfileComponent {
    @Input() userId!: string;
    user$!: Observable<User>;

    constructor(private userService: UserService) {}

    ngOnInit(): void {
        this.user$ = userService.getUser(this.userId);
    }
}

Configuring features of HttpClient

withFetch

By default HttpClient uses XMLHttpRequest api to make requests. Use withFetch feature to switch to fetch api instead.

It has limitations such as not producing upload progress events.

export const appConfig: ApplicationConfig = {
    providers: [provideHttpClient(withFetch())],
};

withInterceptors(...)

Configures the set of interceptor functions which will process requests made through HttpClient.

Making Requests

Each HttpClient method for request returns RxJS Observable which, when subscribed, sends the request and then emits the results when the server responds.

Observable created by HttpClient may be subscribed any number of times and will make a new request for each subscription.

These subscribtions will be automatically unsubscribed, when the Component is destroyed.

@Component({ ... })
export class Component {
    constructor(private http: HttpClient) {}

    getEmployees(formData: Employee) {
        // This request will not even be sent - There is no Subscription on it
        this.http.get('/api/employees', formData);

        this.http.get<Employee[]>('/api/employees').subscribe((response) => {
            // Process the response
        });

        this.http.get<Employee[]>('/api/employees', { responseType: 'arraybuffer' }).subscribe((response) => {
            // Process the response
        });
    }
}

If the data has unknown shape, don't use any, use unknown type as the response type.

HttpClient does not verify if the actual returned data matches the method type asserted.

By default HttpClient assumes that servers will return JSON data.

  • You may configure different types with responseType:

    • json: JSON data.

    • text: string data.

    • arraybuffer: ArrayBuffer containing raw response bytes.

    • blob: A blob instance.

In post requests many different types of values can be provided as request's body, an HttpClient will serialize them accordingly:

  • string: As plain text.

  • number, boolean, array, object: As JSON data.

  • ArrayBuffer: As raw data from the buffer.

  • Blob: As raw data with the Blob's content type.

  • FormData: As multipart/form-data encoded data.

  • HttpParams or URLSearchParams: As application/x-www-form-urlencoded formatted string.

Setting URL Parameters

Specify request parameters that should be included in the request URL.

http.get('/api/endpoint', {
    params: { filter: 'all' }
}).subscribe(response => { ... });

Setting Request Headers

  • Specify request headers that should be included in the request.

http.get('/api/endpoint', {
    headers: { 'X-Debug-Level': 'verbose' }
}).subscribe(response => { ... });

Accessing More Data from the Response

For convenience HttpClient only gets the body from the response.

To access the entire response, set the option observe: 'response'.

http.get<Config>("/api/config", { observe: "response" }).subscribe((res) => {
    console.log("Response status:", res.status);
    console.log("Body:", res.body);
});

Accessing Raw Progress Events

HttpClient can also return a stream of raw events corresponding to specific moments in the request lifecycle.

These events include when:

  • The request is sent;

  • The response header is returned;

  • The body is complete.

These events can also include progress events which report upload and download status for large request or response bodies.

They are disabled by default as they have performance costs.

The optional fetch implementation of HttpClient does not report upload progress events.

To observe the event stream, set the option observe: 'events'.

http.post("/api/upload", myData, {
    reportProgress: true,
    observe: "events",
}).subscribe((event) => {
    switch (event.type) {
        case HttpEventType.UploadProgress:
            console.log("Uploaded " + event.loaded + " out of " + event.total + " bytes");
            break;
        case HttpEventType.Response:
            console.log("Finished uploading!");
            break;
    }
});
Type Value
Event Meaning

HttpEventType.Sent

The request has been dispatched to the server

HttpEventType.UploadProgress

An HttpUploadProgressEvent reporting progress on uploading the request body

HttpEventType.ResponseHeader

The head of the response has been received, including status and headers

HttpEventType.DownloadProgress

An HttpDownloadProgressEvent reporting progress on downloading the response body

HttpEventType.Response

The entire response has been received, including the response body

HttpEventType.User

A custom event from an Http interceptor

Handling Request Failures

HttpClient will catch errors requests which returns through the Observable error channel.

There are two ways HTTP requests can fail:

  • A network or connection error - that prevents the request from reaching the server.

    • Will have a status code of 0.

    • An error which is an instance of ProgressEvent.

  • The backend receives the request but fails to proccess it, and returns an error response.

    • Will have a failing status code returned by the server.

    • An error which is the error from the server.

RxJS offers several operators which can be useful for error handling, like catchError().

It also provides several retry operators for automatically re-subscribing to failed Observables under certain conditions, like retry().

Handling Errors - with Subject

If multiple parts of the application would want to get the error from the request.

@Injectable({})
export class EmployeeGateway {
    error = new Subject<string>();

    constructor(private http: HttpClient) {}

    fetch<T>(): Observable<T> {
        return this.http.get<T>("/api/employees");
    }

    add(data: any): Observable<T> {
        // But now I need to handle the errors from this
        return this.http.post("/api/employee", data).subscribe(
            () => {},
            (error) => {
                // `error.message` since Subject is of type 'string' only
                this.error.next(error.message);
            }
        );
    }
}
@Component({ ... })
export class Component implements onDestroy {
    gatewayError: Subscription;

    constructor(private employeeGateway: EmployeeGateway) {}

    showError() {
        // Subscribing to the erro Subject from the service
        this.gatewayError = this.employeeGateway.error.subscribe((errorMsg) => {
            console.log(errorMsg);
        })
    }

    fetch() {
        this.employeeGateway.fetch<{ [key: string]: Employee }>().subscribe(
            (response) => {
                console.log(response);
            },
            (error) => {
                console.error(error);
            }
        );
    }

    submit(data: Employee) {
        this.employeeGateway.add(data);
    }

    // Must unsubscribe from the Subject
    ngOnDestroy() {
        this.gatewayError.unsubscribe();
    }
}

Handling Errors - with Operator catchError

Useful for handling error behind the scenes, like to send statistics.

import { throwError } from 'rxjs'

@Injectable({})
export class EmployeeGateway {
    error = new Subject<string>();

    constructor(private http: HttpClient) {}

    fetch<T>(): Observable<T> {
        return this.http
            .get<T>('/api/endpoint')
            .pipe(
                map(...),
                catchError(errorRes => {
                    // Do something with the error
                    ...
                    // You must return this `throwError` that will create a new Observable to keep the chain going
                    return throwError(errorRes)
                })
            );
    }

    add(data: any): Observable<T> {
        return this.http.post('/api/endpoint' data).subscribe(
            () => {},
            (error) => {
                this.error.next(error.message);
            }
        );
    }
}

Http Observable

Each request method construct an Observable of the requested response type.

HttpClient procudes what RxJS calls cold Observables, meaning that no actual request happens until a subscription takes place.

Each subscription is independent, so multiple subscriptions will trigger multiple requests.

Unsubscribing will abort the in-progress request.

  • This is very useful if the subscription was done via async pipes, as it will automatically cancel the request if the user navigates away.

  • If you use the Observable with the RxJS combinator switchMap, this cancellation will clean up any stale requests.

It is strongly recommended that you clean up subscriptions when the component using them is destroyed.

Transforming Request Data - with rxjs/Operators

@Component({
    ...
})
export class Component {
    ...
    fetchData() {
        /*
            Transform:
            {
                "key1": { name: "" },
                "key2": { name: "" },
                "key3": { name: "" },
                "key4": { name: "" },
            }
            Into:
            [
                { name: "" },
                { name: "" },
                { name: "" },
                { name: "" },
            ]
        */
        this.http
            .get<{ [key: string]: Employee }>('/api/employees')
            .pipe(map((response) => {
                return Object.values(response);
            }))
            .subscribe((transformedResponse: { name: string }[]) => {
                console.log(transformedResponse);
            });
    }
}

Interceptors

HttpClient supports a form of middleware known as interceptor.

There are two types:

  • functional:

    • The recommended type to use, since they have more predictable behavior.

  • DI-based:

    • Are interceptors defined as injectable classes and configured through the DI system.

    • Have the same capabilities as functional ones.

Interceptors are generally function which you can run for each request.

You can make a chain of interceptors where each interceptor processes the request or response before forwarding it to the next interceptor in the chain.

Some commom uses

  • Adding authentication headers to outgoing requests to a particular API.

  • Retrying failed requests with exponential backoff.

  • Caching responses for a period of time, or until invalidated by mutations.

  • Customizing the parsing of responses.

  • Measuring server response times and log them.

  • Driving UI elements such as a loading spinner while network operations are in progress.

  • Collecting and batch requests made within a certain timeframe.

  • Automatically failing requests after a configurable deadline or timeout.

  • Regularly polling the server and refreshing results.

Functional Interceptors

The basic form

The basic form is a function which receives the outgoing HttpRequest and a next function representing the next processing step in the interceptor chain.

export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
    console.log(req.url);
    return next(req);
}

Configuring Interceptors

You declare the set of interceptors to use through dependency injection, with withInterceptors.

The interceptors are chained in the order that you've listed them in the providers.

export const appConfig: ApplicationConfig = {
    providers: [provideHttpClient(withInterceptors([loggingInterceptor, cachingInterceptor]))],
};

Intercepting Responses

Tap into the next returned stream in order to manipulate the response.

export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
    return next(req).pipe(
        tap((event) => {
            if (event.type === HttpEventType.Response) {
                console.log(req.url, "returned a response with status", event.status);
            }
        })
    );
}

Modifying the Request

Most aspects of HttpRequest and HttpResponse instances are immutable, and interceptors cannot directly modify them.

Instead, they apply mutations by clonning these objects with .clone() operation.

export function loggingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
    const newReq = req.clone({
        headers: req.headers.set("New-header", "Value of it"),
    });
    return next(newReq);
}

The body of a request or response is NOT protected from deep mutations. If an interceptor must mutate the body, take care to handle running multiple times on the same request.

Dependency Injection in Interceptors

Interceptors are run in the injection context of the injector which registered them.

export function authInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
    // Inject the current `AuthService` and use it to get an authentication token:
    const authToken = inject(AuthService).getAuthToken();
    // Clone the request to add the authentication header.
    const newReq = req.clone({headers: {
        req.headers.append('X-Authentication-Token', authToken),
    }});
    return next(newReq);
}

Request and Response Metadata

Often it is useful to include information, in a request, that is not sent to the backend, but specifically meant for interceptors.

HttpRequest have a .context object which stores this kind of metada.

The .context is mutable.

  • If an interceptor changes the context of a request that is later retried, the same interceptor will observe the context mutation when it runs again.

  • This is useful for passing state across multiple retries if needed.

Defining the Tokens

Define a new HttpContextToken to act as a key in the .context.

// Suppose you want to store this token in the request metada
export const CACHING_ENABLED = new HttpContextToken<boolean>(() => true);

Reading the token inside the Interceptor

export function cachingInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn): Observable<HttpEvent<unknown>> {
    // Access the token in the `.context`
    if (req.context.get(CACHING_ENABLED)) {
        return ...;
    } else {
        return next(req);
    }
}

Setting context tokens when making the requests

http.get("/api/data", {
    context: new HttpContext().set(CACHING_ENABLED, false),
});

Synthetic Responses

Interceptors are not required to invoke next.

They may instead choose to construct responses through some other mechanims, such as from a cache or by sending the request through an alternate mechanims.

You can construct a response with:

const resp = new HttpResponse({
    body: "Some response body",
});

DI-based Interceptors

A DI-based interceptor is an injectable class which implements the HttpInterceptor interface.

These interceptors will run in the order that their providers are registered.

  • So, in apps with extensive and hierarchical DI configuration, this order can be very hard to predict.

@Injectable()
class LoggingInterceptor implements HttpInterceptor {
    // The interface will implement the `intercept` method
    intercept(req: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {
        console.log("Request URL: " + req.url);
        return handler.handle(req);
    }
}

They are configured through a dependency injection multi-provider:

export const appConfig: ApplicationConfig = {
    providers: [
        provideHttpClient(
            // DI-based interceptors must be explicitly enabled.
            withInterceptorsFromDi()
        ),
        { provide: HTTP_INTERCEPTORS, useClass: LoggingInterceptor, multi: true },
    ],
};

For intercepting the Response:

class LoggingInterceptor implements HttpInterceptor {
    intercept(req: HttpRequest<any>, handler: HttpHandler): Observable<HttpEvent<any>> {
        // The `handle` return an Observable
        return handler.handle(req).pipe(
            // You always get an `event`
            tap((event) => {
                // You can work on all the different `HttpEventTypes`
                if (event.type === HttpEventType.Response) {
                    console.log("Response has arrived", event.body);
                }
            })
        );
    }
}

Last updated