Skip to content

Commit 8968be4

Browse files
feat(http_fetcher): improve network error handling with exponential backoff
Network errors now use exponential backoff strategy instead of fixed reloadInterval delay, enabling faster recovery from transient issues. Changes: - Add networkErrorCount tracking alongside serverErrorCount - Network errors retry at: 15s → 30s → 60s → cap at reloadInterval - Gradual log-level escalation: WARN for first 2 attempts, ERROR after - Extract retry calculation to static HTTPFetcher.calculateBackoffDelay() - Apply same backoff strategy to WeatherGov initialization retries - Reset both counters on successful response Benefits: - Faster recovery from transient network glitches (15s vs 10min) - Less log spam for temporary issues (WARN vs ERROR initially) - Consistent retry behavior across HTTPFetcher and provider init - Reusable backoff calculation for future providers Example: SMHI "fetch failed" now retries after 15s instead of 10min.
1 parent bf676be commit 8968be4

File tree

2 files changed

+38
-5
lines changed

2 files changed

+38
-5
lines changed

defaultmodules/weather/providers/weathergov.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class WeatherGovProvider {
5050
// Retry on temporary errors (DNS, timeout, network)
5151
if (errorInfo.isRetryable && this.initRetryCount < 5) {
5252
this.initRetryCount++;
53-
const delay = Math.min(30000 * Math.pow(2, this.initRetryCount - 1), 5 * 60 * 1000); // 30s, 60s, 120s, 240s, 300s max
53+
const delay = HTTPFetcher.calculateBackoffDelay(this.initRetryCount);
5454
Log.info(`[weathergov] Will retry initialization in ${Math.round(delay / 1000)}s (attempt ${this.initRetryCount}/5)`);
5555
this.initRetryTimer = setTimeout(() => this.initialize(), delay);
5656
} else if (this.onErrorCallback) {

js/http_fetcher.js

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ const ERROR_TYPE_TO_TRANSLATION = {
4242
*/
4343
class HTTPFetcher extends EventEmitter {
4444

45+
/**
46+
* Calculates exponential backoff delay for retries
47+
* @param {number} attempt - Attempt number (1-based)
48+
* @param {object} options - Configuration options
49+
* @param {number} [options.baseDelay] - Initial delay in ms (default: 15s)
50+
* @param {number} [options.maxDelay] - Maximum delay in ms (default: 5min)
51+
* @returns {number} Delay in milliseconds
52+
* @example
53+
* HTTPFetcher.calculateBackoffDelay(1) // 15000 (15s)
54+
* HTTPFetcher.calculateBackoffDelay(2) // 30000 (30s)
55+
* HTTPFetcher.calculateBackoffDelay(3) // 60000 (60s)
56+
* HTTPFetcher.calculateBackoffDelay(6) // 300000 (5min, capped)
57+
*/
58+
static calculateBackoffDelay (attempt, { baseDelay = 15000, maxDelay = 300000 } = {}) {
59+
return Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
60+
}
61+
4562
/**
4663
* Creates a new HTTPFetcher instance
4764
* @param {string} url - The URL to fetch
@@ -71,6 +88,7 @@ class HTTPFetcher extends EventEmitter {
7188

7289
this.reloadTimer = null;
7390
this.serverErrorCount = 0;
91+
this.networkErrorCount = 0;
7492
}
7593

7694
/**
@@ -226,7 +244,7 @@ class HTTPFetcher extends EventEmitter {
226244
errorType,
227245
translationKey: ERROR_TYPE_TO_TRANSLATION[errorType] || "MODULE_ERROR_UNSPECIFIED",
228246
retryAfter,
229-
retryCount: this.serverErrorCount,
247+
retryCount: errorType === "NETWORK_ERROR" ? this.networkErrorCount : this.serverErrorCount,
230248
url: this.url,
231249
originalError
232250
};
@@ -255,8 +273,9 @@ class HTTPFetcher extends EventEmitter {
255273
nextDelay = delay;
256274
this.emit("error", errorInfo);
257275
} else {
258-
// Reset server error count on success
276+
// Reset error counts on success
259277
this.serverErrorCount = 0;
278+
this.networkErrorCount = 0;
260279

261280
/**
262281
* Response event - fired when fetch succeeds
@@ -269,6 +288,13 @@ class HTTPFetcher extends EventEmitter {
269288
const isTimeout = error.name === "AbortError";
270289
const message = isTimeout ? `Request timeout after ${this.timeout}ms` : `Network error: ${error.message}`;
271290

291+
// Apply exponential backoff for network errors
292+
this.networkErrorCount = Math.min(this.networkErrorCount + 1, this.maxRetries);
293+
const backoffDelay = HTTPFetcher.calculateBackoffDelay(this.networkErrorCount, {
294+
maxDelay: this.reloadInterval
295+
});
296+
nextDelay = backoffDelay;
297+
272298
// Truncate URL for cleaner logs
273299
let shortUrl = this.url;
274300
try {
@@ -277,13 +303,20 @@ class HTTPFetcher extends EventEmitter {
277303
} catch (urlError) {
278304
// If URL parsing fails, use original URL
279305
}
280-
Log.error(`${this.logContext}${shortUrl} - ${message}`);
306+
307+
// Gradual log-level escalation: WARN for first 2 attempts, ERROR after
308+
const retryMessage = `Retry #${this.networkErrorCount} in ${Math.round(nextDelay / 1000)}s.`;
309+
if (this.networkErrorCount <= 2) {
310+
Log.warn(`${this.logContext}${shortUrl} - ${message} ${retryMessage}`);
311+
} else {
312+
Log.error(`${this.logContext}${shortUrl} - ${message} ${retryMessage}`);
313+
}
281314

282315
const errorInfo = this.#createErrorInfo(
283316
message,
284317
null,
285318
"NETWORK_ERROR",
286-
this.reloadInterval,
319+
nextDelay,
287320
error
288321
);
289322

0 commit comments

Comments
 (0)