Skip to content

Commit 4a446ae

Browse files
authored
Merge pull request #395 from contentstack/enhancement/DX-3178
feat: add robust error handling for transient network failures
2 parents f43ae6a + 9b0208f commit 4a446ae

File tree

5 files changed

+405
-39
lines changed

5 files changed

+405
-39
lines changed

.talismanrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,6 @@ fileignoreconfig:
2222
checksum: 51ae00f07f4cc75c1cd832b311c2e2482f04a8467a0139da6013ceb88fbdda2f
2323
- filename: lib/stack/deliveryToken/previewToken/index.js
2424
checksum: b506f33bffdd20dfc701f964370707f5d7b28a2c05c70665f0edb7b3c53c165b
25+
- filename: examples/robust-error-handling.js
26+
checksum: e8a32ffbbbdba2a15f3d327273f0a5b4eb33cf84cd346562596ab697125bbbc6
2527
version: "1.0"

examples/robust-error-handling.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// Example: Configuring Robust Error Handling for Transient Network Failures
2+
// This example shows how to use the enhanced retry mechanisms in the Contentstack Management SDK
3+
4+
const contentstack = require('../lib/contentstack')
5+
6+
// Example 1: Basic configuration with enhanced network retry
7+
const clientWithBasicRetry = contentstack.client({
8+
api_key: 'your_api_key',
9+
management_token: 'your_management_token',
10+
// Enhanced network retry configuration
11+
retryOnNetworkFailure: true, // Enable network failure retries
12+
maxNetworkRetries: 3, // Max 3 attempts for network failures
13+
networkRetryDelay: 100, // Start with 100ms delay
14+
networkBackoffStrategy: 'exponential' // Use exponential backoff (100ms, 200ms, 400ms)
15+
})
16+
17+
// Example 2: Advanced configuration with fine-grained control
18+
const clientWithAdvancedRetry = contentstack.client({
19+
api_key: 'your_api_key',
20+
management_token: 'your_management_token',
21+
// Network failure retry settings
22+
retryOnNetworkFailure: true,
23+
retryOnDnsFailure: true, // Retry on DNS resolution failures (EAI_AGAIN)
24+
retryOnSocketFailure: true, // Retry on socket errors (ECONNRESET, ETIMEDOUT, etc.)
25+
retryOnHttpServerError: true, // Retry on HTTP 5xx errors
26+
maxNetworkRetries: 5, // Allow up to 5 network retries
27+
networkRetryDelay: 200, // Start with 200ms delay
28+
networkBackoffStrategy: 'exponential',
29+
30+
// Original retry settings (for non-network errors)
31+
retryOnError: true,
32+
retryLimit: 3,
33+
retryDelay: 500,
34+
35+
// Custom logging
36+
logHandler: (level, message) => {
37+
console.log(`[${level.toUpperCase()}] ${new Date().toISOString()}: ${message}`)
38+
}
39+
})
40+
41+
// Example 3: Conservative configuration for production
42+
const clientForProduction = contentstack.client({
43+
api_key: 'your_api_key',
44+
management_token: 'your_management_token',
45+
// Conservative retry settings for production
46+
retryOnNetworkFailure: true,
47+
maxNetworkRetries: 2, // Only 2 retries to avoid long delays
48+
networkRetryDelay: 300, // Longer initial delay
49+
networkBackoffStrategy: 'fixed', // Fixed delay instead of exponential
50+
51+
// Custom retry condition for additional control
52+
retryCondition: (error) => {
53+
// Custom logic: only retry on specific conditions
54+
return error.response && error.response.status >= 500
55+
}
56+
})
57+
58+
// Example usage with error handling
59+
async function demonstrateRobustErrorHandling () {
60+
try {
61+
const stack = clientWithAdvancedRetry.stack('your_stack_api_key')
62+
const contentTypes = await stack.contentType().query().find()
63+
console.log('Content types retrieved successfully:', contentTypes.items.length)
64+
} catch (error) {
65+
if (error.retryAttempts) {
66+
console.error(`Request failed after ${error.retryAttempts} retry attempts:`, error.message)
67+
console.error('Original error:', error.originalError?.code)
68+
} else {
69+
console.error('Request failed:', error.message)
70+
}
71+
}
72+
}
73+
74+
// The SDK will now automatically handle:
75+
// ✅ DNS resolution failures (EAI_AGAIN)
76+
// ✅ Socket errors (ECONNRESET, ETIMEDOUT, ECONNREFUSED)
77+
// ✅ HTTP timeouts (ECONNABORTED)
78+
// ✅ HTTP 5xx server errors (500-599)
79+
// ✅ Exponential backoff with configurable delays
80+
// ✅ Clear logging and user-friendly error messages
81+
82+
module.exports = {
83+
clientWithBasicRetry,
84+
clientWithAdvancedRetry,
85+
clientForProduction,
86+
demonstrateRobustErrorHandling
87+
}

