Advanced

The Aurelia Fetch Client offers powerful advanced features for enterprise applications, including sophisticated caching, request monitoring, custom interceptor patterns, and integration with complex authentication systems.

Advanced Header Management

Dynamic Authorization Headers

Headers can be functions that are evaluated for each request, perfect for handling token refresh scenarios:

import { IHttpClient } from '@aurelia/fetch-client';
import { resolve } from '@aurelia/kernel';

export class AuthenticatedApiService {
  private http = resolve(IHttpClient);
  private tokenStorage = new TokenStorage();

  constructor() {
    this.http.configure(config => config
      .withDefaults({
        headers: {
          'Accept': 'application/json',
          'Content-Type': 'application/json',
          'Authorization': () => {
            const token = this.tokenStorage.getAccessToken();
            return token ? `Bearer ${token}` : '';
          },
          'X-Client-Version': () => this.getClientVersion(),
          'X-Request-ID': () => this.generateRequestId()
        }
      })
    );
  }

  private getClientVersion(): string {
    return process.env.APP_VERSION || '1.0.0';
  }

  private generateRequestId(): string {
    return `${Date.now()}-${Math.random().toString(36).substring(2)}`;
  }
}

class TokenStorage {
  getAccessToken(): string | null {
    const tokenData = localStorage.getItem('auth_tokens');
    if (!tokenData) return null;

    const { accessToken, expiresAt } = JSON.parse(tokenData);
    
    // Check if token is expired
    if (Date.now() >= expiresAt) {
      this.refreshToken();
      return this.getAccessToken(); // Recursive call after refresh
    }

    return accessToken;
  }

  private refreshToken(): void {
    // Token refresh logic here
    // This is synchronous for simplicity, but could be async
  }
}

Conditional Headers

Apply different headers based on request characteristics:

export class ConditionalHeaderService {
  private http = resolve(IHttpClient);

  constructor() {
    this.http.configure(config => config.withInterceptor({
      request(request) {
        const url = new URL(request.url);
        
        // Add API key for external APIs
        if (url.hostname !== window.location.hostname) {
          request.headers.set('X-API-Key', this.getExternalApiKey(url.hostname));
        }
        
        // Add CSRF token for state-changing operations
        if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method)) {
          const csrfToken = this.getCsrfToken();
          if (csrfToken) {
            request.headers.set('X-CSRF-Token', csrfToken);
          }
        }
        
        // Add correlation ID for internal services
        if (url.pathname.startsWith('/api/internal/')) {
          request.headers.set('X-Correlation-ID', this.generateCorrelationId());
        }

        return request;
      }
    }));
  }

  private getExternalApiKey(hostname: string): string {
    const keyMap = {
      'api.external1.com': process.env.EXTERNAL_API_1_KEY,
      'api.external2.com': process.env.EXTERNAL_API_2_KEY
    };
    return keyMap[hostname] || '';
  }

  private getCsrfToken(): string {
    return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
  }

  private generateCorrelationId(): string {
    return `corr-${Date.now()}-${Math.random().toString(36).substring(2)}`;
  }
}

Built-in Cache Interceptor

The fetch client includes a sophisticated caching system with multiple storage options:

import { IHttpClient, CacheInterceptor } from '@aurelia/fetch-client';
import { DI } from '@aurelia/kernel';

export class CachedApiService {
  private http = resolve(IHttpClient);

  constructor() {
    this.setupCaching();
  }

  private setupCaching() {
    // Create cache interceptor with configuration
    const cacheInterceptor = DI.getGlobalContainer().invoke(CacheInterceptor, [{
      cacheTime: 300_000,        // Cache for 5 minutes
      staleTime: 60_000,         // Data becomes stale after 1 minute
      refreshStaleImmediate: false, // Don't block on stale refresh
      refreshInterval: 30_000,   // Background refresh every 30 seconds
    }]);

    this.http.configure(config => config.withInterceptor(cacheInterceptor));
  }

  // Cache interceptor automatically handles these GET requests
  async getUserProfile(userId: string) {
    const response = await this.http.get(`/api/users/${userId}`);
    return response.json();
  }

