Skip to content

Stage 5: HTTP Client Integration

mandla-enkosi edited this page Apr 5, 2025 · 1 revision

1. Overview

  • Objective: Develop the main httpClient (as an Axios wrapper) and integrate core functionalities including:
    • Axios interceptors for authentication (token attachment, refresh trigger based on 401/403) and error handling.
    • A request queue to hold requests during token refresh.
    • Standardized error handling (defining and using StandardError).
    • A configurable retry strategy for transient network/server errors.
    • Request cancellation support (via AbortController).
    • Caching and request deduplication features.
    • Ensuring behavior correctly adapts based on the configured authType.
  • Expected Result: A robust, exported httpClient instance that:
    • Abstracts authentication complexities (token attachment, refresh).
    • Handles common network/server errors gracefully through retries.
    • Provides a standardized error format (StandardError).
    • Supports request cancellation.
    • Offers performance enhancements (caching, deduplication).

2. Dependency Validation

  • Pre-Stage Requirements:
    • Authentication Manager providing getToken, _triggerRefresh, auth state.
    • Platform Abstraction Layer providing INetworkInfo.
    • Internal State Management providing store setters.
  • External Dependencies: Axios.
  • Environment Configurations:
    • Axios defaults (baseURL, timeout).
    • Retry config (count, backoff).
    • Cache/dedupe config.

3. Diagrams

  • Visual Aids:

    HTTP Request Flow with Token Refresh

    sequenceDiagram
        participant App as Application
        participant HTTPClient as IHttpClient
        participant AxiosInst as Axios Instance
        participant Interceptors as Request/Response Interceptors
        participant AuthMgr as IAuthenticationManager
        participant ReqQueue as RequestQueue
        participant RetryHandler as RetryHandler
        participant ErrorHandler as ErrorHandler
        participant Backend as Backend API
        participant StateStore as InternalStateStore
    
        App->>HTTPClient: request(config)
        HTTPClient->>Interceptors: Request Interceptor(config)
        alt Requires Auth (Default) & AuthType is JWT/OAuth2
            Interceptors->>AuthMgr: getToken()
            AuthMgr-->>Interceptors: token (or null/expired)
            Interceptors->>Interceptors: Add 'Authorization: Bearer token' header
        else AuthType is Session
            Interceptors->>Interceptors: Ensure 'withCredentials: true'
        end
        Interceptors-->>AxiosInst: Modified Config
        AxiosInst->>Backend: HTTP Request
        Backend-->>AxiosInst: HTTP Response
        AxiosInst-->>Interceptors: Response Interceptor(response/error)
    
        alt Success Response (2xx)
            Interceptors-->>HTTPClient: Processed Response
            HTTPClient-->>App: Promise Resolved (Data)
        else Error Response
            alt Auth Error (401/403) & AuthType is JWT/OAuth2 & Not Skip Refresh
                Interceptors->>ReqQueue: enqueue(originalRequest)
                Interceptors->>AuthMgr: _triggerRefresh()
                AuthMgr-->>Interceptors: refreshPromise (resolves/rejects later)
                Interceptors-->>HTTPClient: Returns refreshPromise linked to queued request
                Note over HTTPClient, App: App awaits...
                alt Refresh Succeeds
                    AuthMgr->>ReqQueue: processQueue()
                    ReqQueue->>Interceptors: Re-execute original request via interceptors
                    Note over Interceptors: Gets new token, retries...
                else Refresh Fails
                    AuthMgr->>ReqQueue: clearQueue(refreshError)
                    ReqQueue-->>Interceptors: Reject queued request promises
                    Interceptors->>ErrorHandler: handleAxiosError(original 401 error, refreshError context)
                    ErrorHandler-->>Interceptors: StandardError (Auth Refresh Failed)
                    Interceptors-->>HTTPClient: Error
                    HTTPClient-->>App: Promise Rejected (StandardError)
                end
            else Transient Error (Network, 5xx) & Retryable
                Interceptors->>RetryHandler: shouldRetry(error, config)
                RetryHandler-->>Interceptors: true (with delay)
                Interceptors->>Interceptors: Schedule Retry via setTimeout(axiosInst.request(...))
            else Non-Retryable Error / Retry Limit Reached
                Interceptors->>ErrorHandler: handleAxiosError(error) / handleNetworkError(error)
                ErrorHandler-->>Interceptors: StandardError
                Interceptors-->>HTTPClient: Error
                HTTPClient-->>App: Promise Rejected (StandardError)
            end
        end
    
    Loading

