( url: string, timeoutMs: number )
| 182 | * Normalizes different error types (timeout, network, HTTP errors) into FetchError. |
| 183 | */ |
| 184 | export async function fetchWithTimeout( |
| 185 | url: string, |
| 186 | timeoutMs: number |
| 187 | ): Promise<{ data: unknown; url: string }> { |
| 188 | const controller = new AbortController() |
| 189 | // eslint-disable-next-line no-restricted-globals -- Network timeout utility runs outside React and cannot use hooks. |
| 190 | const timeoutId = setTimeout(() => controller.abort(), timeoutMs) |
| 191 | |
| 192 | try { |
| 193 | const response = await fetch(url, { signal: controller.signal }) |
| 194 | clearTimeout(timeoutId) |
| 195 | |
| 196 | if (!response.ok) { |
| 197 | // Server responded with error status |
| 198 | let data: unknown |
| 199 | try { |
| 200 | data = await response.json() |
| 201 | } catch { |
| 202 | try { |
| 203 | data = await response.text() |
| 204 | } catch { |
| 205 | data = null |
| 206 | } |
| 207 | } |
| 208 | |
| 209 | throw new FetchError( |
| 210 | `Request failed with status ${response.status}`, |
| 211 | url, |
| 212 | { |
| 213 | response: { |
| 214 | status: response.status, |
| 215 | statusText: response.statusText, |
| 216 | data, |
| 217 | }, |
| 218 | } |
| 219 | ) |
| 220 | } |
| 221 | |
| 222 | // Try JSON first, fall back to text (healthz returns string, host-config returns JSON) |
| 223 | // We don't rely on Content-Type since proxies/CDNs may not preserve headers correctly |
| 224 | const text = await response.text() |
| 225 | try { |
| 226 | return { data: JSON.parse(text), url } |
| 227 | } catch { |
| 228 | return { data: text, url } |
| 229 | } |
| 230 | } catch (error) { |
| 231 | clearTimeout(timeoutId) |
| 232 | |
| 233 | // Re-throw FetchError as-is |
| 234 | if (error instanceof FetchError) { |
| 235 | throw error |
| 236 | } |
| 237 | |
| 238 | // Handle AbortController timeout |
| 239 | if (error instanceof DOMException && error.name === "AbortError") { |
| 240 | throw new FetchError("Connection timed out", url, { isTimeout: true }) |
| 241 | } |
no test coverage detected
searching dependent graphs…