  async getStaticData() {
    // This will be cached and refreshed in background
    const response = await this.http.get('/api/config/static');
    return response.json();
  }
}

Custom Cache Storage

Use different storage backends for caching:

import { 
  CacheInterceptor, 
  BrowserLocalStorage, 
  BrowserSessionStorage,
  BrowserIndexDBStorage,
  MemoryStorage 
} from '@aurelia/fetch-client';

export class CustomCacheService {
  private http = resolve(IHttpClient);

  setupPersistentCaching() {
    // Use localStorage for persistent caching across sessions
    const persistentCache = DI.getGlobalContainer().invoke(CacheInterceptor, [{
      cacheTime: 3600_000, // 1 hour
      storage: new BrowserLocalStorage()
    }]);

    this.http.configure(config => config.withInterceptor(persistentCache));
  }

  setupSessionCaching() {
    // Use sessionStorage for session-only caching
    const sessionCache = DI.getGlobalContainer().invoke(CacheInterceptor, [{
      cacheTime: 1800_000, // 30 minutes
      storage: new BrowserSessionStorage()
    }]);

    this.http.configure(config => config.withInterceptor(sessionCache));
  }

  setupIndexDBCaching() {
    // Use IndexedDB for large data caching
    const indexDbCache = DI.getGlobalContainer().invoke(CacheInterceptor, [{
      cacheTime: 7200_000, // 2 hours
      storage: new BrowserIndexDBStorage()
    }]);

    this.http.configure(config => config.withInterceptor(indexDbCache));
  }
}

Request Tracking and Lifecycle Management

The HttpClient provides built-in request tracking capabilities that enable you to monitor active requests and respond to request lifecycle events. This is essential for building loading indicators, progress tracking, and request coordination features.

Built-in Request Properties

The HttpClient exposes two key properties for request tracking:

import { IHttpClient } from '@aurelia/fetch-client';
import { resolve } from '@aurelia/kernel';

export class RequestTrackerService {
  private http = resolve(IHttpClient);

  checkRequestStatus() {
    // Get the current number of active requests
    console.log('Active requests:', this.http.activeRequestCount);

    // Check if any requests are currently active
    console.log('Is requesting:', this.http.isRequesting);
  }

  async makeTrackedRequest() {
    console.log('Before request:', this.http.activeRequestCount);  // 0
    console.log('Is requesting:', this.http.isRequesting);         // false

    const promise = this.http.get('/api/data');

    console.log('During request:', this.http.activeRequestCount);  // 1
    console.log('Is requesting:', this.http.isRequesting);         // true

    await promise;

    console.log('After request:', this.http.activeRequestCount);   // 0
    console.log('Is requesting:', this.http.isRequesting);         // false
  }
}

Key Properties:

  • activeRequestCount: The current number of active requests (including those being processed by interceptors)

  • isRequesting: Boolean indicating whether one or more requests are currently active

Request Lifecycle Events

The HttpClient can dispatch DOM events for request lifecycle tracking. This requires configuring an event dispatcher using withDispatcher().

Available Events

import { HttpClientEvent } from '@aurelia/fetch-client';

// Available lifecycle events:
HttpClientEvent.started  // 'aurelia-fetch-client-request-started'
HttpClientEvent.drained  // 'aurelia-fetch-client-requests-drained'
  • started: Fired when the first request starts (when activeRequestCount goes from 0 to 1)

  • drained: Fired when all requests complete (when activeRequestCount returns to 0)

Configuring Event Dispatcher

import { IHttpClient, HttpClientEvent } from '@aurelia/fetch-client';
import { resolve } from '@aurelia/kernel';

export class RequestEventService {
  private http = resolve(IHttpClient);

  constructor() {
    // Configure event dispatcher on a DOM node
    this.http.configure(config => config.withDispatcher(document.body));

    // Listen for lifecycle events
    this.setupEventListeners();
  }