4. Touched Parts

  • Modules/Files:
    • /packages/core/src/http/index.ts: Export httpClient.
    • /packages/core/src/http/httpClient.ts: Create/configure Axios instance, attach interceptors, implement IHttpClient wrapper methods, handle configure updates.
    • /packages/core/src/http/interceptors.ts: Implement detailed request interceptor (auth header, withCredentials, cancellation) and response interceptor (error detection: 401/403, transient; triggering refresh/retry; queue interaction; error handling).
    • /packages/core/src/http/requestQueue.ts: Implement queue logic (add, process, clear, timeout).
    • /packages/core/src/http/errorHandler.ts: Define StandardError, implement error creation functions (handleAxiosError, handleNetworkError), define standard error codes enum/type. Retrieves and calls the global onError hook.
    • /packages/core/src/http/retryHandler.ts: Implement retry decision logic, backoff calculation (using /polling/backoffStrategies), track retry attempts.
    • /packages/core/src/http/caching.ts: Implement cache store and interceptor logic.
    • /packages/core/src/http/deduplication.ts: Implement in-flight request tracking and interceptor logic.
    • /packages/core/src/http/types.ts: Define HttpClientRequestConfig, StandardErrorCode enum/type.
    • /packages/core/src/auth/index.ts: Consume IAuthenticationManager interface.
    • /packages/core/src/auth/authManager.ts: Consumed by interceptors (getToken, _triggerRefresh).
    • /packages/core/src/state/store.ts: Updated with OperationState (pending requests, refresh status).
    • /packages/core/src/state/hooks.ts: Implement useOperationState.
    • /packages/core/src/platform/index.ts: Consumed to get INetworkInfo.
  • Functionalities:
    • Axios instance setup and configuration.
    • Request Interception: Attach token or set withCredentials based on authType. Handle cancellation.
    • Response Interception: Handle 401/403 by triggering refresh and queuing. Handle transient errors by triggering retries. Normalize errors.
    • Request Queuing during refresh.
    • Configurable Retry Strategy for transient errors.
    • Standardized Error Handling.
    • Optional Caching & Deduplication.
    • Update internal operational state.
    • Provide useOperationState hook.
    • Error Handler:
      • Creates StandardError from Axios errors, network errors, etc.
      • Provides classification (retryable, auth error).
      • Invokes the global onError callback.

5. API Contracts & Interface Definitions

// --- HTTP Types (/packages/core/src/http/types.ts) ---

