ai-chatbot/lib/net/url-access.ts

91 lines
2.4 KiB
TypeScript

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<boolean> {
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),
};
}