  private setupEventListeners() {
    document.body.addEventListener(HttpClientEvent.started, (event: CustomEvent) => {
      console.log('First request started');
      // Event fires when activeRequestCount goes from 0 to 1
    });

    document.body.addEventListener(HttpClientEvent.drained, (event: CustomEvent) => {
      console.log('All requests completed');
      // Event fires when activeRequestCount returns to 0
    });
  }
}

Building a Loading Indicator

Use request tracking to implement a global loading indicator:

export class LoadingIndicatorService {
  private http = resolve(IHttpClient);
  private loadingElement: HTMLElement;

  constructor() {
    this.loadingElement = document.getElementById('loading-indicator');
    this.setupLoadingIndicator();
  }

  private setupLoadingIndicator() {
    // Configure event dispatcher
    this.http.configure(config => config.withDispatcher(document.body));

    // Show loading indicator when requests start
    document.body.addEventListener(HttpClientEvent.started, () => {
      this.showLoadingIndicator();
    });

    // Hide loading indicator when all requests complete
    document.body.addEventListener(HttpClientEvent.drained, () => {
      this.hideLoadingIndicator();
    });
  }

  private showLoadingIndicator() {
    this.loadingElement.classList.add('active');
    document.body.classList.add('loading');
  }

  private hideLoadingIndicator() {
    this.loadingElement.classList.remove('active');
    document.body.classList.remove('loading');
  }
}

Advanced Request Monitoring

Combine built-in tracking with custom monitoring:

export class AdvancedRequestMonitor {
  private http = resolve(IHttpClient);
  private requestDetails = new Map<string, {
    url: string;
    method: string;
    startTime: number;
  }>();

  constructor() {
    this.setupComprehensiveMonitoring();
  }

  private setupComprehensiveMonitoring() {
    // Configure event dispatcher
    this.http.configure(config => config
      .withDispatcher(document.body)
      .withInterceptor({
        request: (request) => {
          const requestId = this.generateRequestId();
          request.headers.set('X-Request-ID', requestId);

          // Store request details
          this.requestDetails.set(requestId, {
            url: request.url,
            method: request.method,
            startTime: Date.now(),
          });

          console.log(`[${requestId}] Starting: ${request.method} ${request.url}`);
          console.log(`Active requests: ${this.http.activeRequestCount}`);

          return request;
        },

        response: (response, request) => {
          const requestId = request?.headers.get('X-Request-ID');
          if (requestId) {
            const details = this.requestDetails.get(requestId);
            if (details) {
              const duration = Date.now() - details.startTime;
              console.log(`[${requestId}] Completed in ${duration}ms: ${response.status}`);
              this.requestDetails.delete(requestId);
            }
          }

          console.log(`Remaining requests: ${this.http.activeRequestCount - 1}`);
          return response;
        },

        responseError: (error, request) => {
          const requestId = request?.headers.get('X-Request-ID');
          if (requestId) {
            const details = this.requestDetails.get(requestId);
            if (details) {
              const duration = Date.now() - details.startTime;
              console.error(`[${requestId}] Failed after ${duration}ms`);
              this.requestDetails.delete(requestId);
            }
          }

          throw error;
        }
      })
    );

    // Listen for lifecycle events
    document.body.addEventListener(HttpClientEvent.started, () => {
      console.log('🚀 Request activity started');
      this.onRequestActivityStarted();
    });

    document.body.addEventListener(HttpClientEvent.drained, () => {
      console.log('✅ Request activity completed');
      this.onRequestActivityCompleted();
    });
  }

  private onRequestActivityStarted() {
    // Custom logic when requests begin
    // This fires only when going from 0 to 1 active requests
  }

  private onRequestActivityCompleted() {
    // Custom logic when all requests complete
    // This fires only when going from 1+ to 0 active requests
    console.log('All tracked requests completed:', this.requestDetails.size === 0);
  }

  private generateRequestId(): string {
    return `req-${Date.now()}-${Math.random().toString(36).substring(2)}`;
  }

  // Public API
  getActiveRequestCount(): number {
    return this.http.activeRequestCount;
  }