/** Standardized Error Code values. */
export type StandardErrorCode =
  // Authentication Errors
  | 'AUTH_UNAUTHORIZED'         // General 401 from backend API
  | 'AUTH_FORBIDDEN'            // General 403 from backend API
  | 'AUTH_TOKEN_EXPIRED'        // Specifically detected expired token
  | 'AUTH_REFRESH_FAILED'       // Token refresh attempt failed
  | 'AUTH_LOGIN_REQUIRED'       // Action requires authentication
  | 'AUTH_CONFIG_ERROR'         // Invalid auth configuration
  | 'AUTH_REQUEST_FAILED'       // Generic auth request failure
  | 'AUTH_SERVICE_UNAVAILABLE'  // Auth server unavailable
  | 'AUTH_STATE_MISMATCH'       // OAuth state validation failed
  | 'AUTH_PKCE_ERROR'           // PKCE verification failed
  | 'AUTH_REDIRECT_ERROR'       // OAuth redirect handling failed
  
  // Network Errors
  | 'NETWORK_ERROR'             // Generic connection issue
  | 'NETWORK_TIMEOUT'           // Request timed out
  | 'NETWORK_OFFLINE'           // Client is offline
  
  // HTTP Server Errors
  | 'HTTP_BAD_REQUEST'          // 400
  | 'HTTP_NOT_FOUND'            // 404
  | 'HTTP_METHOD_NOT_ALLOWED'   // 405
  | 'HTTP_CONFLICT'             // 409
  | 'HTTP_UNPROCESSABLE_ENTITY' // 422 (Validation errors)
  | 'HTTP_TOO_MANY_REQUESTS'    // 429
  | 'HTTP_INTERNAL_SERVER_ERROR' // 500
  | 'HTTP_SERVICE_UNAVAILABLE'  // 503
  | 'HTTP_GATEWAY_TIMEOUT'      // 504
  | 'HTTP_UNKNOWN_ERROR'        // Other non-2xx status
  
  // Request/Client Errors
  | 'REQUEST_CANCELLED'         // Cancelled via AbortController
  | 'REQUEST_QUEUE_TIMEOUT'     // Timed out in refresh queue
  | 'RETRY_LIMIT_EXCEEDED'      // Max retries reached
  | 'CACHE_ERROR'               // Cache-related error
  
  // Platform Errors
  | 'PLATFORM_STORAGE_ERROR'    // Error in IStorage operations
  | 'PLATFORM_NAVIGATION_ERROR' // Error in INavigation operations
  | 'PLATFORM_NETWORK_ERROR'    // Error in INetworkInfo operations
  
  // Polling Errors
  | 'POLLING_MAX_ATTEMPTS'      // Polling max attempts reached
  | 'POLLING_TIMEOUT'           // Polling overall timeout reached
  | 'POLLING_CANCELLED'         // Polling was cancelled
  
  // General
  | 'UNKNOWN_ERROR';            // Fallback for unclassified errors

/** Standardized Error structure used throughout the kit. */
export interface StandardError extends Error { // Extend Error for stack trace etc.
  code: StandardErrorCode;          // Machine-readable code
  message: string;                  // Human-readable message (can be generic)
  messageKey?: string;              // Optional I18n key
  originalError?: any;              // The original caught error (AxiosError, Error, etc.)
  metadata?: {                      // Contextual information
    statusCode?: number;
    url?: string;
    method?: string;
    config?: HttpClientRequestConfig; // Request config that failed
    // Other relevant info (e.g., from platform layer)
  };
  severity: 'info' | 'warning' | 'error' | 'critical'; // Severity level
  retryable: boolean;               // Can this operation be retried?
  name: 'StandardError';            // For type identification
}

/** Extends AxiosRequestConfig with kit-specific options. */
export interface HttpClientRequestConfig extends AxiosRequestConfig {
  /**
   * Does this request require authentication? If true, attempts to attach token.
   * If false, skips token attachment. If authType is 'session', ignored (browser handles cookies).
   * @default true
   */
  requireAuth?: boolean;
  /**
   * If true, skip the automatic token refresh mechanism if this request fails with 401/403.
   * The error will be returned directly. Useful for login/refresh endpoints themselves.
   * @default false
   */
  skipRefresh?: boolean;

  // Caching control (Optional Feature)
  /** Force use of cache if available and valid, bypass network. */
  // useCache?: boolean;
  /** Override global cache TTL (in ms) for this request. */
  // cacheTTL?: number;
  /** Bypass cache read and force network request; update cache on success. */
  // forceRefresh?: boolean;

  // Queue control
  /** Priority for processing if request gets queued during token refresh. Lower number = higher priority. */
  queuePriority?: number;

  // Deduplication control (Optional Feature)
  /** If true (and feature enabled), check for identical in-flight GET requests. */
  // deduplicate?: boolean;

