From 08f1374fa0a458afd7975eb619bcf4adfc48830c Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 28 Jul 2024 09:51:32 +0200 Subject: [PATCH 1/3] fix: fetch timeout for external requests to small --- packages/api/src/router/location.ts | 3 ++- packages/api/src/router/widgets/weather.ts | 3 ++- packages/common/src/fetch-with-timeout.ts | 17 +++++++++++++++++ packages/common/src/index.ts | 1 + packages/icons/package.json | 5 +++-- .../src/repositories/github.icon-repository.ts | 4 +++- .../repositories/jsdelivr.icon-repository.ts | 4 +++- packages/ping/src/index.ts | 4 ++-- pnpm-lock.yaml | 3 +++ 9 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 packages/common/src/fetch-with-timeout.ts diff --git a/packages/api/src/router/location.ts b/packages/api/src/router/location.ts index 11e6e6770..c2060314b 100644 --- a/packages/api/src/router/location.ts +++ b/packages/api/src/router/location.ts @@ -1,3 +1,4 @@ +import { fetchWithTimeout } from "@homarr/common"; import type { z } from "@homarr/validation"; import { validation } from "@homarr/validation"; @@ -8,7 +9,7 @@ export const locationRouter = createTRPCRouter({ .input(validation.location.searchCity.input) .output(validation.location.searchCity.output) .query(async ({ input }) => { - const res = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`); + const res = await fetchWithTimeout(`https://geocoding-api.open-meteo.com/v1/search?name=${input.query}`); return (await res.json()) as z.infer; }), }); diff --git a/packages/api/src/router/widgets/weather.ts b/packages/api/src/router/widgets/weather.ts index 665fdedd4..3a53157f8 100644 --- a/packages/api/src/router/widgets/weather.ts +++ b/packages/api/src/router/widgets/weather.ts @@ -1,10 +1,11 @@ +import { fetchWithTimeout } from "@homarr/common"; import { validation } from "@homarr/validation"; import { createTRPCRouter, publicProcedure } from "../../trpc"; export const weatherRouter = createTRPCRouter({ atLocation: publicProcedure.input(validation.widget.weather.atLocationInput).query(async ({ input }) => { - const res = await fetch( + const res = await fetchWithTimeout( `https://api.open-meteo.com/v1/forecast?latitude=${input.latitude}&longitude=${input.longitude}&daily=weathercode,temperature_2m_max,temperature_2m_min¤t_weather=true&timezone=auto`, ); const json: unknown = await res.json(); diff --git a/packages/common/src/fetch-with-timeout.ts b/packages/common/src/fetch-with-timeout.ts new file mode 100644 index 000000000..c01930c5a --- /dev/null +++ b/packages/common/src/fetch-with-timeout.ts @@ -0,0 +1,17 @@ +/** + * Same as fetch, but with a timeout of 10 seconds. + * https://stackoverflow.com/questions/46946380/fetch-api-request-timeout + * @param param0 fetch arguments + * @returns fetch response + */ +export const fetchWithTimeout = (...[url, requestInit]: Parameters) => { + const controller = new AbortController(); + + // 10 seconds timeout: + const timeoutId = setTimeout(() => controller.abort(), 10000); + + return fetch(url, { signal: controller.signal, ...requestInit }).then((response) => { + clearTimeout(timeoutId); + return response; + }); +}; diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 0b6d4cb65..45141ab45 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -8,3 +8,4 @@ export * from "./url"; export * from "./number"; export * from "./error"; export * from "./encryption"; +export * from "./fetch-with-timeout"; diff --git a/packages/icons/package.json b/packages/icons/package.json index fd72d8133..751ed256c 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -21,7 +21,8 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "@homarr/log": "workspace:^0.1.0" + "@homarr/log": "workspace:^0.1.0", + "@homarr/common": "workspace:^0.1.0" }, "devDependencies": { "@homarr/eslint-config": "workspace:^0.2.0", @@ -31,4 +32,4 @@ "typescript": "^5.5.4" }, "prettier": "@homarr/prettier-config" -} +} \ No newline at end of file diff --git a/packages/icons/src/repositories/github.icon-repository.ts b/packages/icons/src/repositories/github.icon-repository.ts index 780f280da..695ade2a0 100644 --- a/packages/icons/src/repositories/github.icon-repository.ts +++ b/packages/icons/src/repositories/github.icon-repository.ts @@ -1,3 +1,5 @@ +import { fetchWithTimeout } from "@homarr/common"; + import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { RepositoryIconGroup } from "../types/repository-icon-group"; import { IconRepository } from "./icon-repository"; @@ -19,7 +21,7 @@ export class GitHubIconRepository extends IconRepository { throw new Error("Repository URLs are required for this repository"); } - const response = await fetch(this.repositoryIndexingUrl); + const response = await fetchWithTimeout(this.repositoryIndexingUrl); const listOfFiles = (await response.json()) as GitHubApiResponse; return { diff --git a/packages/icons/src/repositories/jsdelivr.icon-repository.ts b/packages/icons/src/repositories/jsdelivr.icon-repository.ts index 919cdd2ba..1c153d5b2 100644 --- a/packages/icons/src/repositories/jsdelivr.icon-repository.ts +++ b/packages/icons/src/repositories/jsdelivr.icon-repository.ts @@ -1,3 +1,5 @@ +import { fetchWithTimeout } from "@homarr/common"; + import type { IconRepositoryLicense } from "../types/icon-repository-license"; import type { RepositoryIconGroup } from "../types/repository-icon-group"; import { IconRepository } from "./icon-repository"; @@ -15,7 +17,7 @@ export class JsdelivrIconRepository extends IconRepository { } protected async getAllIconsInternalAsync(): Promise { - const response = await fetch(this.repositoryIndexingUrl); + const response = await fetchWithTimeout(this.repositoryIndexingUrl); const listOfFiles = (await response.json()) as JsdelivrApiResponse; return { diff --git a/packages/ping/src/index.ts b/packages/ping/src/index.ts index 3dafac233..93dbc88c4 100644 --- a/packages/ping/src/index.ts +++ b/packages/ping/src/index.ts @@ -1,9 +1,9 @@ -import { extractErrorMessage } from "@homarr/common"; +import { extractErrorMessage, fetchWithTimeout } from "@homarr/common"; import { logger } from "@homarr/log"; export const sendPingRequestAsync = async (url: string) => { try { - return await fetch(url).then((response) => ({ statusCode: response.status })); + return await fetchWithTimeout(url).then((response) => ({ statusCode: response.status })); } catch (error) { logger.error("packages/ping/src/index.ts:", error); return { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ffbb7b3a0..0da1e0fdc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -868,6 +868,9 @@ importers: packages/icons: dependencies: + '@homarr/common': + specifier: workspace:^0.1.0 + version: link:../common '@homarr/log': specifier: workspace:^0.1.0 version: link:../log From e0cffbab1ce78121f45cbc22acfd25ed8cfef24b Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 28 Jul 2024 09:55:05 +0200 Subject: [PATCH 2/3] fix: format issue --- packages/icons/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/icons/package.json b/packages/icons/package.json index 751ed256c..a6b345042 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -32,4 +32,4 @@ "typescript": "^5.5.4" }, "prettier": "@homarr/prettier-config" -} \ No newline at end of file +} From 1a64ffd7b99a9dccacc80aae4feba75923d5a49e Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sun, 28 Jul 2024 19:32:06 +0200 Subject: [PATCH 3/3] fix: move clear timeout for fetch to finally --- packages/common/src/fetch-with-timeout.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/common/src/fetch-with-timeout.ts b/packages/common/src/fetch-with-timeout.ts index c01930c5a..48353415f 100644 --- a/packages/common/src/fetch-with-timeout.ts +++ b/packages/common/src/fetch-with-timeout.ts @@ -10,8 +10,7 @@ export const fetchWithTimeout = (...[url, requestInit]: Parameters // 10 seconds timeout: const timeoutId = setTimeout(() => controller.abort(), 10000); - return fetch(url, { signal: controller.signal, ...requestInit }).then((response) => { + return fetch(url, { signal: controller.signal, ...requestInit }).finally(() => { clearTimeout(timeoutId); - return response; }); };