Project Files
src / sources / http.ts
export async function fetchTextWithLimits(
url: string,
options: {
timeoutMs: number;
maxBytes: number;
headers?: Record<string, string>;
}
): Promise<{ text: string; finalUrl: string; etag?: string; lastModified?: string }> {
const parsed = new URL(url);
const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "::1";
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && isLocalhost)) {
throw new Error(`Remote source must be HTTPS unless localhost: ${url}`);
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
try {
const response = await fetch(url, {
redirect: "follow",
headers: options.headers,
signal: controller.signal,
});
if (!response.ok) {
throw new Error(`HTTP ${response.status} ${response.statusText}`);
}
const reader = response.body?.getReader();
if (!reader) {
const text = await response.text();
if (Buffer.byteLength(text, "utf8") > options.maxBytes) {
throw new Error(`Remote source exceeds maxBytes (${options.maxBytes})`);
}
return headersResult(text, response);
}
const chunks: Uint8Array[] = [];
let total = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!value) continue;
total += value.byteLength;
if (total > options.maxBytes) {
throw new Error(`Remote source exceeds maxBytes (${options.maxBytes})`);
}
chunks.push(value);
}
const buffer = Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)));
return headersResult(buffer.toString("utf8"), response);
} finally {
clearTimeout(timer);
}
}
function headersResult(text: string, response: Response): { text: string; finalUrl: string; etag?: string; lastModified?: string } {
return {
text,
finalUrl: response.url,
etag: response.headers.get("etag") ?? undefined,
lastModified: response.headers.get("last-modified") ?? undefined,
};
}
export function resolveUrl(ref: string, baseUrl: string): string | null {
try {
const raw = ref.trim();
if (!raw || raw.startsWith("data:")) return null;
return new URL(raw, baseUrl).toString();
} catch {
return null;
}
}