  // Retry control
  /** Override global max retry count for transient errors for this request. */
  maxRetries?: number;
  /** Provide specific backoff options for retries of this request. */
  // retryBackoffOptions?: BackoffOptions; // Reuse from polling types

  /** Internal usage for tracking retries. */
  _retryCount?: number;
  /** Internal usage for identifying queued requests. */
  _queuedId?: string;
}

// --- HTTP Client Interface (/packages/core/src/http/index.ts) ---

import { AxiosInstance, AxiosResponse } from 'axios';

export interface IHttpClient {
  /** Makes an HTTP request using the configured Axios instance and interceptors. */
  request<T = any, R = AxiosResponse<T>, D = any>(config: HttpClientRequestConfig): Promise<R>;
  get<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: HttpClientRequestConfig): Promise<R>;
  delete<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: HttpClientRequestConfig): Promise<R>;
  head<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: HttpClientRequestConfig): Promise<R>;
  options<T = any, R = AxiosResponse<T>, D = any>(url: string, config?: HttpClientRequestConfig): Promise<R>;
  post<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: HttpClientRequestConfig): Promise<R>;
  put<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: HttpClientRequestConfig): Promise<R>;
  patch<T = any, R = AxiosResponse<T>, D = any>(url: string, data?: D, config?: HttpClientRequestConfig): Promise<R>;

  // Configuration Methods
  setBaseURL(url: string): void;
  setTimeout(timeout: number): void;
  setDefaultHeaders(headers: Record<string, string>): void;

  // Cache Management (Optional Feature)
  /** Clears cached responses. Accepts optional matcher for URL targeting. */
  // clearCache(urlMatcher?: string | RegExp): void;

  /** Returns the underlying Axios instance for advanced use cases (use cautiously). */
  getAxiosInstance(): AxiosInstance;
}

// --- Internal Interfaces ---

/** Internal handler for deciding if/when to retry requests. */
interface IRetryHandler {
  /** Determines if an error warrants a retry based on config and error type. */
  shouldRetry(error: StandardError, config: HttpClientRequestConfig): boolean;
  /** Calculates the delay before the next retry attempt. */
  getRetryDelay(attempt: number, config: HttpClientRequestConfig): number;
}

/** Internal handler for normalizing errors into StandardError. */
interface IErrorHandler {
  /** Converts an AxiosError into a StandardError, potentially calling global error hook. */
  handleAxiosError(error: AxiosError, config: HttpClientRequestConfig): StandardError;
  /** Converts generic errors (network, etc.) into a StandardError, potentially calling global error hook. */
  handleGenericError(error: any, config?: HttpClientRequestConfig): StandardError;
  /** Internal helper to create and potentially report a StandardError. */
  // createAndReportError(options: ...): StandardError; // Maybe refactor reporting logic here
}

/** Internal queue for requests pending token refresh. */
interface IRequestQueue {
  /** Adds a request function (that returns a promise) to the queue. Returns a promise that resolves/rejects when the request eventually executes. */
  enqueue<T>(requestExecutor: () => Promise<T>, config: HttpClientRequestConfig): Promise<T>;
  /** Executes all queued requests. Should be called on successful refresh. */
  processQueue(): Promise<void[]>; // Returns array of promises for queued requests
  /** Rejects all queued requests with the provided error. Should be called on failed refresh. */
  clearQueue(reason: StandardError): void;
  /** Returns the number of requests currently in the queue. */
  getQueueLength(): number;
}