  isRequesting(): boolean {
    return this.http.isRequesting;
  }

  getCurrentRequests(): Array<{ url: string; method: string; duration: number }> {
    return Array.from(this.requestDetails.values()).map(details => ({
      ...details,
      duration: Date.now() - details.startTime,
    }));
  }
}

Progress Tracking Component

Build a reactive progress tracker:

export class ProgressTracker {
  private http = resolve(IHttpClient);
  private progressCallbacks = new Set<(progress: RequestProgress) => void>();

  constructor() {
    this.setupProgressTracking();
  }

  private setupProgressTracking() {
    this.http.configure(config => config
      .withDispatcher(document.body)
      .withInterceptor({
        request: (request) => {
          this.notifyProgress();
          return request;
        },

        response: (response) => {
          this.notifyProgress();
          return response;
        },

        responseError: (error) => {
          this.notifyProgress();
          throw error;
        }
      })
    );

    // React to lifecycle events
    document.body.addEventListener(HttpClientEvent.started, () => {
      this.notifyProgress();
    });

    document.body.addEventListener(HttpClientEvent.drained, () => {
      this.notifyProgress();
    });
  }

  private notifyProgress() {
    const progress: RequestProgress = {
      activeCount: this.http.activeRequestCount,
      isRequesting: this.http.isRequesting,
      timestamp: Date.now(),
    };

    this.progressCallbacks.forEach(callback => callback(progress));
  }

  subscribe(callback: (progress: RequestProgress) => void): () => void {
    this.progressCallbacks.add(callback);

    // Return unsubscribe function
    return () => {
      this.progressCallbacks.delete(callback);
    };
  }

  getCurrentProgress(): RequestProgress {
    return {
      activeCount: this.http.activeRequestCount,
      isRequesting: this.http.isRequesting,
      timestamp: Date.now(),
    };
  }
}

interface RequestProgress {
  activeCount: number;
  isRequesting: boolean;
  timestamp: number;
}

Request Queue Visualization

Display active requests in real-time:

export class RequestQueueVisualizer {
  private http = resolve(IHttpClient);
  private queueDisplay: HTMLElement;

  constructor(queueDisplay: HTMLElement) {
    this.queueDisplay = queueDisplay;
    this.setupVisualization();
  }

  private setupVisualization() {
    this.http.configure(config => config
      .withDispatcher(document.body)
      .withInterceptor({
        request: (request) => {
          this.updateDisplay();
          return request;
        },

        response: (response) => {
          this.updateDisplay();
          return response;
        },

        responseError: (error) => {
          this.updateDisplay();
          throw error;
        }
      })
    );
  }

  private updateDisplay() {
    const count = this.http.activeRequestCount;
    const status = this.http.isRequesting ? 'active' : 'idle';

    this.queueDisplay.innerHTML = `
      <div class="request-queue ${status}">
        <div class="status">${status.toUpperCase()}</div>
        <div class="count">
          <span class="number">${count}</span>
          <span class="label">${count === 1 ? 'request' : 'requests'} active</span>
        </div>
        <div class="indicator">
          ${this.createIndicatorDots(count)}
        </div>
      </div>
    `;
  }

  private createIndicatorDots(count: number): string {
    return Array.from({ length: Math.min(count, 10) }, () =>
      '<span class="dot"></span>'
    ).join('');
  }
}

Best Practices for Request Tracking

1. Use Events for UI Updates

Prefer lifecycle events over polling for UI updates:

// Good - Event-driven
document.body.addEventListener(HttpClientEvent.started, () => {
  showLoadingSpinner();
});

// Avoid - Polling
setInterval(() => {
  if (this.http.isRequesting) {
    showLoadingSpinner();
  }
}, 100);

2. Single Event Dispatcher

Configure the dispatcher once during initialization:

export class HttpClientSetup {
  static initialize(http: IHttpClient) {
    http.configure(config => config
      .withDispatcher(document.body)
      .withBaseUrl('/api')
      // ... other configuration
    );
  }
}

3. Cleanup Event Listeners

