Skip to content

Commit b7a292e

Browse files
committed
fix: improve API request reliability with retry logic and DNS configuration
- Add automatic retry mechanism for transient network failures (EAI_AGAIN, ECONNRESET, ENOTFOUND, ETIMEDOUT) - Implement exponential backoff for retries (500ms, 1000ms delays) - Configure up to 2 retry attempts in production, disabled in test environment - Add explicit timeout configuration (30s) and Connection: close header to prevent keep-alive issues - Add Google DNS servers (8.8.8.8, 8.8.4.4) to Docker Compose for reliable DNS resolution - Consolidate all timeout and retry settings into this.options for consistency - Improve error logging with retry attempt counters and warnings Fixes socket hang up and DNS resolution errors that prevented the application from displaying data.
1 parent 8e9aa89 commit b7a292e

File tree

4 files changed

+81
-12
lines changed

4 files changed

+81
-12
lines changed

docker-compose.prod.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ services:
66
target: production
77
environment:
88
- NODE_ENV=production
9+
dns:
10+
- 8.8.8.8
11+
- 8.8.4.4
912
ports:
1013
- "3000:3000"
1114
healthcheck:
@@ -15,6 +18,9 @@ services:
1518
retries: 3
1619
start_period: 40s
1720
restart: unless-stopped
21+
# hosts file entries
22+
# extra_hosts:
23+
# - "arquivo.pt:194.210.235.20"
1824
# Security hardening options
1925
security_opt:
2026
# Prevents container processes from gaining additional privileges

docker-compose.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ services:
66
target: development
77
environment:
88
- NODE_ENV=development
9+
dns:
10+
- 8.8.8.8
11+
- 8.8.4.4
912
volumes:
1013
- ./:/home/node/app
1114
- /home/node/app/node_modules

src/apis/api-request.js

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,25 @@ class ApiRequest {
4040
this.defaultApiParams = defaultApiParams;
4141
this.defaultApiReply = defaultApiReply;
4242
this.logger = logger('ApiRequest');
43-
this.options = { method: 'GET' };
43+
this.options = {
44+
method: 'GET',
45+
timeout: 30000, // 30 second timeout
46+
maxRetries: process.env.NODE_ENV === 'test' ? 0 : 2, // Disable retries in test environment
47+
retryDelay: 500, // 500ms between retries
48+
headers: {
49+
'Connection': 'close' // Prevent keep-alive issues
50+
}
51+
};
4452
}
4553

4654
/**
47-
* Performs a GET request to the configured API URL
55+
* Performs a GET request to the configured API URL with retry logic
4856
*
4957
* @param {URLSearchParams} requestData - Query parameters for the request
5058
* @param {Function} callback - Callback function(data) invoked with response data
59+
* @param {number} attempt - Current retry attempt (internal use)
5160
*/
52-
get(requestData, callback) {
61+
get(requestData, callback, attempt = 0) {
5362
let apiReply = '';
5463
let callbackInvoked = false;
5564

@@ -94,16 +103,39 @@ class ApiRequest {
94103
});
95104

96105
apiReq.on('error', (e) => {
97-
this.logger.error(`${this.apiUrl} : Request error: ${e.message}`);
98-
safeCallback(this.defaultApiReply);
106+
// Retry on DNS failures (EAI_AGAIN) or connection resets
107+
const isRetryableError = e.code === 'EAI_AGAIN' ||
108+
e.code === 'ECONNRESET' ||
109+
e.code === 'ENOTFOUND' ||
110+
e.code === 'ETIMEDOUT';
111+
112+
// Only retry if not already retrying and retries are enabled
113+
const shouldRetry = isRetryableError &&
114+
attempt < this.options.maxRetries &&
115+
!callbackInvoked;
116+
117+
if (shouldRetry) {
118+
this.logger.warn(`${this.apiUrl} : ${e.message} (attempt ${attempt + 1}/${this.options.maxRetries + 1})`);
119+
setTimeout(() => {
120+
this.get(requestData, callback, attempt + 1);
121+
}, this.options.retryDelay * (attempt + 1)); // Exponential backoff
122+
} else {
123+
this.logger.error(`${this.apiUrl} : Request error: ${e.message}`);
124+
safeCallback(this.defaultApiReply);
125+
}
99126
});
100127

101128
apiReq.on('timeout', () => {
102-
this.logger.error(`${this.apiUrl} : Timeout (${this.options.timeout || 120000} ms)`);
129+
this.logger.error(`${this.apiUrl} : Timeout (${this.options.timeout}ms)`);
103130
apiReq.destroy();
104131
safeCallback(this.defaultApiReply);
105132
});
106133

134+
// Set the timeout on the request
135+
if (this.options.timeout) {
136+
apiReq.setTimeout(this.options.timeout);
137+
}
138+
107139
apiReq.end();
108140

109141
} catch (e) {

src/apis/base-api-request.js

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,15 @@ class BaseApiRequest {
3333
this.defaultApiParams = defaultApiParams;
3434
this.defaultApiReply = defaultApiReply;
3535
this.logger = logger(loggerName);
36-
this.options = options || { method: 'GET' };
36+
this.options = options || {
37+
method: 'GET',
38+
timeout: 30000,
39+
maxRetries: process.env.NODE_ENV === 'test' ? 0 : 2,
40+
retryDelay: 500,
41+
headers: {
42+
'Connection': 'close'
43+
}
44+
};
3745
}
3846

3947
/**
@@ -62,8 +70,9 @@ class BaseApiRequest {
6270
*
6371
* @param {URLSearchParams} requestData - Query parameters for the request
6472
* @param {Function} callback - Callback function invoked with response data
73+
* @param {number} attempt - Current retry attempt (internal use)
6574
*/
66-
get(requestData, callback) {
75+
get(requestData, callback, attempt = 0) {
6776
// Validate callback
6877
if (typeof callback !== 'function') {
6978
return;
@@ -90,11 +99,30 @@ class BaseApiRequest {
9099

91100
// Handle request errors
92101
apiReq.on('error', (e) => {
93-
this.logger.error(`${this.apiUrl} : Request error: ${e.message}`);
94-
if (!apiReq.destroyed) {
95-
apiReq.destroy();
102+
// Retry on DNS failures (EAI_AGAIN) or connection resets
103+
const isRetryableError = e.code === 'EAI_AGAIN' ||
104+
e.code === 'ECONNRESET' ||
105+
e.code === 'ENOTFOUND' ||
106+
e.code === 'ETIMEDOUT';
107+
108+
// Only retry if we haven't exceeded max retries
109+
const shouldRetry = isRetryableError && attempt < this.options.maxRetries;
110+
111+
if (shouldRetry) {
112+
this.logger.warn(`${this.apiUrl} : ${e.message} (attempt ${attempt + 1}/${this.options.maxRetries + 1})`);
113+
if (!apiReq.destroyed) {
114+
apiReq.destroy();
115+
}
116+
setTimeout(() => {
117+
this.get(requestData, callback, attempt + 1);
118+
}, this.options.retryDelay * (attempt + 1)); // Exponential backoff
119+
} else {
120+
this.logger.error(`${this.apiUrl} : Request error: ${e.message}`);
121+
if (!apiReq.destroyed) {
122+
apiReq.destroy();
123+
}
124+
safeCallback(this.defaultApiReply);
96125
}
97-
safeCallback(this.defaultApiReply);
98126
});
99127

100128
// Handle timeout

0 commit comments

Comments
 (0)