JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 161
  • Score
    100M100P100Q79065F
  • License MIT

HTTP transport layer for 23blocks SDK with fetch-based implementation

Package Exports

  • @23blocks/transport-http
  • @23blocks/transport-http/package.json

Readme

@23blocks/transport-http

HTTP transport implementation for the 23blocks SDK.

npm version License: MIT

Installation

npm install @23blocks/transport-http

Overview

This package provides the HTTP transport layer for the 23blocks SDK. It handles:

  • HTTP requests - GET, POST, PUT, PATCH, DELETE operations
  • Error handling - Automatic conversion to BlockErrorException
  • Timeouts - Configurable request timeouts with AbortController
  • Dynamic headers - Static headers or async header providers
  • Query parameters - Automatic serialization including arrays
  • Debug logging - Built-in request/response logging for development
  • Request tracing - Automatic request IDs for debugging and support
  • Automatic retries - Exponential backoff with jitter for transient failures
  • Interceptors - Hook into request/response lifecycle

Usage

Basic Configuration

import { createHttpTransport } from '@23blocks/transport-http';

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
});

// Make requests
const data = await transport.get('/api/users');
const user = await transport.post('/api/users', { name: 'John' });

With Authentication Headers

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  headers: {
    'x-api-key': 'your-api-key',
  },
});

With Dynamic Headers

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  headers: () => {
    const token = localStorage.getItem('access_token');
    return token ? { Authorization: `Bearer ${token}` } : {};
  },
});

With Async Headers

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  headers: async () => {
    const token = await getTokenFromSecureStorage();
    return { Authorization: `Bearer ${token}` };
  },
});

Debug Logging

Enable debug mode to log all requests and responses to the console. This is invaluable for development and troubleshooting.

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  debug: true, // Enable debug logging
});

Console Output

When debug mode is enabled, you'll see output like this:

[23blocks] POST /auth/sign_in [req_m5abc_xyz123]
[23blocks] → Headers: { "content-type": "application/json", "x-api-key": "***" }
[23blocks] → Body: { "email": "user@example.com", "password": "***" }
[23blocks] ← 200 OK (145ms) [req_m5abc_xyz123]
[23blocks] ← Body: { "data": { "type": "user", "id": "123", ... } }

Sensitive data like passwords, tokens, and API keys are automatically masked in logs.

Custom Logger

You can provide a custom logger implementation:

import { createHttpTransport } from '@23blocks/transport-http';
import type { Logger } from '@23blocks/contracts';

const customLogger: Logger = {
  debug: (msg, meta) => myLoggingService.log('debug', msg, meta),
  info: (msg, meta) => myLoggingService.log('info', msg, meta),
  warn: (msg, meta) => myLoggingService.log('warn', msg, meta),
  error: (msg, meta) => myLoggingService.log('error', msg, meta),
};

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  logger: customLogger,
  debug: true,
});

Request Tracing

Every request automatically includes a unique X-Request-ID header. This ID is:

  • Included in all error responses for easy debugging
  • Passed to your backend for end-to-end tracing
  • Logged in debug mode for correlation
try {
  await transport.get('/api/users/123');
} catch (error) {
  if (isBlockErrorException(error)) {
    console.log('Request ID:', error.requestId); // "req_m5abc_xyz123"
    console.log('Duration:', error.duration);     // 145 (ms)

    // Send to support: "Please check request req_m5abc_xyz123"
  }
}

Custom Request ID Generator

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  generateRequestId: () => `myapp-${Date.now()}-${Math.random().toString(36).slice(2)}`,
});

Automatic Retries

Configure automatic retries with exponential backoff for transient failures:

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  retry: {
    maxRetries: 3,                        // Retry up to 3 times
    initialDelay: 1000,                   // Start with 1 second delay
    maxDelay: 10000,                      // Cap at 10 seconds
    backoffMultiplier: 2,                 // Double delay each retry
    retryableStatuses: [429, 502, 503, 504], // Which status codes to retry
  },
});

