From 3e4b36ac710b709c97ba73c96dd37400f5759490 Mon Sep 17 00:00:00 2001 From: Maurice Date: Tue, 18 Aug 2026 20:02:28 +0200 Subject: [PATCH] fix(weather): give the Open-Meteo calls a deadline All six outbound calls ran without an AbortSignal, so a hung connection held the caller until the socket gave up on its own. Every other outbound client in the codebase carries a timeout; this one is the reference domain and did not. --- server/src/nest/weather/weather.impl.ts | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/server/src/nest/weather/weather.impl.ts b/server/src/nest/weather/weather.impl.ts index 76523b44c..fa3eea1be 100644 --- a/server/src/nest/weather/weather.impl.ts +++ b/server/src/nest/weather/weather.impl.ts @@ -1,4 +1,9 @@ +// Open-Meteo sits on the request path as a third party: without a deadline a hung +// connection holds the caller until the socket gives up on its own. Every other +// outbound client in the codebase carries one. +const WEATHER_TIMEOUT_MS = 8000; + // ── Interfaces ────────────────────────────────────────────────────────── export interface WeatherResult { @@ -191,7 +196,7 @@ async function _getWeatherImpl( // Forecast range (-1 .. +16 days) if (diffDays >= -1 && diffDays <= 16) { const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}&daily=temperature_2m_max,temperature_2m_min,weathercode&timezone=auto&forecast_days=16`; - const response = await fetch(url); + const response = await fetch(url, { signal: AbortSignal.timeout(WEATHER_TIMEOUT_MS) }); const data = await response.json() as OpenMeteoForecast; if (!response.ok || data.error) { @@ -223,7 +228,7 @@ async function _getWeatherImpl( if (diffDays < -1) { const dateStr = targetDate.toISOString().slice(0, 10); const url = `https://archive-api.open-meteo.com/v1/archive?latitude=${lat}&longitude=${lng}&start_date=${dateStr}&end_date=${dateStr}&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum&timezone=auto`; - const response = await fetch(url); + const response = await fetch(url, { signal: AbortSignal.timeout(WEATHER_TIMEOUT_MS) }); const data = await response.json() as OpenMeteoForecast; if (!response.ok || data.error) { @@ -264,7 +269,7 @@ async function _getWeatherImpl( const endStr = endDate.toISOString().slice(0, 10); const url = `https://archive-api.open-meteo.com/v1/archive?latitude=${lat}&longitude=${lng}&start_date=${startStr}&end_date=${endStr}&daily=temperature_2m_max,temperature_2m_min,precipitation_sum&timezone=auto`; - const response = await fetch(url); + const response = await fetch(url, { signal: AbortSignal.timeout(WEATHER_TIMEOUT_MS) }); const data = await response.json() as OpenMeteoForecast; if (!response.ok || data.error) { @@ -317,7 +322,7 @@ async function _getWeatherImpl( if (cached) return cached; const url = `https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lng}¤t=temperature_2m,weathercode&timezone=auto`; - const response = await fetch(url); + const response = await fetch(url, { signal: AbortSignal.timeout(WEATHER_TIMEOUT_MS) }); const data = await response.json() as OpenMeteoForecast; if (!response.ok || data.error) { @@ -388,7 +393,7 @@ async function _getDetailedWeatherImpl( + `&hourly=temperature_2m,precipitation,weathercode,windspeed_10m,relativehumidity_2m` + `&daily=temperature_2m_max,temperature_2m_min,weathercode,precipitation_sum,windspeed_10m_max,sunrise,sunset` + `&timezone=auto`; - const response = await fetch(url); + const response = await fetch(url, { signal: AbortSignal.timeout(WEATHER_TIMEOUT_MS) }); const data = await response.json() as OpenMeteoForecast; if (!response.ok || data.error) { @@ -451,7 +456,7 @@ async function _getDetailedWeatherImpl( + `&daily=temperature_2m_max,temperature_2m_min,weathercode,sunrise,sunset,precipitation_probability_max,precipitation_sum,windspeed_10m_max` + `&timezone=auto&start_date=${dateStr}&end_date=${dateStr}`; - const response = await fetch(url); + const response = await fetch(url, { signal: AbortSignal.timeout(WEATHER_TIMEOUT_MS) }); const data = await response.json() as OpenMeteoForecast; if (!response.ok || data.error) {