diff options
Diffstat (limited to 'packages/core/src/utils/fetch.ts')
| -rw-r--r-- | packages/core/src/utils/fetch.ts | 57 |
1 files changed, 57 insertions, 0 deletions
diff --git a/packages/core/src/utils/fetch.ts b/packages/core/src/utils/fetch.ts new file mode 100644 index 00000000..e78a3247 --- /dev/null +++ b/packages/core/src/utils/fetch.ts @@ -0,0 +1,57 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { getErrorMessage, isNodeError } from './errors.js'; +import { URL } from 'url'; + +const PRIVATE_IP_RANGES = [ + /^10\./, + /^127\./, + /^172\.(1[6-9]|2[0-9]|3[0-1])\./, + /^192\.168\./, + /^::1$/, + /^fc00:/, + /^fe80:/, +]; + +export class FetchError extends Error { + constructor( + message: string, + public code?: string, + ) { + super(message); + this.name = 'FetchError'; + } +} + +export function isPrivateIp(url: string): boolean { + try { + const hostname = new URL(url).hostname; + return PRIVATE_IP_RANGES.some((range) => range.test(hostname)); + } catch (_e) { + return false; + } +} + +export async function fetchWithTimeout( + url: string, + timeout: number, +): Promise<Response> { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { signal: controller.signal }); + return response; + } catch (error) { + if (isNodeError(error) && error.code === 'ABORT_ERR') { + throw new FetchError(`Request timed out after ${timeout}ms`, 'ETIMEDOUT'); + } + throw new FetchError(getErrorMessage(error)); + } finally { + clearTimeout(timeoutId); + } +} |
