diff --git a/package-lock.json b/package-lock.json index c92aa18..a6e63c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "axios": "^1", - "axios-retry": "^4", + "fetch-retry": "^6.0.0", "winston": "^3" }, "devDependencies": { @@ -3319,6 +3318,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, "license": "MIT" }, "node_modules/atomic-sleep": { @@ -3342,29 +3342,6 @@ "fastq": "^1.17.1" } }, - "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/axios-retry": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.4.1.tgz", - "integrity": "sha512-JGzNoglDHtHWIEvvAampB0P7jxQ/sT4COmW0FgSQkVg6o4KqNjNMBI6uFVOq517hkw/OAYYAG08ADtBlV8lvmQ==", - "license": "Apache-2.0", - "dependencies": { - "is-retry-allowed": "^2.2.0" - }, - "peerDependencies": { - "axios": "0.x || 1.x" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3767,6 +3744,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4000,6 +3978,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4845,6 +4824,12 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, + "node_modules/fetch-retry": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fetch-retry/-/fetch-retry-6.0.0.tgz", + "integrity": "sha512-BUFj1aMubgib37I3v4q78fYo63Po7t4HUPTpQ6/QE6yK6cIQrP+W43FYToeTEyg5m2Y7eFUtijUuAv/PDlWuag==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4967,26 +4952,6 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", "license": "MIT" }, - "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", @@ -5008,6 +4973,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5637,18 +5603,6 @@ "node": ">=8" } }, - "node_modules/is-retry-allowed": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", - "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", @@ -6283,6 +6237,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -6292,6 +6247,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -9822,12 +9778,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index 81d93d3..fc916d7 100644 --- a/package.json +++ b/package.json @@ -123,8 +123,7 @@ "printWidth": 80 }, "dependencies": { - "axios": "^1", - "axios-retry": "^4", + "fetch-retry": "^6.0.0", "winston": "^3" }, "peerDependencies": { diff --git a/src/common/client.ts b/src/common/client.ts index fab4829..89eb8db 100644 --- a/src/common/client.ts +++ b/src/common/client.ts @@ -1,7 +1,5 @@ -import axios, { AxiosError, AxiosInstance } from "axios"; -import axiosRetry from "axios-retry"; import { randomUUID } from "crypto"; - +import fetchRetry from "fetch-retry"; import { Logger, getLogger } from "./logging.js"; import { isValidClientId, isValidEnv } from "./paramValidation.js"; import RequestCounter from "./requestCounter.js"; @@ -17,9 +15,26 @@ import ValidationErrorCounter from "./validationErrorCounter.js"; const SYNC_INTERVAL = 60000; // 60 seconds const INITIAL_SYNC_INTERVAL = 10000; // 10 seconds const INITIAL_SYNC_INTERVAL_DURATION = 3600000; // 1 hour -const REQUEST_TIMEOUT = 10000; // 10 seconds const MAX_QUEUE_TIME = 3.6e6; // 1 hour +const fetchWithRetry = fetchRetry(fetch, { + retries: 3, + retryDelay: 1000, + retryOn: [408, 429, 500, 502, 503, 504], +}); + +class HTTPError extends Error { + public response: Response; + + constructor(response: Response) { + const reason = response.status + ? `status code ${response.status}` + : "an unknown error"; + super(`Request failed with ${reason}`); + this.response = response; + } +} + export class ApitallyClient { private clientId: string; private env: string; @@ -27,7 +42,6 @@ export class ApitallyClient { private static instance?: ApitallyClient; private instanceUuid: string; private requestsDataQueue: Array<[number, RequestsDataPayload]>; - private axiosClient: AxiosInstance; private syncIntervalId?: NodeJS.Timeout; public appInfo?: AppInfo; private appInfoSent: boolean = false; @@ -62,16 +76,6 @@ export class ApitallyClient { this.serverErrorCounter = new ServerErrorCounter(); this.logger = logger || getLogger(); - this.axiosClient = axios.create({ - baseURL: this.getHubUrl(), - timeout: REQUEST_TIMEOUT, - }); - axiosRetry(this.axiosClient, { - retries: 3, - retryDelay: (retryCount, error) => - axiosRetry.exponentialDelay(retryCount, error, 1000), - }); - this.startSync(); this.handleShutdown = this.handleShutdown.bind(this); } @@ -95,11 +99,22 @@ export class ApitallyClient { ApitallyClient.instance = undefined; } - private getHubUrl() { + private getHubUrlPrefix() { const baseURL = process.env.APITALLY_HUB_BASE_URL || "https://hub.apitally.io"; const version = "v1"; - return `${baseURL}/${version}/${this.clientId}/${this.env}`; + return `${baseURL}/${version}/${this.clientId}/${this.env}/`; + } + + private async makeHubRequest(url: string, payload: any) { + const response = await fetchWithRetry(this.getHubUrlPrefix() + url, { + method: "POST", + body: JSON.stringify(payload), + headers: { "Content-Type": "application/json" }, + }); + if (!response.ok) { + throw new HTTPError(response); + } } private startSync() { @@ -123,7 +138,7 @@ export class ApitallyClient { } await Promise.all(promises); } catch (error) { - this.logger.error("Error while syncing with Apitally Hub.", { + this.logger.error("Error while syncing with Apitally Hub", { error, }); } @@ -144,22 +159,21 @@ export class ApitallyClient { private async sendAppInfo() { if (this.appInfo) { - this.logger.debug("Sending app info to Apitally Hub."); + this.logger.debug("Sending app info to Apitally Hub"); const payload: AppInfoPayload = { instance_uuid: this.instanceUuid, message_uuid: randomUUID(), ...this.appInfo, }; try { - await this.axiosClient.post("/info", payload); + await this.makeHubRequest("info", payload); this.appInfoSent = true; } catch (error) { const handled = this.handleHubError(error); if (!handled) { + this.logger.error((error as Error).message); this.logger.debug( - `Error while sending app info to Apitally Hub (${ - (error as AxiosError).code - }). Will retry.`, + "Error while sending app info to Apitally Hub (will retry)", { error }, ); } @@ -168,7 +182,7 @@ export class ApitallyClient { } private async sendRequestsData() { - this.logger.debug("Sending requests data to Apitally Hub."); + this.logger.debug("Sending requests data to Apitally Hub"); const newPayload: RequestsDataPayload = { time_offset: 0, instance_uuid: this.instanceUuid, @@ -189,15 +203,13 @@ export class ApitallyClient { const timeOffset = Date.now() - time; if (timeOffset <= MAX_QUEUE_TIME) { payload.time_offset = timeOffset / 1000.0; // In seconds - await this.axiosClient.post("/requests", payload); + await this.makeHubRequest("requests", payload); } } catch (error) { const handled = this.handleHubError(error); if (!handled) { this.logger.debug( - `Error while sending requests data to Apitally Hub (${ - (error as AxiosError).code - }). Will retry.`, + "Error while sending requests data to Apitally Hub (will retry)", { error }, ); failedItems.push(queueItem); @@ -209,14 +221,16 @@ export class ApitallyClient { } private handleHubError(error: unknown) { - if ( - error instanceof AxiosError && - error.response && - error.response.status === 404 - ) { - this.logger.error(`Invalid Apitally client ID '${this.clientId}'.`); - this.stopSync(); - return true; + if (error instanceof HTTPError) { + if (error.response.status === 404) { + this.logger.error(`Invalid Apitally client ID: '${this.clientId}'`); + this.stopSync(); + return true; + } + if (error.response.status === 422) { + this.logger.error("Received validation error from Apitally Hub"); + return true; + } } return false; }