HTTP Adapter
The HTTP adapter provides remote content storage and retrieval through HTTP/HTTPS protocols for the ReX content system. It enables integration with remote content APIs, headless CMS systems, and other HTTP-based content services.
Overview
The HTTP adapter enables the content system to interact with remote content endpoints, providing a bridge between local applications and remote content repositories. It supports RESTful operations, authentication, caching, and resilient network handling.
import { createHttpAdapter } from '@lib/content/adapters/common/http'
const adapter = createHttpAdapter({
baseUrl: 'https://api.example.com/content',
headers: {
Accept: 'application/json',
},
authentication: {
type: 'bearer',
token: 'your-access-token',
},
})
API Reference
Creation
function createHttpAdapter(options?: HttpAdapterOptions): ContentAdapter
Creates a new HTTP adapter instance.
Options
interface HttpAdapterOptions extends ContentAdapterOptions {
/**
* Base URL for HTTP requests (required)
*/
baseUrl?: string
/**
* Default HTTP headers to include with every request
*/
headers?: Record<string, string>
/**
* Default fetch options (RequestInit)
*/
fetchOptions?: RequestInit
/**
* Retry configuration
*/
retryOptions?: {
/**
* Maximum number of retry attempts (default: 3)
*/
maxRetries?: number
/**
* Delay between retries in ms (default: 1000)
*/
retryDelay?: number
/**
* HTTP status codes that trigger a retry (default: [408, 429, 500, 502, 503, 504])
*/
retryStatusCodes?: number[]
/**
* Use exponential backoff for retries (default: true)
*/
useExponentialBackoff?: boolean
}
/**
* Caching configuration
*/
cacheOptions?: {
/**
* Enable client-side caching (default: true)
*/
enabled?: boolean
/**
* Cache TTL in milliseconds (default: 60000)
*/
maxAge?: number
/**
* Maximum cache items (default: 100)
*/
maxItems?: number
/**
* Storage mechanism (default: 'memory')
*/
storage?: 'memory' | 'localStorage' | 'sessionStorage'
/**
* Cache key generator function
*/
keyGenerator?: (uri: string, options?: any) => string
}
/**
* Authentication configuration
*/
authentication?: {
/**
* Authentication type
*/
type: 'none' | 'basic' | 'bearer' | 'custom'
/**
* Basic auth credentials
*/
credentials?: {
username: string
password: string
}
/**
* Bearer token
*/
token?: string
/**
* Function to generate authentication headers
*/
headerFactory?: () => Promise<Record<string, string>>
/**
* Refresh token functionality
*/
refreshToken?: {
/**
* Token refresh URL
*/
url: string
/**
* Refresh token value
*/
token: string
/**
* Function to process refresh response
*/
handleResponse: (response: any) => {
token: string
refreshToken?: string
}
}
}
/**
* URL mapping for HTTP endpoints
*/
urlMapping?: {
/**
* Read operation URL template (default: '{baseUrl}/{uri}')
*/
read?: string
/**
* Write operation URL template (default: '{baseUrl}/{uri}')
*/
write?: string
/**
* Delete operation URL template (default: '{baseUrl}/{uri}')
*/
delete?: string
/**
* List operation URL template (default: '{baseUrl}?pattern={pattern}')
*/
list?: string
/**
* Exists operation URL template (default: '{baseUrl}/{uri}')
*/
exists?: string
}
}
Methods
read
async read(uri: string, options?: ReadOptions): Promise<Content>;
Reads content from the remote endpoint.
Parameters:
uri
: Content URIoptions
: Optional read configuration
Returns:
- A Promise resolving to a Content object
Example:
const content = await adapter.read('articles/welcome.md')
console.log(content.data) // Content data
console.log(content.metadata) // Associated metadata
Implementation Details:
- Constructs request URL from baseUrl and URI
- Adds authentication headers if configured
- Makes HTTP GET request to the endpoint
- Parses response based on content type
- Caches response if caching is enabled
- Handles network errors and HTTP status codes
- Implements retry logic for transient failures
write
async write(uri: string, content: Content, options?: WriteOptions): Promise<void>;
Writes content to the remote endpoint.
Parameters:
uri
: Content URIcontent
: Content object to writeoptions
: Optional write configuration
Example:
await adapter.write('articles/welcome.md', {
data: '# Welcome\n\nThis is a welcome article.',
contentType: 'text/markdown',
metadata: {
title: 'Welcome',
createdAt: new Date(),
},
})
Implementation Details:
- Constructs request URL from baseUrl and URI
- Adds authentication headers if configured
- Serializes content based on endpoint requirements
- Makes HTTP PUT or POST request to the endpoint
- Handles network errors and HTTP status codes
- Implements retry logic for transient failures
- Invalidates cache entries if caching is enabled
delete
async delete(uri: string, options?: DeleteOptions): Promise<void>;
Deletes content from the remote endpoint.
Parameters:
uri
: Content URIoptions
: Optional delete configuration
Example:
await adapter.delete('articles/outdated-article.md')
Implementation Details:
- Constructs request URL from baseUrl and URI
- Adds authentication headers if configured
- Makes HTTP DELETE request to the endpoint
- Handles network errors and HTTP status codes
- Implements retry logic for transient failures
- Invalidates cache entries if caching is enabled
list
async list(pattern?: string, options?: ListOptions): Promise<string[]>;
Lists content URIs matching the pattern from the remote endpoint.
Parameters:
pattern
: Optional glob pattern for content URIsoptions
: Optional list configuration
Returns:
- A Promise resolving to an array of content URIs
Example:
const uris = await adapter.list('articles/**/*.md')
console.log(uris) // ['articles/welcome.md', 'articles/guide.md', ...]
Implementation Details:
- Constructs request URL with pattern parameter
- Adds authentication headers if configured
- Makes HTTP GET request to the list endpoint
- Parses response to extract URI list
- Handles network errors and HTTP status codes
- Implements retry logic for transient failures
- Optionally caches results based on configuration
exists
async exists(uri: string, options?: ExistsOptions): Promise<boolean>;
Checks if content exists at the remote endpoint.
Parameters:
uri
: Content URIoptions
: Optional exists configuration
Returns:
- A Promise resolving to a boolean indicating existence
Example:
const exists = await adapter.exists('articles/welcome.md')
if (exists) {
console.log('Article exists on the remote server')
}
Implementation Details:
- Constructs request URL from baseUrl and URI
- Makes HTTP HEAD request to check existence
- Returns true for successful responses (2xx)
- Returns false for 404 Not Found responses
- Throws errors for other HTTP status codes
- Implements retry logic for transient failures
dispose
async dispose(): Promise<void>;
Cleans up resources used by the adapter.
Example:
await adapter.dispose()
Implementation Details:
- Cancels any pending requests
- Clears internal caches
- Revokes any active authentication tokens
- Releases other resources
URL Templating
The HTTP adapter uses URL templates to construct endpoint URLs:
function resolveUrl(template: string, params: Record<string, string>): string {
return template.replace(/\{(\w+)\}/g, (_, key) => {
return encodeURIComponent(params[key] || '')
})
}
// Example
const readUrl = resolveUrl(options.urlMapping.read, {
baseUrl: options.baseUrl,
uri: uri,
})
Authentication
The adapter supports multiple authentication methods:
Basic Authentication
function applyBasicAuth(headers: Headers): void {
const { username, password } = options.authentication.credentials
const authValue = btoa(`${username}:${password}`)
headers.set('Authorization', `Basic ${authValue}`)
}
Bearer Authentication
function applyBearerAuth(headers: Headers): void {
const { token } = options.authentication
headers.set('Authorization', `Bearer ${token}`)
}
Custom Authentication
async function applyCustomAuth(headers: Headers): Promise<void> {
const { headerFactory } = options.authentication
const authHeaders = await headerFactory()
for (const [key, value] of Object.entries(authHeaders)) {
headers.set(key, value)
}
}
Token Refresh
async function refreshAuthToken(): Promise<void> {
const { url, token, handleResponse } = options.authentication.refreshToken
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ refresh_token: token }),
})
if (!response.ok) {
throw new ContentAccessError(
`Failed to refresh authentication token: ${response.statusText}`
)
}
const data = await response.json()
const tokens = handleResponse(data)
// Update tokens
options.authentication.token = tokens.token
if (tokens.refreshToken) {
options.authentication.refreshToken.token = tokens.refreshToken
}
}
Caching System
The HTTP adapter implements client-side caching for read operations:
class HttpCache {
private cache: Map<string, { content: Content; expires: number }>
private maxItems: number
constructor(options: { maxItems?: number; maxAge?: number }) {
this.cache = new Map()
this.maxItems = options.maxItems || 100
this.maxAge = options.maxAge || 60000
}
get(key: string): Content | undefined {
const cached = this.cache.get(key)
if (!cached) {
return undefined
}
// Check if expired
if (cached.expires < Date.now()) {
this.cache.delete(key)
return undefined
}
return cached.content
}
set(key: string, content: Content): void {
// Enforce capacity limits
if (this.cache.size >= this.maxItems) {
// Remove oldest item
const oldestKey = this.cache.keys().next().value
this.cache.delete(oldestKey)
}
this.cache.set(key, {
content,
expires: Date.now() + this.maxAge,
})
}
invalidate(key: string): void {
this.cache.delete(key)
}
clear(): void {
this.cache.clear()
}
}
Error Handling
The adapter maps HTTP errors to content errors:
function handleHttpError(response: Response, uri: string): ContentError {
switch (response.status) {
case 400:
return new ContentError(
`Bad request: ${response.statusText}`,
uri,
'http',
false
)
case 401:
case 403:
return new ContentAccessError(
`Access denied: ${response.statusText}`,
uri,
'http'
)
case 404:
return new ContentNotFoundError(uri, 'http')
case 409:
return new ContentError(
`Conflict: ${response.statusText}`,
uri,
'http',
true,
{ canRetry: false }
)
case 422:
return new ContentValidationError(
`Validation failed: ${response.statusText}`,
uri,
'http',
[`Server validation failed: ${response.statusText}`]
)
default:
return new ContentError(
`HTTP error ${response.status}: ${response.statusText}`,
uri,
'http',
response.status >= 500, // Server errors are potentially recoverable
{ canRetry: response.status >= 500 }
)
}
}
Retry Mechanism
The adapter implements a retry mechanism for transient failures:
async function executeWithRetry<T>(operation: () => Promise<T>): Promise<T> {
const {
maxRetries = 3,
retryDelay = 1000,
useExponentialBackoff = true,
} = options.retryOptions
let attempts = 0
let lastError: Error
while (attempts <= maxRetries) {
try {
return await operation()
} catch (error) {
lastError = error
// Check if error is retryable
if (!isRetryableError(error)) {
throw error
}
// Last attempt failed
if (attempts >= maxRetries) {
break
}
// Delay before retry
const delay = useExponentialBackoff
? retryDelay * Math.pow(2, attempts)
: retryDelay
await sleep(delay)
attempts++
}
}
// All retries failed
throw new ContentError(
`Operation failed after ${attempts} attempts: ${lastError.message}`,
undefined,
'http',
false
)
}
function isRetryableError(error: any): boolean {
// Network errors are retryable
if (error instanceof TypeError && error.message.includes('network')) {
return true
}
// Check if HTTP status is retryable
if (error instanceof ContentError && error.recoverable) {
return true
}
// Specific HTTP status codes
if (
error.status &&
options.retryOptions.retryStatusCodes.includes(error.status)
) {
return true
}
return false
}
Content Serialization
The adapter handles content serialization based on content type:
function serializeContent(content: Content): string | FormData {
const contentType = content.contentType.toLowerCase()
// JSON content
if (contentType.includes('json')) {
return JSON.stringify({
data: content.data,
metadata: content.metadata,
})
}
// Form data
if (contentType.includes('form')) {
const formData = new FormData()
formData.append('data', content.data as string)
// Add metadata as separate fields
for (const [key, value] of Object.entries(content.metadata)) {
formData.append(`metadata[${key}]`, String(value))
}
return formData
}
// Default to plain text
return String(content.data)
}
function deserializeContent(
data: any,
contentType: string,
responseHeaders: Headers
): Content {
// Extract content type from response if not provided
if (!contentType) {
contentType = responseHeaders.get('Content-Type') || 'text/plain'
}
// JSON response
if (contentType.includes('json')) {
// Handle various JSON response formats
if (typeof data === 'string') {
data = JSON.parse(data)
}
// Format 1: { data, metadata } structure
if (data.data !== undefined && data.metadata !== undefined) {
return {
data: data.data,
contentType,
metadata: data.metadata,
}
}
// Format 2: { content, meta } structure
if (data.content !== undefined && data.meta !== undefined) {
return {
data: data.content,
contentType,
metadata: data.meta,
}
}
// Format 3: Complete object is the data
return {
data: JSON.stringify(data),
contentType,
metadata: {},
}
}
// Handle text content
return {
data: String(data),
contentType,
metadata: {},
}
}
Performance Considerations
The HTTP adapter includes performance optimizations:
- Caching: Client-side caching reduces network requests
- Connection Pooling: Reuses connections when possible
- Request Batching: Groups operations to reduce overhead
- Compression: Supports gzip/deflate for reduced bandwidth
- Streaming: Handles large content with response streaming
Environment Compatibility
This adapter works in all JavaScript environments that support the Fetch API:
import { createHttpAdapter } from '@lib/content/adapters'
import { isNode } from '@lib/utils/env'
// Choose the appropriate fetch implementation
const adapter = createHttpAdapter({
baseUrl: 'https://api.example.com/content',
fetchOptions: {
// In Node.js environments, may need to disable SSL verification for development
...(isNode()
? { agent: new https.Agent({ rejectUnauthorized: false }) }
: {}),
},
})