This document describes the HTTP timeout configuration for outbound requests to Stellar Horizon and external risk evaluation providers.
All outbound HTTP requests use configurable connect and read timeouts to prevent hanging connections and ensure predictable failure modes. The timeout utilities provide:
- Separate connect and read timeout configuration
- Structured error types for timeout vs. other failures
- Environment-based configuration with sensible defaults
- Consistent error handling across all HTTP clients
| Variable | Default | Description |
|---|---|---|
HTTP_CONNECT_TIMEOUT_MS |
5000 |
Connection timeout in milliseconds (time to establish TCP connection) |
HTTP_READ_TIMEOUT_MS |
10000 |
Read timeout in milliseconds (time to receive complete response after connection) |
Development (.env file):
HTTP_CONNECT_TIMEOUT_MS=3000
HTTP_READ_TIMEOUT_MS=8000Production (environment):
export HTTP_CONNECT_TIMEOUT_MS=5000
export HTTP_READ_TIMEOUT_MS=15000Docker Compose:
services:
api:
environment:
- HTTP_CONNECT_TIMEOUT_MS=5000
- HTTP_READ_TIMEOUT_MS=15000import { fetchWithTimeout } from '../utils/fetchWithTimeout.js';
// Uses default timeouts from environment
const response = await fetchWithTimeout('https://horizon-testnet.stellar.org/ledgers');
if (response.ok) {
const data = await response.json();
// Process data
}import { fetchJsonWithTimeout } from '../utils/fetchWithTimeout.js';
interface HorizonLedgerResponse {
_embedded: {
records: Array<{ sequence: number; closed_at: string }>;
};
}
try {
const data = await fetchJsonWithTimeout<HorizonLedgerResponse>(
'https://horizon-testnet.stellar.org/ledgers?limit=10'
);
console.log('Latest ledgers:', data._embedded.records);
} catch (error) {
// Handle errors (see Error Handling section)
}import { fetchWithTimeout } from '../utils/fetchWithTimeout.js';
// Override timeouts for a specific request
const response = await fetchWithTimeout('https://slow-api.example.com/data', {
timeouts: {
connectTimeoutMs: 10000, // 10 seconds to connect
readTimeoutMs: 30000, // 30 seconds to read response
},
headers: {
'Authorization': 'Bearer token',
},
});The timeout utilities provide structured error types for different failure modes:
Thrown when a request exceeds the configured timeout.
import { HttpTimeoutError, fetchWithTimeout } from '../utils/fetchWithTimeout.js';
try {
const response = await fetchWithTimeout('https://horizon-testnet.stellar.org/ledgers');
} catch (error) {
if (error instanceof HttpTimeoutError) {
console.error(`${error.type} timeout after ${error.timeoutMs}ms: ${error.url}`);
// error.type is 'connect' or 'read'
// error.timeoutMs is the timeout value that was exceeded
// error.url is the URL that timed out
}
}Thrown for other HTTP failures (network errors, invalid JSON, non-OK status).
import { HttpRequestError, fetchJsonWithTimeout } from '../utils/fetchWithTimeout.js';
try {
const data = await fetchJsonWithTimeout('https://api.example.com/data');
} catch (error) {
if (error instanceof HttpRequestError) {
console.error(`Request failed: ${error.message}`);
console.error(`URL: ${error.url}`);
if (error.cause) {
console.error(`Cause: ${error.cause.message}`);
}
}
}import {
fetchJsonWithTimeout,
HttpTimeoutError,
HttpRequestError,
} from '../utils/fetchWithTimeout.js';
async function fetchHorizonData(url: string) {
try {
return await fetchJsonWithTimeout(url);
} catch (error) {
if (error instanceof HttpTimeoutError) {
// Timeout - may want to retry with exponential backoff
console.error(`Timeout (${error.type}): ${error.url}`);
throw new Error('Horizon API timeout - please try again');
} else if (error instanceof HttpRequestError) {
// Other HTTP error - check if retryable
console.error(`HTTP error: ${error.message}`);
if (error.cause) {
console.error(`Underlying cause: ${error.cause.message}`);
}
throw new Error('Horizon API request failed');
} else {
// Unknown error
console.error('Unexpected error:', error);
throw error;
}
}
}The Horizon listener (src/services/horizonListener.ts) uses fetchJsonWithTimeout for polling Stellar Horizon events:
// In production, pollOnce would use:
const url = `${config.horizonUrl}/contracts/${contractId}/events?startLedger=${ledger}`;
const response = await fetchJsonWithTimeout<HorizonEventsResponse>(url, {
timeouts: {
connectTimeoutMs: 5000,
readTimeoutMs: 10000,
}
});Errors are caught and logged without stopping the polling loop.
Future integration with external risk providers should use the same timeout utilities:
// src/services/riskService.ts
import { fetchJsonWithTimeout, HttpTimeoutError } from '../utils/fetchWithTimeout.js';
async function fetchRiskScore(walletAddress: string): Promise<RiskScore> {
const url = `${RISK_PROVIDER_URL}/evaluate`;
try {
return await fetchJsonWithTimeout<RiskScore>(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ walletAddress }),
timeouts: {
connectTimeoutMs: 3000,
readTimeoutMs: 8000,
},
});
} catch (error) {
if (error instanceof HttpTimeoutError) {
// Return cached score or default
return getDefaultRiskScore(walletAddress);
}
throw error;
}
}| Service | Connect Timeout | Read Timeout | Rationale |
|---|---|---|---|
| Stellar Horizon | 5s | 10s | Public API, generally fast |
| Risk Providers | 3s | 8s | External service, may be slower |
| Internal APIs | 2s | 5s | Should be very fast |
| Blockchain RPCs | 5s | 15s | Can be slow during high load |
| Environment | Connect Timeout | Read Timeout |
|---|---|---|
| Development | 5s | 10s |
| Staging | 5s | 10s |
| Production | 5s | 15s |
All timeout errors are logged with structured context:
[HorizonListener] read timeout after 10000ms: https://horizon-testnet.stellar.org/contracts/...
[RiskService] HTTP request failed: HTTP 503 Service Unavailable (Network error)
Consider tracking these metrics:
http_request_duration_ms- Histogram of request durationshttp_timeout_total- Counter of timeout errors by type (connect/read)http_request_errors_total- Counter of all HTTP errors by type
Alert on:
- High timeout rate (> 5% of requests)
- Sustained increase in request duration
- Specific service unavailability
- Never log API keys or secrets in error messages
- The timeout utilities do not log request bodies or headers
- Ensure sensitive data is not included in URLs (use POST body instead)
- Wallet addresses may be considered PII in some jurisdictions
- Error logs include URLs but not request/response bodies
- Consider redacting wallet addresses in production logs
- Private keys should NEVER be sent in HTTP requests
- The backend only reads from Horizon (public data)
- Signing operations happen client-side or in secure enclaves
Symptoms: High rate of HttpTimeoutError in logs
Possible causes:
- Network latency to external services
- External service degradation
- Timeout values too aggressive
Solutions:
- Check external service status pages
- Increase timeout values if appropriate
- Implement retry logic with exponential backoff
- Consider caching responses
Symptoms: Requests completing just under timeout threshold
Possible causes:
- Large response payloads
- Database query performance
- Network congestion
Solutions:
- Add pagination to reduce response size
- Optimize database queries
- Use CDN or caching layer
- Increase read timeout if justified
Symptoms: HttpRequestError with network-related causes
Possible causes:
- DNS resolution failures
- Firewall blocking outbound connections
- Service endpoint down
Solutions:
- Verify DNS configuration
- Check firewall rules
- Test connectivity with curl/wget
- Verify service endpoint is correct
The timeout utilities have comprehensive unit tests in src/utils/__tests__/fetchWithTimeout.test.ts:
npm test -- fetchWithTimeoutTest timeout behavior with real services:
// Test with intentionally slow endpoint
const response = await fetchWithTimeout('https://httpbin.org/delay/5', {
timeouts: { connectTimeoutMs: 1000, readTimeoutMs: 2000 }
});
// Should throw HttpTimeoutErrorThe load testing harness (scripts/load/) includes timeout scenarios. See docs/load-testing.md.
- Retry logic with exponential backoff
- Circuit breaker pattern for failing services
- Request/response caching
- Metrics and observability integration
- Connection pooling and keep-alive
- Request prioritization and queuing