@23blocks/transport-http
v3.1.0
Published
HTTP transport layer for 23blocks SDK with fetch-based implementation
Maintainers
Readme
@23blocks/transport-http
HTTP transport implementation for the 23blocks SDK.
Installation
npm install @23blocks/transport-httpOverview
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": "[email protected]", "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: '[email protected]',
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';Related Packages
@23blocks/contracts- Core types and interfaces@23blocks/jsonapi-codec- JSON:API encoder/decoder@23blocks/sdk- Full SDK package
License
MIT - Copyright (c) 2024 23blocks
