Utilities and Lifecycle
This guide covers advanced utilities, lifecycle methods, error handling, and cache implementation details for the Aurelia Fetch Client.
Advanced HttpClient Methods
buildRequest()
The buildRequest() method allows you to construct a Request object using the HttpClient's configuration without actually sending the request. This is useful for request inspection, manual request manipulation, or integration with other libraries.
Method Signature
buildRequest(input: string | Request, init?: RequestInit): RequestHow It Works
The buildRequest() method:
Applies the client's
baseUrlto relative URLsMerges the client's default
RequestInitsettings with provided optionsApplies default headers
Auto-detects JSON content and sets appropriate
Content-TypeheaderReturns a fully-configured
Requestobject
Basic Usage
import { IHttpClient } from '@aurelia/fetch-client';
import { resolve } from '@aurelia/kernel';
export class RequestBuilderService {
private http = resolve(IHttpClient);
constructor() {
this.http.configure(config => config
.withBaseUrl('https://api.example.com')
.withDefaults({
headers: {
'Authorization': 'Bearer token123',
'Accept': 'application/json'
}
})
);
}
buildExampleRequest() {
// Build a request without sending it
const request = this.http.buildRequest('/users/123');
console.log(request.url); // 'https://api.example.com/users/123'
console.log(request.method); // 'GET'
console.log(request.headers.get('Authorization')); // 'Bearer token123'
console.log(request.headers.get('Accept')); // 'application/json'
return request;
}
}Advanced Request Building
export class AdvancedRequestBuilder {
private http = resolve(IHttpClient);
buildPostRequest() {
// Build a POST request with body
const request = this.http.buildRequest('/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'John Doe', email: '[email protected]' })
});
// Content-Type automatically set to 'application/json' when body is JSON
console.log(request.headers.get('Content-Type')); // 'application/json'
return request;
}
buildRequestWithCustomHeaders() {
const request = this.http.buildRequest('/api/data', {
headers: {
'X-Custom-Header': 'CustomValue'
}
});
// Default headers are merged with custom headers
return request;
}
buildFromExistingRequest() {
// You can also pass an existing Request object
const originalRequest = new Request('https://example.com/api/data');
const enhancedRequest = this.http.buildRequest(originalRequest);
// The enhanced request will have the client's defaults applied
return enhancedRequest;
}
}Practical Use Cases
1. Request Inspection and Debugging
export class RequestDebugger {
private http = resolve(IHttpClient);
async inspectRequest(url: string, init?: RequestInit) {
// Build the request to inspect it before sending
const request = this.http.buildRequest(url, init);
console.group('Request Details');
console.log('URL:', request.url);
console.log('Method:', request.method);
console.log('Headers:', Object.fromEntries(request.headers.entries()));
console.log('Mode:', request.mode);
console.log('Credentials:', request.credentials);
console.groupEnd();
// Now send it
return this.http.fetch(request);
}
}2. Manual Request Queue Management
export class RequestQueue {
private http = resolve(IHttpClient);
private queue: Request[] = [];
queueRequest(url: string, init?: RequestInit) {
// Build requests and add to queue
const request = this.http.buildRequest(url, init);
this.queue.push(request);
}
async processQueue() {
console.log(`Processing ${this.queue.length} queued requests`);
// Process all queued requests
const results = await Promise.all(
this.queue.map(request => this.http.fetch(request))
);
this.queue = [];
return results;
}
}3. Integration with Third-Party Libraries
export class RequestAdapter {
private http = resolve(IHttpClient);
buildForExternalLibrary(url: string) {
// Build request with HttpClient configuration
const request = this.http.buildRequest(url);
// Pass to third-party library that expects a Request object
return someExternalLibrary.processRequest(request);
}
buildForWebSocket(url: string) {
// Build HTTP request to get configuration
const httpRequest = this.http.buildRequest(url);
// Use request details to configure WebSocket
const wsUrl = httpRequest.url.replace('http', 'ws');
const authHeader = httpRequest.headers.get('Authorization');
return new WebSocket(wsUrl, ['protocol', authHeader]);
}
}4. Conditional Request Execution
export class ConditionalRequestService {
private http = resolve(IHttpClient);
async fetchWithCondition(url: string, shouldFetch: () => boolean) {
// Build the request early
const request = this.http.buildRequest(url);
// Perform expensive computation or wait for condition
await this.waitForCondition();
if (shouldFetch()) {
// Send the pre-built request
return this.http.fetch(request);
} else {
console.log('Request cancelled based on condition');
return null;
}
}
private waitForCondition(): Promise<void> {
return new Promise(resolve => setTimeout(resolve, 1000));
}
}Important Notes
BaseURL Resolution: Relative URLs are resolved against the configured
baseUrlHeader Merging: Default headers are merged with request-specific headers (request headers take precedence)
Content-Type Detection: JSON bodies automatically get
Content-Type: application/jsonRequest Reusability: Built
Requestobjects can be reused withfetch()but remember that request bodies can only be read once
dispose()
The dispose() method performs cleanup of the HttpClient instance, releasing resources and cleaning up interceptors.
Method Signature
dispose(): voidWhat It Does
When dispose() is called:
Calls
dispose()on all registered interceptors (if they implement it)Clears the interceptor array
Removes the event dispatcher reference
Basic Usage
import { IHttpClient } from '@aurelia/fetch-client';
import { resolve } from '@aurelia/kernel';
export class HttpClientService {
private http = resolve(IHttpClient);
constructor() {
this.setupClient();
}
private setupClient() {
this.http.configure(config => config
.withBaseUrl('https://api.example.com')
.withInterceptor({
request: (request) => {
console.log('Processing request');
return request;
},
dispose: () => {
console.log('Interceptor disposed');
}
})
);
}
// Cleanup method
dispose() {
console.log('Disposing HttpClient');
this.http.dispose();
// This will:
// 1. Call dispose() on all interceptors
// 2. Clear the interceptor array
// 3. Remove dispatcher reference
}
}Interceptor Cleanup
Interceptors can implement a dispose() method for cleanup:
export class ResourceManagingInterceptor implements IFetchInterceptor {
private intervalId: number;
private eventListeners: Array<{ target: EventTarget; type: string; listener: EventListener }> = [];
constructor() {
// Set up resources
this.intervalId = setInterval(() => {
console.log('Background task');
}, 60000);
// Add event listeners
const listener = () => console.log('Event');
document.addEventListener('visibilitychange', listener);
this.eventListeners.push({ target: document, type: 'visibilitychange', listener });
}
request(request: Request): Request {
return request;
}
dispose(): void {
// Clean up interval
clearInterval(this.intervalId);
// Remove event listeners
this.eventListeners.forEach(({ target, type, listener }) => {
target.removeEventListener(type, listener);
});
this.eventListeners.length = 0;
console.log('Interceptor resources cleaned up');
}
}Component Lifecycle Integration
Integrate with Aurelia component lifecycle:
export class ApiService {
private http = resolve(IHttpClient);
constructor() {
this.setupHttpClient();
}
private setupHttpClient() {
this.http.configure(config => config
.withBaseUrl('https://api.example.com')
.withInterceptor(new ResourceManagingInterceptor())
);
}
async fetchData() {
return this.http.get('/data');
}
// Called by Aurelia when component is disposed
dispose() {
// Clean up HttpClient and all its interceptors
this.http.dispose();
}
}Complete Cleanup Example
export class ManagedHttpClientService {
private http = resolve(IHttpClient);
private cacheInterceptor: CacheInterceptor;
private retryInterceptor: RetryInterceptor;
constructor() {
this.cacheInterceptor = new CacheInterceptor({ cacheTime: 300_000 });
this.retryInterceptor = new RetryInterceptor({ maxRetries: 3 });
this.http.configure(config => config
.withInterceptor(this.cacheInterceptor)
.withInterceptor(this.retryInterceptor)
);
}
dispose() {
// Option 1: Dispose individual interceptors manually
this.cacheInterceptor.dispose?.();
this.retryInterceptor.dispose?.();
// Option 2: Dispose entire client (calls dispose on all interceptors)
this.http.dispose();
console.log('All resources cleaned up');
}
}Best Practices
Always implement cleanup: If your interceptor allocates resources, implement
dispose()Component integration: Call
http.dispose()in componentdispose()methodsSingleton clients: For application-scoped clients, dispose on application shutdown
Testing: Always dispose clients in test cleanup to prevent memory leaks
Utility Functions
json()
A utility function for serializing objects to JSON strings, primarily for creating request bodies.
Function Signature
function json(body: unknown, replacer?: (key: string, value: unknown) => unknown): stringBasic Usage
import { json } from '@aurelia/fetch-client';
export class JsonUtilityExample {
createJsonBody() {
const user = {
name: 'John Doe',
email: '[email protected]',
age: 30
};
// Simple JSON serialization
const jsonString = json(user);
console.log(jsonString); // '{"name":"John Doe","email":"[email protected]","age":30}'
return jsonString;
}
}With Request Body
import { IHttpClient, json } from '@aurelia/fetch-client';
import { resolve } from '@aurelia/kernel';
export class UserService {
private http = resolve(IHttpClient);
async createUser(userData: UserData) {
// Use json() utility to create request body
const response = await this.http.post('/api/users', json(userData));
return response.json();
}
async updateUser(userId: string, updates: Partial<UserData>) {
const response = await this.http.put(
`/api/users/${userId}`,
json(updates)
);
return response.json();
}
}
interface UserData {
name: string;
email: string;
age: number;
preferences?: Record<string, unknown>;
}Custom Replacer Function
The replacer parameter allows you to customize serialization:
export class AdvancedJsonService {
createFilteredJson() {
const data = {
name: 'John',
password: 'secret123', // Should not be serialized
email: '[email protected]',
internalId: '12345' // Should not be serialized
};
// Filter out sensitive fields
const jsonString = json(data, (key, value) => {
if (key === 'password' || key === 'internalId') {
return undefined; // Exclude from JSON
}
return value;
});
console.log(jsonString); // '{"name":"John","email":"[email protected]"}'
return jsonString;
}
createTransformedJson() {
const data = {
createdAt: new Date('2024-01-01'),
updatedAt: new Date('2024-01-15'),
values: [1, 2, 3, 4, 5]
};
// Transform values during serialization
const jsonString = json(data, (key, value) => {
// Convert dates to ISO strings
if (value instanceof Date) {
return value.toISOString();
}
// Convert arrays to comma-separated strings
if (Array.isArray(value)) {
return value.join(',');
}
return value;
});
console.log(jsonString);
// '{"createdAt":"2024-01-01T00:00:00.000Z","updatedAt":"2024-01-15T00:00:00.000Z","values":"1,2,3,4,5"}'
return jsonString;
}
}Handling Edge Cases
export class EdgeCaseHandling {
testEdgeCases() {
// Undefined becomes empty object
console.log(json(undefined)); // '{}'
// Null is preserved
console.log(json(null)); // 'null'
// Empty object
console.log(json({})); // '{}'
// Circular references will throw (use replacer to handle)
const circular: any = { name: 'test' };
circular.self = circular;
try {
json(circular);
} catch (error) {
console.error('Cannot serialize circular reference');
}
}
handleCircularReferences() {
const seen = new WeakSet();
const data: any = { name: 'test' };
data.self = data;
const jsonString = json(data, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
}
return value;
});
console.log(jsonString); // '{"name":"test","self":"[Circular]"}'
}
}Practical Examples
API Request Builder
export class ApiRequestBuilder {
private http = resolve(IHttpClient);
async createResource(type: string, data: Record<string, unknown>) {
// Wrap data in API envelope format
const envelope = {
type,
data,
timestamp: new Date(),
version: '1.0'
};
const response = await this.http.post(
'/api/resources',
json(envelope, (key, value) => {
// Convert dates to ISO strings
if (value instanceof Date) {
return value.toISOString();
}
return value;
})
);
return response.json();
}
}Data Sanitization
export class DataSanitizer {
private http = resolve(IHttpClient);
private sensitiveFields = ['password', 'ssn', 'creditCard', 'apiKey'];
async sendSanitizedData(url: string, data: Record<string, unknown>) {
// Automatically filter sensitive fields
const sanitized = json(data, (key, value) => {
if (this.sensitiveFields.includes(key)) {
return '[REDACTED]';
}
return value;
});
return this.http.post(url, sanitized);
}
}Comparison with JSON.stringify()
// Using json() utility
import { json } from '@aurelia/fetch-client';
const body1 = json({ name: 'John' }); // Returns '{"name":"John"}'
const body2 = json(undefined); // Returns '{}'
// Using JSON.stringify() directly
const body3 = JSON.stringify({ name: 'John' }); // Returns '{"name":"John"}'
const body4 = JSON.stringify(undefined); // Returns 'undefined'
// The json() utility treats undefined as an empty object,
// which is more convenient for optional request bodiesError Handling
The Fetch Client includes a comprehensive error code system for debugging and error handling.
Error Code System
All errors from the Fetch Client use the AUR50XX code range and include helpful error messages in development mode.
Error Codes Reference
enum ErrorNames {
http_client_fetch_fn_not_found = 5000,
http_client_configure_invalid_return = 5001,
http_client_configure_invalid_config = 5002,
http_client_configure_invalid_header = 5003,
http_client_more_than_one_retry_interceptor = 5004,
http_client_retry_interceptor_not_last = 5005,
http_client_invalid_request_from_interceptor = 5006,
retry_interceptor_invalid_exponential_interval = 5007,
retry_interceptor_invalid_strategy = 5008,
}AUR5000: Fetch Function Not Found
Error Message: "Could not resolve fetch function. Please provide a fetch function implementation or a polyfill for the global fetch function."
Cause: The global fetch function is not available.
Solution: Provide a fetch polyfill or implementation:
// Install a fetch polyfill
import 'whatwg-fetch';
// Or provide custom fetch implementation
import { DI } from '@aurelia/kernel';
import { IFetchFn } from '@aurelia/fetch-client';
DI.getGlobalContainer().register(
Registration.instance(IFetchFn, myCustomFetchImplementation)
);AUR5001: Invalid Configuration Return
Error Message: "The config callback did not return a valid HttpClientConfiguration like instance. Received {type}"
Cause: Configuration callback returned an invalid value.
Solution: Ensure your configuration callback returns a valid configuration:
// Wrong - returning wrong type
this.http.configure(config => {
return 'invalid'; // ❌ Returns string instead of configuration
});
// Correct - return configuration or void
this.http.configure(config => {
config.withBaseUrl('https://api.example.com');
return config; // ✅ Return the configuration object
});
// Also correct - no return (void)
this.http.configure(config => {
config.withBaseUrl('https://api.example.com');
// ✅ Void return is fine
});AUR5002: Invalid Configuration Type
Error Message: "invalid config, expecting a function or an object, received {type}"
Cause: Called configure() with an invalid argument type.
Solution: Pass either a function or RequestInit object:
// Wrong - invalid type
this.http.configure('invalid'); // ❌
// Correct - function
this.http.configure(config => {
config.withBaseUrl('https://api.example.com');
}); // ✅
// Correct - RequestInit object
this.http.configure({
headers: { 'Accept': 'application/json' }
}); // ✅AUR5003: Invalid Default Headers
Error Message: "Default headers must be a plain object."
Cause: Provided a Headers instance instead of a plain object for default headers.
Solution: Use plain objects for default headers:
// Wrong - Headers instance
this.http.configure(config => config.withDefaults({
headers: new Headers({ 'Accept': 'application/json' }) // ❌
}));
// Correct - plain object
this.http.configure(config => config.withDefaults({
headers: { 'Accept': 'application/json' } // ✅
}));AUR5004: Multiple Retry Interceptors
Error Message: "Only one RetryInterceptor is allowed."
Cause: Attempted to register more than one RetryInterceptor.
Solution: Use only one retry interceptor:
// Wrong - multiple retry interceptors
this.http.configure(config => config
.withRetry({ maxRetries: 3 })
.withRetry({ maxRetries: 5 }) // ❌ Second retry interceptor
);
// Correct - single retry interceptor
this.http.configure(config => config
.withRetry({ maxRetries: 3 }) // ✅
);AUR5005: Retry Interceptor Not Last
Error Message: "The retry interceptor must be the last interceptor defined."
Cause: The retry interceptor was not registered as the final interceptor.
Solution: Always register retry interceptor last:
// Wrong - retry not last
this.http.configure(config => config
.withRetry({ maxRetries: 3 }) // ❌ Not last
.withInterceptor(loggingInterceptor) // This comes after retry
);
// Correct - retry is last
this.http.configure(config => config
.withInterceptor(loggingInterceptor)
.withRetry({ maxRetries: 3 }) // ✅ Last interceptor
);AUR5006: Invalid Interceptor Result
Error Message: "An invalid result was returned by the interceptor chain. Expected a Request or Response instance, but got [{value}]"
Cause: An interceptor returned an invalid value (not a Request or Response).
Solution: Ensure interceptors return valid types:
// Wrong - returning invalid type
config.withInterceptor({
request: (request) => {
return 'invalid'; // ❌ Must return Request or Response
}
});
// Correct - return Request
config.withInterceptor({
request: (request) => {
return request; // ✅ Return Request object
}
});
// Correct - return Response to short-circuit
config.withInterceptor({
request: (request) => {
return new Response('cached'); // ✅ Return Response to bypass fetch
}
});AUR5007: Invalid Exponential Interval
Error Message: "An interval less than or equal to 1 second is not allowed when using the exponential retry strategy. Received: {interval}"
Cause: Exponential retry strategy configured with too short an interval.
Solution: Use an interval > 1000ms for exponential strategy:
// Wrong - interval too short for exponential
this.http.configure(config => config.withRetry({
strategy: RetryStrategy.exponential,
interval: 500 // ❌ < 1000ms
}));
// Correct - interval >= 1000ms
this.http.configure(config => config.withRetry({
strategy: RetryStrategy.exponential,
interval: 2000 // ✅ >= 1000ms
}));AUR5008: Invalid Retry Strategy
Error Message: "Invalid retry strategy: {strategy}"
Cause: Provided an invalid retry strategy value.
Solution: Use valid retry strategy constants:
import { RetryStrategy } from '@aurelia/fetch-client';
// Wrong - invalid strategy
this.http.configure(config => config.withRetry({
strategy: 'invalid' // ❌
}));
// Correct - use RetryStrategy enum
this.http.configure(config => config.withRetry({
strategy: RetryStrategy.fixed // ✅
}));
// Available strategies:
// - RetryStrategy.fixed
// - RetryStrategy.incremental
// - RetryStrategy.exponentialError Handling Best Practices
Development vs Production
export class ErrorAwareService {
private http = resolve(IHttpClient);
async fetchWithErrorHandling(url: string) {
try {
return await this.http.get(url);
} catch (error) {
if (error instanceof Error) {
// In development, errors include full details and documentation links
if (process.env.NODE_ENV === 'development') {
console.error('Detailed error:', error.message);
// Error format: "AUR5000: <message>\n\nFor more information, see: <docs link>"
} else {
// In production, errors are concise
console.error('Error code:', error.message.split(':')[0]);
}
}
throw error;
}
}
}Cache Implementation Details
Cache Key Generation
The cache interceptor uses a simple but effective cache key strategy:
// Cache key format
const cacheKey = `${CacheInterceptor.prefix}${request.url}`;
// Example: 'au:interceptor:https://api.example.com/users/123'Key Components:
Prefix:
'au:interceptor:'- Identifies Aurelia cache entriesURL: Full request URL including query parameters
Important Notes:
Only the URL is used for cache keys
Request headers are NOT part of the cache key
Query parameters ARE part of the cache key (different query = different cache entry)
Cache Key Examples
// These create different cache entries:
http.get('/api/users?page=1'); // Key: 'au:interceptor:/api/users?page=1'
http.get('/api/users?page=2'); // Key: 'au:interceptor:/api/users?page=2'
// These share the same cache entry:
http.get('/api/users', { headers: { 'X-Custom': 'A' } });
http.get('/api/users', { headers: { 'X-Custom': 'B' } });
// Both use key: 'au:interceptor:/api/users'Cache Header Marker
The cache interceptor uses a custom header to mark cached responses:
// Header name
CacheInterceptor.cacheHeader = 'x-au-fetch-cache';
// Header value for cache hits
response.headers.get('x-au-fetch-cache'); // 'hit'Usage:
export class CacheAwareService {
private http = resolve(IHttpClient);
async fetchWithCacheDetection(url: string) {
const response = await this.http.get(url);
if (response.headers.has('x-au-fetch-cache')) {
console.log('Response served from cache');
} else {
console.log('Response fetched from server');
}
return response.json();
}
}Refresh Stale Immediate
When refreshStaleImmediate: true is configured, the cache interceptor sets up automatic refresh timers:
const cacheConfig = {
staleTime: 60_000, // 1 minute
refreshStaleImmediate: true // Enable automatic refresh
};Behavior:
When data is cached, a timer is set for the
staleTimedurationWhen the timer fires:
The cache entry is deleted
The original request is automatically re-fetched
The cache is updated with fresh data
CacheEvent.CacheStaleRefreshedevent is published
Example:
export class AutoRefreshExample {
private http = resolve(IHttpClient);
private cacheService = resolve(ICacheService);
constructor() {
const cacheInterceptor = new CacheInterceptor({
cacheTime: 300_000, // 5 minutes total cache time
staleTime: 60_000, // 1 minute until stale
refreshStaleImmediate: true // Auto-refresh when stale
});
this.http.configure(config => config.withInterceptor(cacheInterceptor));
// Monitor refresh events
this.cacheService.subscribe(CacheEvent.CacheStaleRefreshed, (data) => {
console.log('Cache automatically refreshed:', data.key);
});
}
async getData() {
// First call: fetches from server, caches for 5 min, sets 1 min stale timer
const data1 = await this.http.get('/api/data');
// Calls within 1 minute: served from cache
const data2 = await this.http.get('/api/data');
// After 1 minute: cache automatically refreshed in background
// After refresh: new 5 min cache, new 1 min stale timer set
return data1;
}
}Summary
This guide covered:
buildRequest(): Build requests without sending them
dispose(): Proper cleanup of HttpClient and interceptors
json(): Utility for JSON serialization with custom replacers
Error Codes: Complete AUR50XX error reference with solutions
Cache Details: Key generation, cache headers, and refresh behavior
These advanced features enable robust, production-ready HTTP client implementations with proper resource management and error handling.
Last updated
Was this helpful?