Retry Behavior

  • Retries only trigger for configured status codes (default: 429, 502, 503, 504)
  • Uses exponential backoff with jitter to prevent thundering herd
  • Network errors are also retried
  • Debug mode logs each retry attempt
[23blocks] POST /api/data [req_m5abc_xyz123]
[23blocks] ✗ 503 Service Unavailable (89ms) [req_m5abc_xyz123]
[23blocks] Retrying in 1250ms (attempt 1/3) [req_m5abc_xyz123]
[23blocks] ✗ 503 Service Unavailable (92ms) [req_m5abc_xyz123]
[23blocks] Retrying in 2480ms (attempt 2/3) [req_m5abc_xyz123]
[23blocks] ← 200 OK (134ms) [req_m5abc_xyz123]

Interceptors

Hook into the request/response lifecycle for cross-cutting concerns:

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  interceptors: {
    // Called before each request
    onRequest: async ({ method, path, headers, requestId }) => {
      console.log(`Starting ${method} ${path}`);
      // You could add analytics tracking here
    },

    // Called after each successful response
    onResponse: async (response, { method, path, duration, requestId }) => {
      trackMetric('api_latency', duration, { path });
      return response; // Must return response (can transform it)
    },

    // Called when an error occurs
    onError: async (error, { method, path, duration, requestId }) => {
      // Report to error tracking service
      Sentry.captureException(error, {
        extra: { requestId, path, duration },
      });
      throw error; // Must re-throw or handle
    },
  },
});

Common Interceptor Patterns

Token Refresh

interceptors: {
  onError: async (error, context) => {
    if (error instanceof BlockErrorException && error.code === 'token_expired') {
      await refreshAuthToken();
      // Retry logic would go here
    }
    throw error;
  },
}

Performance Monitoring

interceptors: {
  onResponse: async (response, { path, duration }) => {
    if (duration > 1000) {
      console.warn(`Slow request: ${path} took ${duration}ms`);
    }
    return response;
  },
}

Timeout Configuration

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  timeout: 60000, // 60 seconds (default is 30 seconds)
});

Credentials (Cookies)

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
  credentials: 'include', // Include cookies in cross-origin requests
});

Per-Request Options

const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',
});

// With query parameters
const users = await transport.get('/api/users', {
  params: {
    limit: 20,
    offset: 0,
    status: 'active',
    roles: ['admin', 'user'], // Arrays become ?roles[]=admin&roles[]=user
  },
});

// With custom headers for specific request
const data = await transport.get('/api/sensitive', {
  headers: {
    'X-Custom-Header': 'value',
  },
});

// With custom timeout
const data = await transport.post('/api/long-operation', payload, {
  timeout: 120000, // 2 minutes
});

// With abort signal
const controller = new AbortController();
const data = await transport.get('/api/data', {
  signal: controller.signal,
});

// Cancel the request
controller.abort();

API Reference

createHttpTransport(config)

Creates a new HTTP transport instance.

function createHttpTransport(config: TransportConfig): Transport;

TransportConfig

Property Type Default Description
baseUrl string required Base URL for all requests
headers Record<string, string> | HeadersProvider {} Static or dynamic headers
timeout number 30000 Default timeout in milliseconds
credentials RequestCredentials undefined Fetch credentials option
debug boolean false Enable debug logging
logger Logger consoleLogger Custom logger implementation
generateRequestId () => string built-in Custom request ID generator
retry RetryConfig undefined Retry configuration
interceptors Interceptors undefined Request/response interceptors

RetryConfig

Property Type Default Description
maxRetries number required Maximum number of retry attempts
initialDelay number required Initial delay in milliseconds
maxDelay number required Maximum delay in milliseconds
backoffMultiplier number required Multiplier for exponential backoff
retryableStatuses number[] [429, 502, 503, 504] HTTP status codes to retry

Interceptors

