From 5c85f85e3fccc2f82203ccc312d65528554c06e6 Mon Sep 17 00:00:00 2001 From: Sam Stenvall Date: Thu, 8 Aug 2024 12:05:15 +0300 Subject: [PATCH] Replace axios with native fetch Fixes stupid maxContentLength error on aborted requests (https://github.com/axios/axios/issues/4806) --- package-lock.json | 91 ------------------------------------- package.json | 1 - src/http/client.ts | 24 ++++++---- src/sensor/iotawatt.ts | 12 ++--- src/sensor/shelly.ts | 31 +++++++++---- tests/sensor/shelly.test.ts | 14 +++--- 6 files changed, 47 insertions(+), 126 deletions(-) diff --git a/package-lock.json b/package-lock.json index b7e9f49..cef3bed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "license": "GPL-3.0-or-later", "dependencies": { "@influxdata/influxdb-client": "^1.33.2", - "axios": "^1.6.0", "modbus-serial": "^8.0.16", "mqtt": "^5.1.2", "slugify": "^1.6.6", @@ -2060,21 +2059,6 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz", - "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2510,17 +2494,6 @@ "text-hex": "1.0.x" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commist": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/commist/-/commist-3.2.0.tgz", @@ -2653,14 +2626,6 @@ "node": ">=0.10.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -3155,38 +3120,6 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, - "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" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -4452,25 +4385,6 @@ "node": ">=8.6" } }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4952,11 +4866,6 @@ "node": ">= 6" } }, - "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==" - }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", diff --git a/package.json b/package.json index 3685949..c498af6 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ }, "dependencies": { "@influxdata/influxdb-client": "^1.33.2", - "axios": "^1.6.0", "modbus-serial": "^8.0.16", "mqtt": "^5.1.2", "slugify": "^1.6.6", diff --git a/src/http/client.ts b/src/http/client.ts index bbfc65d..b9bb1d5 100644 --- a/src/http/client.ts +++ b/src/http/client.ts @@ -1,23 +1,26 @@ -import axios, { AxiosResponse } from 'axios' -import http from 'http' import { createLogger } from '../logger' const logger = createLogger('http') -const httpClient = axios.create({ - // We keep polling the same hosts over and over so keep-alive is essential - httpAgent: new http.Agent({ keepAlive: true }), -}) - +let requestTimeout = 0 let lastTimestamp = 0 const promiseCache = new Map() +const createRequestParams = (): RequestInit => { + return { + // We keep polling the same hosts over and over so keep-alive is essential + keepalive: true, + // Use the configured timeout + signal: AbortSignal.timeout(requestTimeout), + } +} + export const setRequestTimeout = (timeoutMs: number) => { - httpClient.defaults.timeout = timeoutMs + requestTimeout = timeoutMs logger.info(`Using ${timeoutMs} millisecond timeout for HTTP requests`) } -export const getDedupedResponse = async (timestamp: number, url: string): Promise => { +export const getDedupedResponse = async (timestamp: number, url: string): Promise => { // Clear the cache whenever the timestamp changes if (timestamp !== lastTimestamp) { lastTimestamp = timestamp @@ -30,7 +33,8 @@ export const getDedupedResponse = async (timestamp: number, url: string): Promis return promiseCache.get(key) } - const promise = httpClient.get(url) + const request = new Request(url, createRequestParams()) + const promise = fetch(request) promiseCache.set(key, promise) return promise diff --git a/src/sensor/iotawatt.ts b/src/sensor/iotawatt.ts index 74dd04a..3d011af 100644 --- a/src/sensor/iotawatt.ts +++ b/src/sensor/iotawatt.ts @@ -114,10 +114,10 @@ export const getSensorData: PowerSensorPollFunction = async ( const sensor = circuit.sensor as IotawattSensor try { - const configurationResult = await getDedupedResponse(timestamp, getConfigurationUrl(sensor)) - const configuration = configurationResult.data as IotawattConfiguration - const statusResult = await getDedupedResponse(timestamp, getStatusUrl(sensor)) - const status = statusResult.data as IotawattStatus + const configurationResult = (await getDedupedResponse(timestamp, getConfigurationUrl(sensor))).clone() + const configuration = (await configurationResult.json()) as IotawattConfiguration + const statusResult = (await getDedupedResponse(timestamp, getStatusUrl(sensor))).clone() + const status = (await statusResult.json()) as IotawattStatus return { timestamp: timestamp, @@ -142,8 +142,8 @@ export const getCharacteristicsSensorData: CharacteristicsSensorPollFunction = a const sensor = characteristics.sensor as IotawattCharacteristicsSensor try { - const queryResult = await getDedupedResponse(timestamp, getQueryUrl(sensor)) - const query = queryResult.data as IotawattCharacteristicsQuery + const queryResult = (await getDedupedResponse(timestamp, getQueryUrl(sensor))).clone() + const query = (await queryResult.json()) as IotawattCharacteristicsQuery return { timestamp: timestamp, diff --git a/src/sensor/shelly.ts b/src/sensor/shelly.ts index 872d5b5..d76b55f 100644 --- a/src/sensor/shelly.ts +++ b/src/sensor/shelly.ts @@ -11,7 +11,6 @@ import { } from '../sensor' import { Circuit } from '../circuit' import { getDedupedResponse } from '../http/client' -import { AxiosResponse } from 'axios' import { Characteristics } from '../characteristics' import { createLogger } from '../logger' @@ -66,9 +65,13 @@ const getSensorDataUrl = (sensor: ShellySensor | ShellyCharacteristicsSensor): s } } -const parseGen1Response = (timestamp: number, circuit: Circuit, httpResponse: AxiosResponse): PowerSensorData => { +const parseGen1Response = async ( + timestamp: number, + circuit: Circuit, + httpResponse: Response, +): Promise => { const sensor = circuit.sensor as ShellySensor - const data = httpResponse.data as Gen1StatusResult + const data = (await httpResponse.json()) as Gen1StatusResult return { timestamp: timestamp, @@ -77,8 +80,12 @@ const parseGen1Response = (timestamp: number, circuit: Circuit, httpResponse: Ax } } -const parseGen2PMResponse = (timestamp: number, circuit: Circuit, httpResponse: AxiosResponse): PowerSensorData => { - const data = httpResponse.data as Gen2SwitchGetStatusResult +const parseGen2PMResponse = async ( + timestamp: number, + circuit: Circuit, + httpResponse: Response, +): Promise => { + const data = (await httpResponse.json()) as Gen2SwitchGetStatusResult return { timestamp: timestamp, @@ -87,9 +94,13 @@ const parseGen2PMResponse = (timestamp: number, circuit: Circuit, httpResponse: } } -const parseGen2EMResponse = (timestamp: number, circuit: Circuit, httpResponse: AxiosResponse): PowerSensorData => { +const parseGen2EMResponse = async ( + timestamp: number, + circuit: Circuit, + httpResponse: Response, +): Promise => { const sensor = circuit.sensor as ShellySensor - const data = httpResponse.data as Gen2EMGetStatusResult + const data = (await httpResponse.json()) as Gen2EMGetStatusResult let power = 0 let apparentPower = 0 @@ -129,7 +140,7 @@ export const getSensorData: PowerSensorPollFunction = async ( const url = getSensorDataUrl(sensor) try { - const httpResponse = await getDedupedResponse(timestamp, url) + const httpResponse = (await getDedupedResponse(timestamp, url)).clone() // Parse the response differently depending on what type of Shelly we're dealing with switch (sensor.shelly.type as ShellyType) { @@ -159,8 +170,8 @@ export const getCharacteristicsSensorData: CharacteristicsSensorPollFunction = a } try { - const httpResponse = await getDedupedResponse(timestamp, url) - const data = httpResponse.data as Gen2EMGetStatusResult + const httpResponse = (await getDedupedResponse(timestamp, url)).clone() + const data = (await httpResponse.json()) as Gen2EMGetStatusResult let voltage = 0 let frequency = 0 diff --git a/tests/sensor/shelly.test.ts b/tests/sensor/shelly.test.ts index 34ce4fd..9735c18 100644 --- a/tests/sensor/shelly.test.ts +++ b/tests/sensor/shelly.test.ts @@ -10,24 +10,22 @@ const gen2pmResponse = fs.readFileSync('./tests/sensor/shelly-plus-1pm.Switch.Ge // Mock getDedupedResponse calls to return real-world data jest.mock('../../src/http/client', () => ({ - getDedupedResponse: (timestamp: number, url: string) => { - let contents + getDedupedResponse: async (timestamp: number, url: string) => { + let contents: string | null = null switch (url) { case 'http://127.0.0.1/status': - contents = gen1Response + contents = String(gen1Response) break case 'http://127.0.0.1/rpc/EM.GetStatus?id=0': - contents = gen2emResponse + contents = String(gen2emResponse) break case 'http://127.0.0.1/rpc/Switch.GetStatus?id=0': - contents = gen2pmResponse + contents = String(gen2pmResponse) break } - return { - data: JSON.parse(String(contents)), - } + return Promise.resolve(new Response(contents)) }, }))