Always remove event listeners when components are destroyed:

export class RequestMonitorComponent {
  private startedListener: EventListener;
  private drainedListener: EventListener;

  constructor() {
    this.startedListener = () => this.onRequestsStarted();
    this.drainedListener = () => this.onRequestsDrained();

    document.body.addEventListener(HttpClientEvent.started, this.startedListener);
    document.body.addEventListener(HttpClientEvent.drained, this.drainedListener);
  }

  dispose() {
    document.body.removeEventListener(HttpClientEvent.started, this.startedListener);
    document.body.removeEventListener(HttpClientEvent.drained, this.drainedListener);
  }
}

4. Combine with Interceptors

Use interceptors for detailed request tracking:

export class DetailedRequestTracker {
  private http = resolve(IHttpClient);

  constructor() {
    this.http.configure(config => config
      .withDispatcher(document.body)
      .withInterceptor({
        request: (request) => {
          // Track individual request start
          console.log('Request started:', request.url);
          console.log('Total active:', this.http.activeRequestCount);
          return request;
        },

        response: (response, request) => {
          // Track individual request completion
          console.log('Request completed:', request?.url);
          console.log('Remaining active:', this.http.activeRequestCount - 1);
          return response;
        }
      })
    );
  }
}

Summary

Request tracking provides:

  • activeRequestCount: Number of currently active requests

  • isRequesting: Boolean indicating if any requests are active

  • HttpClientEvent.started: Fired when first request starts

  • HttpClientEvent.drained: Fired when all requests complete

  • withDispatcher(node): Configure DOM node for event dispatching

These features enable robust loading indicators, progress tracking, and request coordination in your applications.

Advanced Authentication Patterns

Automatic Token Refresh

Handle token expiration and refresh transparently:

export class TokenRefreshService {
  private http = resolve(IHttpClient);
  private refreshPromise: Promise<string> | null = null;

  constructor() {
    this.setupTokenRefresh();
  }

  private setupTokenRefresh() {
    this.http.configure(config => config.withInterceptor({
      async responseError(error, request, client) {
        if (error instanceof Response && error.status === 401) {
          // Token expired, try to refresh
          try {
            const newToken = await this.refreshAccessToken();
            
            // Retry original request with new token
            const newRequest = new Request(request.url, {
              method: request.method,
              headers: {
                ...Object.fromEntries(request.headers.entries()),
                'Authorization': `Bearer ${newToken}`
              },
              body: request.body
            });
            
            return client.fetch(newRequest);
          } catch (refreshError) {
            // Refresh failed, redirect to login
            this.redirectToLogin();
            throw error;
          }
        }
        
        throw error;
      }
    }));
  }

  private async refreshAccessToken(): Promise<string> {
    // Prevent multiple simultaneous refresh attempts
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = this.performTokenRefresh();
    
    try {
      const token = await this.refreshPromise;
      return token;
    } finally {
      this.refreshPromise = null;
    }
  }

  private async performTokenRefresh(): Promise<string> {
    const refreshToken = this.getRefreshToken();
    if (!refreshToken) {
      throw new Error('No refresh token available');
    }

    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refreshToken })
    });

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    const { accessToken, refreshToken: newRefreshToken } = await response.json();
    
    // Store new tokens
    this.storeTokens(accessToken, newRefreshToken);
    
    return accessToken;
  }

  private getRefreshToken(): string | null {
    const tokens = localStorage.getItem('auth_tokens');
    return tokens ? JSON.parse(tokens).refreshToken : null;
  }

  private storeTokens(accessToken: string, refreshToken: string) {
    const tokens = {
      accessToken,
      refreshToken,
      expiresAt: Date.now() + (55 * 60 * 1000) // 55 minutes
    };
    localStorage.setItem('auth_tokens', JSON.stringify(tokens));
  }

  private redirectToLogin() {
    localStorage.removeItem('auth_tokens');
    window.location.href = '/login';
  }
}

Request Batching and Coordination

Request Deduplication

Prevent duplicate concurrent requests:

