Retry Logic
The library includes built-in retry logic to handle transient failures automatically. This is particularly useful for network errors, rate limiting, and temporary server issues.
Enabling Retries
Enable retries at the client level or per-request:
import { createClient } from '@outloud/reqo'
// Enable with defaults
const client = createClient({
retry: true
})
// Or per-request
const data = await reqo.$get('/users', {}, {
retry: true
})
Options
Customize retry behavior with detailed options:
2.const client = createClient({
retry: {
limit: 3 // Retry up to 3 times
}
})
['GET', 'HEAD', 'OPTIONS'].const client = createClient({
retry: {
methods: ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']
}
})
[408, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 530].const client = createClient({
retry: {
statusCodes: [408, 429, 500, 502, 503, 504]
}
})
['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'UND_ERR_SOCKET']const client = createClient({
retry: {
codes: ['ECONNRESET', 'ETIMEDOUT']
}
})
0.1 * (2 ** (retryCount - 1)) * 1000const client = createClient({
retry: {
delay: (state) => {
// Linear backoff: 1s, 2s, 3s
return state.retryCount * 1000
}
}
})
If the function returns
true, the request will be retried. If it returns false, no retry will occur.undefined, the default validation logic is used as fallback.const client = createClient({
retry: {
validate: (state, options) => {
// Custom retry logic
return state.retryCount <= options.limit && state.error.status >= 500
}
}
})
Default Behavior
By default, retries are configured for:
- Methods: GET, HEAD, OPTIONS (safe, idempotent methods)
- Status Codes: 408, 429, 500, 502, 503, 504, 520-526, 530
- Network Errors: ECONNRESET, ETIMEDOUT, ENOTFOUND, ECONNREFUSED
- Limit: 2 retries
- Delay: Exponential backoff (100ms, 200ms, 400ms, ...)
// Default configuration
const defaultRetry = {
limit: 2,
methods: ['GET', 'HEAD', 'OPTIONS'],
statusCodes: [408, 429, 500, 502, 503, 504, 520, 521, 522, 523, 524, 525, 526, 530],
codes: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'ECONNREFUSED', 'UND_ERR_SOCKET'],
delay: (state) => 0.1 * (2 ** (state.retryCount - 1)) * 1000
}
Backoff Strategies
Exponential Backoff (Default)
Doubles the delay between each retry:
const client = createClient({
retry: {
limit: 4,
delay: (state) => {
// 100ms, 200ms, 400ms, 800ms
return 0.1 * (2 ** (state.retryCount - 1)) * 1000
}
}
})
Linear Backoff
Increases delay by a constant amount:
const client = createClient({
retry: {
limit: 3,
delay: (state) => {
// 1s, 2s, 3s
return state.retryCount * 1000
}
}
})
Fixed Delay
Same delay between all retries:
const client = createClient({
retry: {
limit: 3,
delay: () => 2000 // Always 2 seconds
}
})
Jittered Backoff
Add randomness to prevent thundering herd:
const client = createClient({
retry: {
limit: 3,
delay: (state) => {
const baseDelay = 0.1 * (2 ** (state.retryCount - 1)) * 1000
const jitter = Math.random() * 1000
return baseDelay + jitter
}
}
})
Custom Based on Error
Adjust delay based on the error:
const client = createClient({
retry: {
delay: (state) => {
// Longer delay for rate limiting
if (state.error.status === 429) {
const retryAfter = state.error.response?.headers.get('Retry-After')
if (retryAfter) {
return parseInt(retryAfter) * 1000
}
return 60000 // 1 minute
}
// Standard exponential backoff
return 0.1 * (2 ** (state.retryCount - 1)) * 1000
}
}
})
Retry-After Header
Handle the Retry-After response header:
const client = createClient({
retry: {
limit: 3,
delay: (state) => {
// Check for Retry-After header
const retryAfter = state.error.response?.headers.get('Retry-After')
if (retryAfter) {
// Retry-After can be seconds or HTTP date
const delay = parseInt(retryAfter)
if (!isNaN(delay)) {
return delay * 1000
}
// Parse as date
const date = new Date(retryAfter)
return date.getTime() - Date.now()
}
// Default backoff
return state.retryCount * 1000
}
}
})
Retrying Non-Idempotent Methods
By default, only safe methods (GET, HEAD, OPTIONS) are retried. To retry POST, PUT, PATCH, DELETE:
const client = createClient({
retry: {
limit: 2,
methods: ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE']
}
})
// Or per-request
await reqo.$post('/users', userData, {
retry: {
limit: 2,
methods: ['POST']
}
})
Status Code Filtering
Retry only specific status codes:
const client = createClient({
retry: {
// Only retry server errors
statusCodes: [500, 502, 503, 504]
}
})
// Only retry rate limiting
const rateLimitClient = createClient({
retry: {
statusCodes: [429],
delay: (retryCount, error) => {
const retryAfter = error.response?.headers.get('Retry-After')
return retryAfter ? parseInt(retryAfter) * 1000 : 60000
}
}
})
Network Error Handling
Retry on specific network errors:
const client = createClient({
retry: {
codes: [
'ECONNRESET', // Connection reset
'ETIMEDOUT', // Timeout
'ENOTFOUND', // DNS lookup failed
'ECONNREFUSED', // Connection refused
'UND_ERR_SOCKET' // Undici socket error
]
}
})
Custom Validation
Implement complex retry logic with validation function:
const client = createClient({
retry: {
validate: (state, options) => {
// Don't retry after limit
if (state.retryCount > options.limit) {
return false
}
// Don't retry client errors (4xx except 429)
if (state.error.status >= 400 && state.error.status < 500 && state.error.status !== 429) {
return false
}
// Don't retry on specific error messages
if (state.error.message.includes('Invalid token')) {
return false
}
// Retry server errors and network issues
return state.error.status >= 500 || state.error.code !== ''
}
}
})
Per-Request Retry
Override client retry settings for specific requests:
const client = createClient({
retry: {
limit: 2,
methods: ['GET']
}
})
// This request has different retry config
const data = await client.$post('/users', userData, {
retry: {
limit: 5,
methods: ['POST'],
delay: (state) => state.retryCount * 2000
}
})
// This request has no retry
const noRetry = await client.$get('/users', {}, {
retry: false
})
Timeouts and Retries
When using both timeouts and retries, the total time includes all retry attempts:
const client = createClient({
timeout: 10000, // 10 second total timeout
retry: {
limit: 3,
delay: (count) => count * 1000 // 1s, 2s, 3s
}
})
// If the first attempt takes 8s and fails, the retry will be
// canceled after 2s to respect the 10s total timeout
Practical Examples
API with Rate Limiting
const api = createClient({
url: 'https://api.example.com',
retry: {
limit: 5,
statusCodes: [429, 503],
delay: (state) => {
if (state.error.status === 429) {
const retryAfter = state.error.response?.headers.get('Retry-After')
return retryAfter ? parseInt(retryAfter) * 1000 : 60000
}
return state.retryCount * 1000
}
}
})
Unreliable Network
const unreliableApi = createClient({
url: 'https://unreliable-service.com',
retry: {
limit: 5,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
codes: ['ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND'],
delay: (state) => {
// Aggressive exponential backoff with jitter
const base = 0.5 * (2 ** (state.retryCount - 1)) * 1000
const jitter = Math.random() * 500
return base + jitter
}
}
})