Property Type Description
onRequest (config) => void | Promise<void> Called before each request
onResponse <T>(response, context) => T | Promise<T> Called after successful response
onError (error, context) => never Called on error

Transport Interface

interface Transport {
  get<T>(path: string, options?: RequestOptions): Promise<T>;
  post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
  put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
  patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>;
  delete<T>(path: string, options?: RequestOptions): Promise<T>;
}

RequestOptions

Property Type Description
params Record<string, string | number | boolean | string[]> Query parameters
headers Record<string, string> Additional headers (merged with config headers)
timeout number Override default timeout
signal AbortSignal Abort signal for cancellation

Error Handling

The transport automatically converts HTTP errors to BlockErrorException with request context:

import { BlockErrorException, ErrorCodes, isBlockErrorException } from '@23blocks/contracts';

try {
  await transport.get('/api/users/123');
} catch (error) {
  if (isBlockErrorException(error)) {
    // Error details
    console.log('Code:', error.code);           // "not_found"
    console.log('Message:', error.message);     // "User not found"
    console.log('Status:', error.status);       // 404

    // Request context (NEW)
    console.log('Request ID:', error.requestId); // "req_m5abc_xyz123"
    console.log('Duration:', error.duration);    // 145 (ms)

    switch (error.code) {
      case ErrorCodes.UNAUTHORIZED: // 401
        // Redirect to login
        break;
      case ErrorCodes.FORBIDDEN: // 403
        // Show access denied
        break;
      case ErrorCodes.NOT_FOUND: // 404
        // Show not found
        break;
      case ErrorCodes.VALIDATION_ERROR: // 422
        // Show validation errors
        console.log(error.meta?.errors);
        break;
      case ErrorCodes.TIMEOUT:
        // Request timed out
        break;
      case ErrorCodes.NETWORK_ERROR:
        // Network error (offline, DNS, etc.)
        break;
    }
  }
}

JSON:API Error Support

The transport understands JSON:API error format and extracts detailed error information:

// Server returns:
// { "errors": [{ "code": "invalid_email", "detail": "Email is invalid", "source": { "pointer": "/data/attributes/email" } }] }

try {
  await transport.post('/api/users', { email: 'invalid' });
} catch (error) {
  if (isBlockErrorException(error)) {
    console.log(error.message);         // "Email is invalid"
    console.log(error.source);          // "/data/attributes/email"
    console.log(error.requestId);       // "req_m5abc_xyz123"
    console.log(error.meta?.errors);    // Full errors array
  }
}

Complete Example

import { createHttpTransport } from '@23blocks/transport-http';
import { isBlockErrorException } from '@23blocks/contracts';

// Production-ready configuration
const transport = createHttpTransport({
  baseUrl: 'https://api.yourapp.com',

  // Dynamic auth headers
  headers: async () => {
    const token = await getAuthToken();
    return {
      'Authorization': `Bearer ${token}`,
      'x-api-key': process.env.API_KEY,
    };
  },

  // Debug in development only
  debug: process.env.NODE_ENV === 'development',

  // Retry transient failures
  retry: {
    maxRetries: 3,
    initialDelay: 1000,
    maxDelay: 10000,
    backoffMultiplier: 2,
  },

  // Error tracking
  interceptors: {
    onError: async (error, { requestId, path, duration }) => {
      errorTracker.capture(error, { requestId, path, duration });
      throw error;
    },
  },
});

// Use it
try {
  const user = await transport.post('/auth/sign_in', {
    email: 'user@example.com',
    password: 'password',
  });
  console.log('Signed in:', user);
} catch (error) {
  if (isBlockErrorException(error)) {
    console.error(`Request ${error.requestId} failed: ${error.message}`);
  }
}

TypeScript Support

This package is written in TypeScript and exports all types:

import {
  createHttpTransport,
  type Transport,
  type TransportConfig,
  type RequestOptions,
} from '@23blocks/transport-http';

License

MIT - Copyright (c) 2024 23blocks