Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
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
}
89 changes: 89 additions & 0 deletions lib/core/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,92 @@ export default function getUserAgent (sdk, application, integration, feature) {

return `${headerParts.filter((item) => item !== '').join('; ')};`
}

// URL validation functions to prevent SSRF attacks
const isValidURL = (url) => {
try {
// Reject obviously malicious patterns early
if (url.includes('@') || url.includes('file://') || url.includes('ftp://')) {
return false
}

// Allow relative URLs (they are safe as they use the same origin)
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
return true
}

// Only validate absolute URLs for SSRF protection
const parsedURL = new URL(url)

// Reject non-HTTP(S) protocols
if (!['http:', 'https:'].includes(parsedURL.protocol)) {
return false
}

// Prevent IP addresses in URLs to avoid internal network access
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/
const ipv6Regex = /^\[?([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\]?$/
if (ipv4Regex.test(parsedURL.hostname) || ipv6Regex.test(parsedURL.hostname)) {
// Only allow localhost IPs in development
const isDevelopment = process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'test' ||
!process.env.NODE_ENV
const localhostIPs = ['127.0.0.1', '0.0.0.0', '::1', 'localhost']
if (!isDevelopment || !localhostIPs.includes(parsedURL.hostname)) {
return false
}
}

return isAllowedHost(parsedURL.hostname)
} catch (error) {
// If URL parsing fails, it might be a relative URL without protocol
// Allow it if it doesn't contain protocol indicators or suspicious patterns
return !url.includes('://') && !url.includes('\\') && !url.includes('@')
}
}

const isAllowedHost = (hostname) => {
// Define allowed domains for Contentstack API
const allowedDomains = [
'api.contentstack.io',
'eu-api.contentstack.com',
'azure-na-api.contentstack.com',
'azure-eu-api.contentstack.com',
'gcp-na-api.contentstack.com',
'gcp-eu-api.contentstack.com'
]

// Check for localhost/development environments
const localhostPatterns = [
'localhost',
'127.0.0.1',
'0.0.0.0'
]

// Only allow localhost in development environments to prevent SSRF in production
const isDevelopment = process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'test' ||
!process.env.NODE_ENV // Default to allowing in non-production if NODE_ENV is not set

if (isDevelopment && localhostPatterns.includes(hostname)) {
return true
}

// Check if hostname is in allowed domains or is a subdomain of allowed domains
return allowedDomains.some(domain => {
return hostname === domain || hostname.endsWith('.' + domain)
})
}

export const validateAndSanitizeConfig = (config) => {
if (!config || !config.url) {
throw new Error('Invalid request configuration: missing URL')
}

// Validate the URL to prevent SSRF attacks
if (!isValidURL(config.url)) {
throw new Error(`SSRF Prevention: URL "${config.url}" is not allowed`)
}

return config
}
Loading
Loading