export class RequestDeduplicationService {
  private http = resolve(IHttpClient);
  private pendingRequests = new Map<string, Promise<Response>>();

  constructor() {
    this.setupDeduplication();
  }

  private setupDeduplication() {
    this.http.configure(config => config.withInterceptor({
      request: (request) => {
        // Only deduplicate GET requests
        if (request.method !== 'GET') {
          return request;
        }

        const key = this.getRequestKey(request);
        const existingRequest = this.pendingRequests.get(key);

        if (existingRequest) {
          // Return the existing request's promise
          return existingRequest.then(response => response.clone());
        }

        // Store the request promise
        const requestPromise = fetch(request).then(response => {
          // Clean up when request completes
          this.pendingRequests.delete(key);
          return response;
        }).catch(error => {
          // Clean up on error too
          this.pendingRequests.delete(key);
          throw error;
        });

        this.pendingRequests.set(key, requestPromise);
        
        // Return the request to continue normal processing
        return request;
      }
    }));
  }

  private getRequestKey(request: Request): string {
    // Create unique key based on URL and headers
    const headers = Array.from(request.headers.entries()).sort();
    return `${request.method}:${request.url}:${JSON.stringify(headers)}`;
  }
}

Request Queuing

Queue and coordinate multiple requests:

export class RequestQueueService {
  private http = resolve(IHttpClient);
  private requestQueue: Array<() => Promise<any>> = [];
  private isProcessing = false;
  private maxConcurrent = 3;
  private activeRequests = 0;

  async queueRequest<T>(requestFn: () => Promise<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      this.requestQueue.push(async () => {
        try {
          const result = await requestFn();
          resolve(result);
        } catch (error) {
          reject(error);
        }
      });

      this.processQueue();
    });
  }

  private async processQueue() {
    if (this.isProcessing || this.activeRequests >= this.maxConcurrent) {
      return;
    }

    const request = this.requestQueue.shift();
    if (!request) {
      return;
    }

    this.isProcessing = true;
    this.activeRequests++;

    try {
      await request();
    } finally {
      this.activeRequests--;
      this.isProcessing = false;
      
      // Process next request in queue
      if (this.requestQueue.length > 0) {
        setTimeout(() => this.processQueue(), 0);
      }
    }
  }

  // Usage example
  async uploadFiles(files: File[]) {
    const uploadPromises = files.map(file => 
      this.queueRequest(() => 
        this.http.post('/api/upload', { body: this.createFormData(file) })
      )
    );

    return Promise.all(uploadPromises);
  }

  private createFormData(file: File): FormData {
    const formData = new FormData();
    formData.append('file', file);
    return formData;
  }
}

Performance Optimization

Request Timeout Management

Implement sophisticated timeout handling:

export class TimeoutService {
  private http = resolve(IHttpClient);

  constructor() {
    this.setupTimeouts();
  }

  private setupTimeouts() {
    this.http.configure(config => config.withInterceptor({
      request: (request) => {
        // Add timeout based on request type
        const timeout = this.getTimeoutForRequest(request);
        
        if (timeout > 0) {
          const controller = new AbortController();
          const timeoutId = setTimeout(() => controller.abort(), timeout);
          
          // Store cleanup function
          (request as any).__timeoutId = timeoutId;
          
          return new Request(request, { signal: controller.signal });
        }
        
        return request;
      },
      
      response: (response, request) => {
        // Clear timeout on successful response
        const timeoutId = (request as any).__timeoutId;
        if (timeoutId) {
          clearTimeout(timeoutId);
        }
        return response;
      },
      
      responseError: (error, request) => {
        // Clear timeout on error
        const timeoutId = (request as any).__timeoutId;
        if (timeoutId) {
          clearTimeout(timeoutId);
        }
        
        // Convert AbortError to TimeoutError for clarity
        if (error.name === 'AbortError') {
          throw new Error(`Request timeout: ${request?.url}`);
        }
        
        throw error;
      }
    }));
  }

