ai-chatbot/lib/net/url-access.ts
Your Name 1dc893ee5c
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
chore: update canvas flow and deploy fixes
2026-02-24 19:22:42 +08:00

87 lines
2.1 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 isRemoteUrlReachable(
url: string,
options: UrlAccessOptions = {}
): Promise<boolean> {
const target = String(url || "").trim();
if (!/^https?:\/\//i.test(target)) return false;
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,
});
if (headResponse.ok) return true;
if (![403, 405, 429, 500, 501, 502, 503, 504].includes(headResponse.status)) {
return false;
}
} 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,
});
return getResponse.ok || getResponse.status === 206;
} catch {
return false;
} finally {
getCtx.clear();
}
}
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),
};
}