6. Tests

  • Unit Tests:
    • Test interceptors thoroughly (auth logic per authType, 401/403 trigger, retry trigger, error mapping).
    • Test request queue.
    • Test error handler mapping.
    • Test retry handler backoff/decision logic.
    • Test cancellation propagation.
    • Test optional cache/dedupe.
    • Verify that the global onError hook (if provided during setup) is called exactly once with the created StandardError object.
    • Test cases where the global hook might throw an error itself (should be caught gracefully).
  • Integration Tests:
    • Test full request lifecycle including successful auth.
    • Test full refresh cycle (401 -> refresh -> queue -> retry).
    • Test retry cycle (5xx -> retry).
    • Test cancellation of in-flight/queued requests.
    • Test interaction with mocked AuthenticationManager and INetworkInfo.
    • Test state updates via useOperationState.
  • Edge Cases: Concurrent 401s, network errors during refresh, queue limits, non-standard errors, cancellation races.

7. Integration Checkpoints

  • AuthManager Interaction:
    • Verify the Request Interceptor correctly calls AuthenticationManager.getToken() based on requireAuth configuration and authType.
    • Verify the Response Interceptor correctly calls AuthenticationManager._triggerRefresh() (or equivalent public method) upon detecting relevant 401/403 errors for token-based authTypes.
    • Confirm the AuthenticationManager's concurrency lock handles simultaneous _triggerRefresh calls originating from multiple failed requests within the httpClient.
  • Request Handling & Flow:
    • Verify the Request Interceptor correctly attaches the Authorization header (for JWT/OAuth2) or ensures withCredentials: true is set (for Session) based on authType.
    • Verify the RequestQueue correctly enqueues requests when triggered by the interceptor during refresh and processes/clears them based on the refresh outcome notified by AuthenticationManager.
    • Verify the RetryHandler correctly identifies retryable StandardErrors (transient network/server errors) and applies configured backoff delays before rescheduling the request attempt via the interceptor/Axios.
    • Verify request cancellation using AbortController signals works for both in-flight and queued requests.
  • Error Handling & State:
    • Verify the ErrorHandler consistently creates StandardError objects from various Axios and network errors.
    • Confirm that StandardError objects are propagated correctly via Promise rejections and are received by the optional global onError hook configured in AuthenticationManager.
    • Verify the OperationState slice (pendingRequests, isRefreshingToken) in the internal store is accurately updated by the interceptors/client logic.
    • Confirm that state changes are correctly reflected when using the useOperationState hook.
  • Platform Interaction:
    • Verify the httpClient (likely via interceptors) uses INetworkInfo from the Platform Abstraction Layer to check network status before making requests, potentially failing fast if offline.

8. Documentation Deliverables

  • Internal: Comments detailing interceptor logic, queue state management, retry decision tree, error mapping.
  • External:
    • Usage Guide: How to import and use httpClient (get, post, etc.). Basic configuration (baseURL).
    • Authentication: How httpClient interacts with auth (requireAuth, automatic refresh).
    • Error Handling: Explaining StandardError, common codes, how to catch errors.
    • Configuration: Detailing HttpClientRequestConfig options (retries, cache, etc.).
    • Cancellation: How to use AbortController.
    • Session Auth: Note on withCredentials.
  • Changelog: Document httpClient API, configuration options, error codes, retry behavior.

9. Considerations & Notes

  • Interceptor performance overhead should be minimal.
  • Robustness of the refresh-trigger and queue interaction is critical.
  • Retry strategy should be predictable and configurable.
  • Error codes list (StandardErrorCode) should be comprehensive and documented.
  • Features (cache/dedupe) should default off and be simple to configure/understand.
  • httpClient is exported for application use, distinct from the auth module's internal requester.
  • Ensure the global onError hook is called reliably from the central error handler without causing infinite loops if the hook itself throws an error.

10. Platform-Specific Implementations

  • Core Logic:
    • Platform-agnostic using Axios.
    • Web:
      • Use native AbortController.
      • Error handling might consider CORS specifically.
    • React Native:
      • Consider mobile-specific transient error codes for retry logic.
      • Integrate INetworkInfo for offline checks.
      • Cancellation needs testing.
  • Configuration: May set different default timeout values for Web vs RN builds (web default timeouts typically shorter than mobile).
  • Dependencies: Relies on platform-provided INetworkInfo.