  private getTimeoutForRequest(request: Request): number {
    const url = new URL(request.url);
    
    // Different timeouts for different types of requests
    if (url.pathname.includes('/upload')) {
      return 300_000; // 5 minutes for uploads
    } else if (url.pathname.includes('/reports')) {
      return 120_000; // 2 minutes for reports
    } else if (request.method === 'GET') {
      return 30_000;  // 30 seconds for GET requests
    } else {
      return 60_000;  // 1 minute for other requests
    }
  }
}

Response Compression Handling

Handle compressed responses efficiently:

export class CompressionService {
  private http = resolve(IHttpClient);

  constructor() {
    this.setupCompression();
  }

  private setupCompression() {
    this.http.configure(config => config
      .withDefaults({
        headers: {
          'Accept-Encoding': 'gzip, deflate, br'
        }
      })
      .withInterceptor({
        response: (response) => {
          const encoding = response.headers.get('content-encoding');
          
          if (encoding) {
            console.log(`Response compressed with: ${encoding}`);
            
            // The browser automatically decompresses, but we can log it
            const originalSize = response.headers.get('content-length');
            if (originalSize) {
              console.log(`Compressed size: ${originalSize} bytes`);
            }
          }
          
          return response;
        }
      })
    );
  }
}

Testing and Debugging Support

Request/Response Logging

Comprehensive logging for development and debugging:

export class LoggingService {
  private http = resolve(IHttpClient);
  private isDevelopment = process.env.NODE_ENV === 'development';

  constructor() {
    if (this.isDevelopment) {
      this.setupDetailedLogging();
    } else {
      this.setupProductionLogging();
    }
  }

  private setupDetailedLogging() {
    this.http.configure(config => config.withInterceptor({
      request: (request) => {
        const requestId = this.generateRequestId();
        (request as any).__requestId = requestId;
        
        console.group(`🚀 Request ${requestId}`);
        console.log('Method:', request.method);
        console.log('URL:', request.url);
        console.log('Headers:', Object.fromEntries(request.headers.entries()));
        
        if (request.body) {
          this.logRequestBody(request);
        }
        console.groupEnd();
        
        return request;
      },
      
      response: async (response, request) => {
        const requestId = (request as any).__requestId;
        const responseClone = response.clone();
        
        console.group(`✅ Response ${requestId}`);
        console.log('Status:', response.status, response.statusText);
        console.log('Headers:', Object.fromEntries(response.headers.entries()));
        
        try {
          const body = await responseClone.text();
          if (body) {
            console.log('Body:', this.formatResponseBody(body, response));
          }
        } catch (error) {
          console.log('Body: (could not read)');
        }
        
        console.groupEnd();
        return response;
      },
      
      responseError: (error, request) => {
        const requestId = (request as any).__requestId;
        
        console.group(`❌ Error ${requestId}`);
        console.error('Error:', error);
        console.groupEnd();
        
        throw error;
      }
    }));
  }

  private setupProductionLogging() {
    this.http.configure(config => config.withInterceptor({
      responseError: (error, request) => {
        // Only log errors in production
        console.error('HTTP Error:', {
          url: request?.url,
          method: request?.method,
          error: error.message,
          timestamp: new Date().toISOString()
        });
        
        throw error;
      }
    }));
  }

  private logRequestBody(request: Request) {
    // Note: This is tricky because Request.body is a stream
    // In practice, you might want to log at a higher level
    console.log('Body: (stream - cannot log without consuming)');
  }

  private formatResponseBody(body: string, response: Response): any {
    const contentType = response.headers.get('content-type') || '';
    
    if (contentType.includes('application/json')) {
      try {
        return JSON.parse(body);
      } catch {
        return body;
      }
    }
    
    return body.length > 200 ? `${body.substring(0, 200)}...` : body;
  }

  private generateRequestId(): string {
    return Math.random().toString(36).substring(2, 8);
  }
}

These advanced patterns demonstrate the full power of the Aurelia Fetch Client for enterprise applications. The combination of interceptors, event monitoring, caching, and authentication patterns provides a robust foundation for complex HTTP client requirements.

Last updated

Was this helpful?