From 3bcb4da1e5048a7b2141716d9bf0387a75909ee6 Mon Sep 17 00:00:00 2001 From: fallenbagel <98979876+fallenbagel@users.noreply.github.com> Date: Mon, 23 Feb 2026 04:32:31 +0500 Subject: [PATCH] feat(servarr-api): make Servarr API request timeout configurable (#2556) --- server/api/servarr/base.ts | 6 +-- server/api/servarr/radarr.ts | 12 +----- server/api/servarr/sonarr.ts | 12 +----- server/lib/settings/index.ts | 2 + .../Settings/SettingsNetwork/index.tsx | 38 +++++++++++++++++++ src/i18n/locale/en.json | 3 ++ 6 files changed, 50 insertions(+), 23 deletions(-) diff --git a/server/api/servarr/base.ts b/server/api/servarr/base.ts index cdbb2203..5e18f5b4 100644 --- a/server/api/servarr/base.ts +++ b/server/api/servarr/base.ts @@ -1,7 +1,7 @@ import ExternalAPI from '@server/api/externalapi'; import type { AvailableCacheIds } from '@server/lib/cache'; import cacheManager from '@server/lib/cache'; -import type { DVRSettings } from '@server/lib/settings'; +import { getSettings, type DVRSettings } from '@server/lib/settings'; export interface SystemStatus { version: string; @@ -92,14 +92,14 @@ class ServarrBase extends ExternalAPI { apiKey, cacheName, apiName, - timeout = 10000, }: { url: string; apiKey: string; cacheName: AvailableCacheIds; apiName: string; - timeout?: number; }) { + const timeout = getSettings().network.apiRequestTimeout; + super( url, { diff --git a/server/api/servarr/radarr.ts b/server/api/servarr/radarr.ts index 29f492d7..3d0cf53a 100644 --- a/server/api/servarr/radarr.ts +++ b/server/api/servarr/radarr.ts @@ -64,16 +64,8 @@ export interface RadarrMovie { } class RadarrAPI extends ServarrBase<{ movieId: number }> { - constructor({ - url, - apiKey, - timeout, - }: { - url: string; - apiKey: string; - timeout?: number; - }) { - super({ url, apiKey, cacheName: 'radarr', apiName: 'Radarr', timeout }); + constructor({ url, apiKey }: { url: string; apiKey: string }) { + super({ url, apiKey, cacheName: 'radarr', apiName: 'Radarr' }); } public getMovies = async (): Promise => { diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index e8b39c6f..01b429ba 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -111,16 +111,8 @@ class SonarrAPI extends ServarrBase<{ episodeId: number; episode: EpisodeResult; }> { - constructor({ - url, - apiKey, - timeout, - }: { - url: string; - apiKey: string; - timeout?: number; - }) { - super({ url, apiKey, apiName: 'Sonarr', cacheName: 'sonarr', timeout }); + constructor({ url, apiKey }: { url: string; apiKey: string }) { + super({ url, apiKey, apiName: 'Sonarr', cacheName: 'sonarr' }); } public async getSeries(): Promise { diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 0aa9da15..1e659cd2 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -171,6 +171,7 @@ export interface NetworkSettings { trustProxy: boolean; proxy: ProxySettings; dnsCache: DnsCacheSettings; + apiRequestTimeout: number; } interface PublicSettings { @@ -593,6 +594,7 @@ class Settings { forceMinTtl: 0, forceMaxTtl: -1, }, + apiRequestTimeout: 10000, }, migrations: [], }; diff --git a/src/components/Settings/SettingsNetwork/index.tsx b/src/components/Settings/SettingsNetwork/index.tsx index 21dc6a00..3adffd63 100644 --- a/src/components/Settings/SettingsNetwork/index.tsx +++ b/src/components/Settings/SettingsNetwork/index.tsx @@ -56,6 +56,10 @@ const messages = defineMessages('components.Settings.SettingsNetwork', { 'Do NOT enable this if you are experiencing issues with DNS lookups', dnsCacheForceMinTtl: 'DNS Cache Minimum TTL', dnsCacheForceMaxTtl: 'DNS Cache Maximum TTL', + apiRequestTimeout: 'API Request Timeout', + apiRequestTimeoutTip: + 'Maximum time (in seconds) to wait for responses from external services like Radarr/Sonarr. Set to 0 for no timeout.', + validationApiRequestTimeout: 'You must provide a valid timeout value', }); const SettingsNetwork = () => { @@ -91,6 +95,10 @@ const SettingsNetwork = () => { .max(65535, intl.formatMessage(messages.validationProxyPort)) .required(intl.formatMessage(messages.validationProxyPort)), }), + apiRequestTimeout: Yup.number() + .typeError(intl.formatMessage(messages.validationApiRequestTimeout)) + .required(intl.formatMessage(messages.validationApiRequestTimeout)) + .min(0, intl.formatMessage(messages.validationApiRequestTimeout)), }); if (!data && !error) { @@ -130,6 +138,10 @@ const SettingsNetwork = () => { proxyPassword: data?.proxy?.password, proxyBypassFilter: data?.proxy?.bypassFilter, proxyBypassLocalAddresses: data?.proxy?.bypassLocalAddresses, + apiRequestTimeout: + data?.apiRequestTimeout !== undefined + ? data.apiRequestTimeout / 1000 + : 10, }} enableReinitialize validationSchema={NetworkSettingsSchema} @@ -154,6 +166,7 @@ const SettingsNetwork = () => { bypassFilter: values.proxyBypassFilter, bypassLocalAddresses: values.proxyBypassLocalAddresses, }, + apiRequestTimeout: Number(values.apiRequestTimeout) * 1000, }); mutate('/api/v1/settings/public'); mutate('/api/v1/status'); @@ -341,6 +354,31 @@ const SettingsNetwork = () => { )} +
+ +
+ +
+ {errors.apiRequestTimeout && + touched.apiRequestTimeout && + typeof errors.apiRequestTimeout === 'string' && ( +
{errors.apiRequestTimeout}
+ )} +