Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 10 additions & 2 deletions js/http_fetcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ const ERROR_TYPE_TO_TRANSLATION = {
RATE_LIMITED: "MODULE_ERROR_RATE_LIMITED",
SERVER_ERROR: "MODULE_ERROR_SERVER_ERROR",
CLIENT_ERROR: "MODULE_ERROR_CLIENT_ERROR",
NETWORK_ERROR: "MODULE_ERROR_NO_CONNECTION",
NETWORK_ERROR: "MODULE_ERROR_UNSPECIFIED",
UNKNOWN_ERROR: "MODULE_ERROR_UNSPECIFIED"
};

// Error codes meaning the device is offline, as opposed to a single server being unreachable
const OFFLINE_ERROR_CODES = new Set(["EAI_AGAIN", "ENETDOWN", "ENETUNREACH"]);

/**
* HTTPFetcher - Centralized HTTP fetching with intelligent error handling
*
Expand Down Expand Up @@ -357,7 +360,9 @@ class HTTPFetcher extends EventEmitter {
}
} catch (error) {
const isTimeout = error.name === "AbortError";
const message = isTimeout ? `Request timeout after ${this.timeout}ms` : `Network error: ${error.message}`;
// fetch only says "fetch failed", the actual reason (e.g. ENOTFOUND) is in the cause
const reason = error.cause?.code ?? error.message;
const message = isTimeout ? `Request timeout after ${this.timeout}ms` : `Network error: ${reason}`;

this.networkErrorCount = Math.min(this.networkErrorCount + 1, this.maxRetries);
const exhausted = this.networkErrorCount >= this.maxRetries;
Expand Down Expand Up @@ -385,6 +390,9 @@ class HTTPFetcher extends EventEmitter {
error,
requestUrl
);
if (OFFLINE_ERROR_CODES.has(error.cause?.code)) {
errorInfo.translationKey = "MODULE_ERROR_NO_CONNECTION";
}
this.emit("error", errorInfo);
} finally {
clearTimeout(timeoutId);
Expand Down
20 changes: 19 additions & 1 deletion tests/unit/functions/http_fetcher_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,28 @@ describe("HTTPFetcher", () => {
const errorInfo = await errorPromise;

expect(errorInfo).toHaveProperty("errorType", "NETWORK_ERROR");
expect(errorInfo).toHaveProperty("translationKey", "MODULE_ERROR_NO_CONNECTION");
expect(errorInfo).toHaveProperty("url", TEST_URL);
});

it.each([
["EAI_AGAIN", "MODULE_ERROR_NO_CONNECTION"],
["ERR_TLS_CERT_ALTNAME_INVALID", "MODULE_ERROR_UNSPECIFIED"]
])("should report %s as %s and log the reason", async (code, translationKey) => {
const fetchSpy = vi.spyOn(globalThis, "fetch").mockRejectedValue(new TypeError("fetch failed", { cause: Object.assign(new Error(), { code }) }));
fetcher = new HTTPFetcher({ url: TEST_URL, reloadInterval: 60000 });

const errorPromise = new Promise((resolve) => {
fetcher.on("error", resolve);
});

await fetcher.fetch();
const errorInfo = await errorPromise;
fetchSpy.mockRestore();

expect(errorInfo.translationKey).toBe(translationKey);
expect(errorInfo.message).toBe(`Network error: ${code}`);
});

it("should emit error event on timeout", async () => {
server.use(
http.get(TEST_URL, async () => {
Expand Down
Loading