Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@ fileignoreconfig:
checksum: 9d592c580a6890473e007c339d2f91c2d94ad936be1740dcef5ac500fde0cdb4
- filename: lib/stack/asset/index.js
checksum: b3358310e9cb2fb493d70890b7219db71e2202360be764465d505ef71907eefe
- filename: examples/robust-error-handling.js
checksum: e8a32ffbbbdba2a15f3d327273f0a5b4eb33cf84cd346562596ab697125bbbc6
Comment thread
nadeem-cs marked this conversation as resolved.
version: ""
87 changes: 87 additions & 0 deletions examples/robust-error-handling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Example: Configuring Robust Error Handling for Transient Network Failures
// This example shows how to use the enhanced retry mechanisms in the Contentstack Management SDK

const contentstack = require('../lib/contentstack')

// Example 1: Basic configuration with enhanced network retry
const clientWithBasicRetry = contentstack.client({
api_key: 'your_api_key',
management_token: 'your_management_token',
// Enhanced network retry configuration
retryOnNetworkFailure: true, // Enable network failure retries
maxNetworkRetries: 3, // Max 3 attempts for network failures
networkRetryDelay: 100, // Start with 100ms delay
networkBackoffStrategy: 'exponential' // Use exponential backoff (100ms, 200ms, 400ms)
})

// Example 2: Advanced configuration with fine-grained control
const clientWithAdvancedRetry = contentstack.client({
api_key: 'your_api_key',
management_token: 'your_management_token',
// Network failure retry settings
retryOnNetworkFailure: true,
retryOnDnsFailure: true, // Retry on DNS resolution failures (EAI_AGAIN)
retryOnSocketFailure: true, // Retry on socket errors (ECONNRESET, ETIMEDOUT, etc.)
retryOnHttpServerError: true, // Retry on HTTP 5xx errors
maxNetworkRetries: 5, // Allow up to 5 network retries
networkRetryDelay: 200, // Start with 200ms delay
networkBackoffStrategy: 'exponential',

// Original retry settings (for non-network errors)
retryOnError: true,
retryLimit: 3,
retryDelay: 500,

// Custom logging
logHandler: (level, message) => {
console.log(`[${level.toUpperCase()}] ${new Date().toISOString()}: ${message}`)
}
})

// Example 3: Conservative configuration for production
const clientForProduction = contentstack.client({
api_key: 'your_api_key',
management_token: 'your_management_token',
// Conservative retry settings for production
retryOnNetworkFailure: true,
maxNetworkRetries: 2, // Only 2 retries to avoid long delays
networkRetryDelay: 300, // Longer initial delay
networkBackoffStrategy: 'fixed', // Fixed delay instead of exponential

// Custom retry condition for additional control
retryCondition: (error) => {
Comment thread
nadeem-cs marked this conversation as resolved.
// Custom logic: only retry on specific conditions
return error.response && error.response.status >= 500
}
})

// Example usage with error handling
async function demonstrateRobustErrorHandling () {
try {
const stack = clientWithAdvancedRetry.stack('your_stack_api_key')
const contentTypes = await stack.contentType().query().find()
console.log('Content types retrieved successfully:', contentTypes.items.length)
} catch (error) {
if (error.retryAttempts) {
console.error(`Request failed after ${error.retryAttempts} retry attempts:`, error.message)
console.error('Original error:', error.originalError?.code)
} else {
console.error('Request failed:', error.message)
}
}
}

// The SDK will now automatically handle:
// ✅ DNS resolution failures (EAI_AGAIN)
// ✅ Socket errors (ECONNRESET, ETIMEDOUT, ECONNREFUSED)
// ✅ HTTP timeouts (ECONNABORTED)
// ✅ HTTP 5xx server errors (500-599)
// ✅ Exponential backoff with configurable delays
// ✅ Clear logging and user-friendly error messages

module.exports = {
clientWithBasicRetry,
clientWithAdvancedRetry,
clientForProduction,
demonstrateRobustErrorHandling
}
141 changes: 137 additions & 4 deletions lib/core/concurrency-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,16 @@ import OAuthHandler from './oauthHandler'
const defaultConfig = {
maxRequests: 5,
retryLimit: 5,
retryDelay: 300
retryDelay: 300,
// Enhanced retry configuration for transient network failures
retryOnError: true,
retryOnNetworkFailure: true,
retryOnDnsFailure: true,
retryOnSocketFailure: true,
retryOnHttpServerError: true,
maxNetworkRetries: 3,
networkRetryDelay: 100, // Base delay for network retries (ms)
networkBackoffStrategy: 'exponential' // 'exponential' or 'fixed'
}

export function ConcurrencyQueue ({ axios, config }) {
Expand All @@ -19,13 +28,124 @@ export function ConcurrencyQueue ({ axios, config }) {
} else if (config.retryDelay && config.retryDelay < 300) {
throw Error('Retry Policy Error: minimum retry delay for requests is 300')
}
// Validate network retry configuration
if (config.maxNetworkRetries && config.maxNetworkRetries < 0) {
throw Error('Network Retry Policy Error: maxNetworkRetries cannot be negative')
}
if (config.networkRetryDelay && config.networkRetryDelay < 50) {
throw Error('Network Retry Policy Error: minimum network retry delay is 50ms')
}
}

this.config = Object.assign({}, defaultConfig, config)
this.queue = []
this.running = []
this.paused = false

