HTTP Client
About
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
HttpClient
withFetch
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(...)
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.
The request is ONLY sent if you subscribe to the Observable.
@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 theBlob
's content type.FormData
: Asmultipart/form-data
encoded data.HttpParams
orURLSearchParams
: Asapplication/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.
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;
}
});
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 of0
.An
error
which is an instance ofProgressEvent
.
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
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
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
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
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