Angular's httpResource
simplifies data fetching, and HttpContext
allows you to pass metadata to HTTP interceptors. This combination provides a powerful way to manage HTTP requests, especially when you need to modify them dynamically.
This blog post will explore how to integrate httpResource
and HttpContext
. We'll focus on a practical example: setting the responseType of an HTTP request based on context.
The Problem It Solves
Sometimes, you need to provide additional information to your HTTP requests that isn't part of the request body or headers. For example, you might need to tell an interceptor how to handle the response, such as specifying the responseType. HttpContext lets you attach this metadata to the request, and interceptors can then use this metadata to modify the request before it's sent. This is particularly useful when working with binary data, where you might need to specify whether you want an ArrayBuffer
or a Blob
.
First, let's update Angular to the latest version. As of this writing, the Angular version is 19.2.6.
ng update @angular/core @angular/cli
In this demo, we'll create components that download sound and image files in arraybuffer and blob types. The HttpResourceRequest
options will intentionally omit the responseType
property to demonstrate the usage of HttpContextToken
. Without setting the responseType, the requests would result in an error: "Http failure during parsing for ''<URL>". We'll use an HTTP interceptor to set this responseType based on the context dynamically.
Defining HttpContextToken
First, we define a type and then create an HttpContextToken
:
export type BinaryResponseType = 'arraybuffer' | 'blob' | '';
export const RESPONSE_TYPE = new HttpContextToken<BinaryResponseType>(() => 'arraybuffer');
Here, BinaryResponseType
defines the possible values for the response type. RESPONSE_TYPE
is an HttpContextToken
that holds the response type. The token is initialized with a default value of 'arraybuffer'. This default value will be used if no specific responseType is provided in the request context.
Creating an HTTP Interceptor
Next, we create an HTTP interceptor that reads the responseType
from the HttpContext
and modifies the request accordingly:
export function requestInterceptor(req: HttpRequest<unknown>, next: HttpHandlerFn) {
const responseType = req.context.get(RESPONSE_TYPE);
if (responseType !== '') {
const reqCloned = req.clone({ responseType });
return next(reqCloned);
}
return next(req);
}
The requestInterceptor
function intercepts outgoing HTTP requests. It retrieves the responseType from the request's HttpContext using req.context.get(RESPONSE_TYPE)
. If a responseType is present (i.e., not an empty string), the interceptor clones the original request using req.clone()
and sets the responseType property. This cloned request, with the modified responseType, is then passed to the next handler in the chain (or, if this is the last interceptor, to the actual HTTP request). If no responseType is found in the context, the original request is passed along unmodified.
Configuring the Interceptors
We then configure the interceptor in our application's configuration:
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(
withInterceptors([requestInterceptor])
),
],
};
bootstrapApplication(App, appConfig);
In the appConfig
, we use provideHttpClient
to configure the Angular HttpClient. The withInterceptors
function takes an array of interceptor functions. We include our requestInterceptor
in this array. This ensures that our interceptor is applied to all the HTTP requests. The bootstrapApplication
function is used to bootstrap the main component of the Angular application (App) with the configured appConfig
.
Utility Functions
Here's a utility function to create a URL from the binary data:
export function createURLFromBinary(responseType: BinaryResponseType, value: unknown | undefined) {
if (value) {
const data = responseType === 'blob' ?
value as Blob :
new Blob([value as ArrayBuffer]);
return URL.createObjectURL(data);
}
return undefined;
}
The createURLFromBinary
function takes the responseType
and the binary data (value) as input. It converts the value (which can be either an ArrayBuffer or a Blob) into a Blob object. It then uses URL.createObjectURL()
to create a unique URL that points to this Blob in memory. This URL can then be used as the source for an audio or img element. If the value is undefined, the function returns undefined.
Creating HTTP Requests with Context
This function creates the HttpResourceRequest with the specified context:
export function makeHttpRequest(url: string, responseType: BinaryResponseType) {
const defaultOptions = {
url,
reportProgress: true,
method: 'GET',
};
return responseType ? {
...defaultOptions,
context: new HttpContext().set(RESPONSE_TYPE, responseType),
} : defaultOptions;
}
The makeHttpRequest
function constructs the options object needed for an httpResource request. It takes the URL and the desired responseType as arguments. If a responseType is provided, it creates a new HttpContext and sets the RESPONSE_TYPE
token to the given responseType. This HttpContext
is then included in the request options. If no responseType
is provided, it returns the default options without any context.
Retrieving Binary Data with httpResource
Here's how we use these components:
const PIKACHU_OGG_URL = 'https://raw.githubusercontent.com/PokeAPI/cries/main/cries/pokemon/latest/25.ogg';
@Component({
selector: 'app-resource-pokemon-audio',
template: `
<div>
<button (click)="audioSignal.set(audioUrl)">
<ng-content>button</ng-content>
</button>
@if (audioSrc()) {
<label>Listen to Pikachu:</label>
<audio [src]="audioSrc()" controls></audio>
}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HttpResourcePokemonAudioComponent {
audioUrl = PIKACHU_OGG_URL;
audioSignal = signal('');
responseType = input<BinaryResponseType>('');
audioResourceRef = httpResource(
() => this.audioSignal() ? makeHttpRequest(this.audioSignal(), this.responseType()) : undefined
);
audioSrc = computed(() => {
const value = this.audioResourceRef.hasValue() ?
this.audioResourceRef.value() : undefined;
return createURLFromBinary(this.responseType(), value);
});
}
const PIKACHU_IMAGE_URL = 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/25.png';
@Component({
selector: 'app-resource-pokemon',
template: `
<div>
<button (click)="imageSignal.set(imageUrl)">
<ng-content>button</ng-content>
</button>
@if (imgSrc()) {
<label>Pikachu:</label>
<img [src]="imgSrc()" />
}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HttpResourcePokemonComponent {
imageUrl = PIKACHU_IMAGE_URL;
imageSignal = signal('');
responseType = input<BinaryResponseType>('');
imgResourceRef = httpResource(
() => this.imageSignal() ? makeHttpRequest(this.imageSignal(), this.responseType()) : undefined
);
imgSrc = computed(() => {
const value = this.imgResourceRef.hasValue() ?
this.imgResourceRef.value() : undefined;
return createURLFromBinary(this.responseType(), value);
});
}
@Component({
selector: 'app-root',
imports: [
HttpResourcePokemonAudioComponent,
HttpResourcePokemonComponent,
],
template: `
<div>
<app-resource-pokemon>
Retrieve Pikachu ArrayBuffer
</app-resource-pokemon>
<app-resource-pokemon responseType='blob'>
Retrieve Pikachu Blob
</app-resource-pokemon>
</div>
<div>
<app-resource-pokemon-audio>
Retrieve ArrayBuffer Pikachu Cry
</app-resource-pokemon-audio>
<app-resource-pokemon-audio responseType='blob'>
Retrieve Blob Pikachu Cry
</app-resource-pokemon-audio>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App { }
In this code, the App
component serves as the root component of the Angular application. Its primary responsibility is to integrate and render the HttpResourcePokemonComponent
and HttpResourcePokemonAudioComponent
.
Let's break down the App component:
- Imports: The App component imports HttpResourcePokemonAudioComponent and HttpResourcePokemonComponent.
- Template: The template of the App component defines the structure of what will be rendered in the application's main view.
- It includes two instances of HttpResourcePokemonComponent:
- The first one uses the default responseType (which is 'arraybuffer' as defined in the RESPONSE_TYPE token).
- The second one explicitly sets the responseType to 'blob'.
- It also includes two instances of HttpResourcePokemonAudioComponent:
- The first one uses the default responseType ('arraybuffer').
- The second one sets the responseType to 'blob'.
- It includes two instances of HttpResourcePokemonComponent:
- app-root Selector: The App component is typically associated with the selector in the index.html file. This means that when the Angular application is loaded, the App component is the first component to be rendered in the browser.
Benefits
Storing the response type in the HttpContext and overwriting it in an interceptor offers several advantages:
- Decoupling: The component making the HTTP request doesn't need to know how the request is being modified. It simply sets the desired responseType in the context.
- Centralized Logic: The interceptor centralizes the logic for handling the responseType. This keeps your components cleaner and easier to maintain.
- Dynamic Configuration: You can dynamically determine the responseType at runtime based on application state or user input.
- Reusability: The interceptor can be reused across multiple components and services.
Conclusion
By combining httpResource
with HttpContext
and HTTP interceptors, you can create a flexible and maintainable way to handle HTTP requests with dynamic metadata. This approach allows you to keep your components focused on their core logic while centralizing request modification logic in interceptors. This pattern is especially useful for scenarios like handling different response types, adding custom headers, or implementing authentication.
Resources
- The PR relevant to httpResource - https://github.com/angular/angular/pull/60188
- The HttpResource documentation: https://angular.dev/api/common/http/httpResource
- The HttpContextToken documentation: https://angular.dev/api/common/http/HttpInterceptor
- Angular interceptors guide: https://angular.dev/guide/http/interceptors#request-and-response-metadata
- Stackblitz Demo: https://stackblitz.com/edit/stackblitz-starters-9yw3ft92?file=src%2Finterceptors%2Frequest.interceptor.ts
Top comments (0)