import "server-only"; type UrlAccessOptions = { timeoutMs?: number; }; const DEFAULT_TIMEOUT_MS = 3500; function withTimeout(timeoutMs: number) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); return { signal: controller.signal, clear: () => clearTimeout(timer), }; } export async function probeRemoteUrl( url: string, options: UrlAccessOptions = {} ): Promise<"reachable" | "unreachable" | "unknown"> { const target = String(url || "").trim(); if (!/^https?:\/\//i.test(target)) return "unreachable"; const timeoutMs = Math.max(800, Number(options.timeoutMs || DEFAULT_TIMEOUT_MS)); const headCtx = withTimeout(timeoutMs); try { const headResponse = await fetch(target, { method: "HEAD", redirect: "follow", cache: "no-store", signal: headCtx.signal, }); await headResponse.body?.cancel(); if (headResponse.ok) return "reachable"; } catch {} finally { headCtx.clear(); } const getCtx = withTimeout(timeoutMs); try { const getResponse = await fetch(target, { method: "GET", headers: { Range: "bytes=0-0" }, redirect: "follow", cache: "no-store", signal: getCtx.signal, }); await getResponse.body?.cancel(); if (getResponse.ok || getResponse.status === 206) return "reachable"; return [400, 401, 403, 404, 410].includes(getResponse.status) ? "unreachable" : "unknown"; } catch { return "unknown"; } finally { getCtx.clear(); } } export async function isRemoteUrlReachable(url: string, options: UrlAccessOptions = {}): Promise { return (await probeRemoteUrl(url, options)) === "reachable"; } export async function filterReachableRemoteUrls( urls: string[], options: UrlAccessOptions = {} ): Promise<{ kept: string[]; dropped: string[] }> { const unique = Array.from( new Set( (Array.isArray(urls) ? urls : []) .map((item) => String(item || "").trim()) .filter((item) => /^https?:\/\//i.test(item)) ) ); if (unique.length === 0) { return { kept: [], dropped: [] }; } const checks = await Promise.all( unique.map(async (url) => ({ url, ok: await isRemoteUrlReachable(url, options), })) ); return { kept: checks.filter((item) => item.ok).map((item) => item.url), dropped: checks.filter((item) => !item.ok).map((item) => item.url), }; }