lib/core/Util.js

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,92 @@ export default function getUserAgent (sdk, application, integration, feature) {
100100

101101
return `${headerParts.filter((item) => item !== '').join('; ')};`
102102
}
103+
104+
// URL validation functions to prevent SSRF attacks
105+
const isValidURL = (url) => {
106+
try {
107+
// Reject obviously malicious patterns early
108+
if (url.includes('@') || url.includes('file://') || url.includes('ftp://')) {
109+
return false
110+
}
111+
112+
// Allow relative URLs (they are safe as they use the same origin)
113+
if (url.startsWith('/') || url.startsWith('./') || url.startsWith('../')) {
114+
return true
115+
}
116+
117+
// Only validate absolute URLs for SSRF protection
118+
const parsedURL = new URL(url)
119+
120+
// Reject non-HTTP(S) protocols
121+
if (!['http:', 'https:'].includes(parsedURL.protocol)) {
122+
return false
123+
}
124+
125+
// Prevent IP addresses in URLs to avoid internal network access
126+
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/
127+
const ipv6Regex = /^\[?([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\]?$/
128+
if (ipv4Regex.test(parsedURL.hostname) || ipv6Regex.test(parsedURL.hostname)) {
129+
// Only allow localhost IPs in development
130+
const isDevelopment = process.env.NODE_ENV === 'development' ||
131+
process.env.NODE_ENV === 'test' ||
132+
!process.env.NODE_ENV
133+
const localhostIPs = ['127.0.0.1', '0.0.0.0', '::1', 'localhost']
134+
if (!isDevelopment || !localhostIPs.includes(parsedURL.hostname)) {
135+
return false
136+
}
137+
}
138+
139+
return isAllowedHost(parsedURL.hostname)
140+
} catch (error) {
141+
// If URL parsing fails, it might be a relative URL without protocol
142+
// Allow it if it doesn't contain protocol indicators or suspicious patterns
143+
return !url.includes('://') && !url.includes('\\') && !url.includes('@')
144+
}
145+
}
146+
147+
const isAllowedHost = (hostname) => {
148+
// Define allowed domains for Contentstack API
149+
const allowedDomains = [
150+
'api.contentstack.io',
151+
'eu-api.contentstack.com',
152+
'azure-na-api.contentstack.com',
153+
'azure-eu-api.contentstack.com',
154+
'gcp-na-api.contentstack.com',
155+
'gcp-eu-api.contentstack.com'
156+
]
157+
158+
// Check for localhost/development environments
159+
const localhostPatterns = [
160+
'localhost',
161+
'127.0.0.1',
162+
'0.0.0.0'
163+
]
164+
165+
// Only allow localhost in development environments to prevent SSRF in production
166+
const isDevelopment = process.env.NODE_ENV === 'development' ||
167+
process.env.NODE_ENV === 'test' ||
168+
!process.env.NODE_ENV // Default to allowing in non-production if NODE_ENV is not set
169+
170+
if (isDevelopment && localhostPatterns.includes(hostname)) {
171+
return true
172+
}
173+
174+
// Check if hostname is in allowed domains or is a subdomain of allowed domains
175+
return allowedDomains.some(domain => {
176+
return hostname === domain || hostname.endsWith('.' + domain)
177+
})
178+
}
179+
180+
export const validateAndSanitizeConfig = (config) => {
181+
if (!config || !config.url) {
182+
throw new Error('Invalid request configuration: missing URL')
183+
}
184+
185+
// Validate the URL to prevent SSRF attacks
186+
if (!isValidURL(config.url)) {
187+
throw new Error(`SSRF Prevention: URL "${config.url}" is not allowed`)
188+
}
189+
190+
return config
191+
}

0 commit comments

Comments
 (0)