// Helper function to determine if an error is a transient network failure
const isTransientNetworkError = (error) => {
// DNS resolution failures
if (this.config.retryOnDnsFailure && error.code === 'EAI_AGAIN') {
return { type: 'DNS_RESOLUTION', reason: 'DNS resolution failure (EAI_AGAIN)' }
}

// Socket and connection errors
if (this.config.retryOnSocketFailure) {
const socketErrorCodes = ['ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'ENOTFOUND', 'EHOSTUNREACH']
Comment thread
nadeem-cs marked this conversation as resolved.
if (socketErrorCodes.includes(error.code)) {
Comment thread
nadeem-cs marked this conversation as resolved.
return { type: 'SOCKET_ERROR', reason: `Socket error: ${error.code}` }
}
}

// Connection timeouts
if (this.config.retryOnNetworkFailure && error.code === 'ECONNABORTED') {
return { type: 'TIMEOUT', reason: 'Connection timeout' }
}

// HTTP 5xx server errors
if (this.config.retryOnHttpServerError && error.response && error.response.status >= 500 && error.response.status <= 599) {
return { type: 'HTTP_SERVER_ERROR', reason: `HTTP ${error.response.status} server error` }
}

return null
}

// Calculate retry delay with backoff strategy
const calculateNetworkRetryDelay = (attempt) => {
const baseDelay = this.config.networkRetryDelay
if (this.config.networkBackoffStrategy === 'exponential') {
return baseDelay * Math.pow(2, attempt - 1)
Comment thread
nadeem-cs marked this conversation as resolved.
Outdated
}
return baseDelay // Fixed delay
}

// Log retry attempts
const logRetryAttempt = (errorInfo, attempt, delay) => {
const message = `Transient ${errorInfo.type} detected: ${errorInfo.reason}. Retry attempt ${attempt}/${this.config.maxNetworkRetries} in ${delay}ms`
if (this.config.logHandler) {
this.config.logHandler('warning', message)
} else {
console.warn(`[Contentstack SDK] ${message}`)
}
}

// Log final failure
const logFinalFailure = (errorInfo, maxRetries) => {
const message = `Final retry failed for ${errorInfo.type}: ${errorInfo.reason}. Exceeded max retries (${maxRetries}).`
if (this.config.logHandler) {
this.config.logHandler('error', message)
} else {
console.error(`[Contentstack SDK] ${message}`)
}
}

// Enhanced retry function for network errors
const retryNetworkError = (error, errorInfo, attempt) => {
if (attempt > this.config.maxNetworkRetries) {
logFinalFailure(errorInfo, this.config.maxNetworkRetries)
// Final error message
const finalError = new Error(`Network request failed after ${this.config.maxNetworkRetries} retries: ${errorInfo.reason}`)
finalError.code = error.code
finalError.originalError = error
finalError.retryAttempts = attempt - 1
return Promise.reject(finalError)
}

const delay = calculateNetworkRetryDelay(attempt)
logRetryAttempt(errorInfo, attempt, delay)

// Initialize retry count if not present
if (!error.config.networkRetryCount) {
error.config.networkRetryCount = 0
}
error.config.networkRetryCount = attempt

return new Promise((resolve, reject) => {
setTimeout(() => {
// Remove the failed request from running queue
const runningIndex = this.running.findIndex(item => item.request === error.config)
Comment thread
nadeem-cs marked this conversation as resolved.
Outdated
if (runningIndex !== -1) {
this.running.splice(runningIndex, 1)
}

// Retry the request
axios(updateRequestConfig(error, `Network retry ${attempt}`, delay))
Comment thread
nadeem-cs marked this conversation as resolved.
Outdated
.then(resolve)
.catch((retryError) => {
// Check if this is still a transient error and we can retry again
const retryErrorInfo = isTransientNetworkError(retryError)
if (retryErrorInfo) {
retryNetworkError(retryError, retryErrorInfo, attempt + 1)
.then(resolve)
.catch(reject)
Comment thread
nadeem-cs marked this conversation as resolved.
Outdated
} else {
reject(retryError)
}
})
}, delay)
})
Comment thread
nadeem-cs marked this conversation as resolved.
}

// Initial shift will check running request,
// and adds request to running queue if max requests are not running
this.initialShift = () => {
Expand Down Expand Up @@ -226,12 +346,20 @@ export function ConcurrencyQueue ({ axios, config }) {
const responseErrorHandler = error => {
let networkError = error.config.retryCount
let retryErrorType = null

// First, check for transient network errors
const networkErrorInfo = isTransientNetworkError(error)
if (networkErrorInfo && this.config.retryOnNetworkFailure) {
const networkRetryCount = error.config.networkRetryCount || 0
return retryNetworkError(error, networkErrorInfo, networkRetryCount + 1)
}

// Original retry logic for non-network errors
if (!this.config.retryOnError || networkError > this.config.retryLimit) {
return Promise.reject(responseHandler(error))
}
// Check rate limit remaining header before retrying

// Error handling
// Check rate limit remaining header before retrying
const wait = this.config.retryDelay
var response = error.response
if (!response) {
Expand Down Expand Up @@ -300,7 +428,12 @@ export function ConcurrencyQueue ({ axios, config }) {

const updateRequestConfig = (error, retryErrorType, wait) => {
const requestConfig = error.config
this.config.logHandler('warning', `${retryErrorType} error occurred. Waiting for ${wait} ms before retrying...`)
const message = `${retryErrorType} error occurred. Waiting for ${wait} ms before retrying...`
if (this.config.logHandler) {
this.config.logHandler('warning', message)
} else {
console.warn(`[Contentstack SDK] ${message}`)
}
if (axios !== undefined && axios.defaults !== undefined) {
if (axios.defaults.agent === requestConfig.agent) {
delete requestConfig.agent
Expand Down
Loading