All files remoteHar.ts

79.16% Statements 38/48
74.35% Branches 29/39
100% Functions 4/4
85.71% Lines 36/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 781x                     9x 9x       9x 9x 2x   7x   6x 1x 1x 1x   6x 6x       5x 5x           1x   1x 1x         1x   1x 1x     1x   1x 1x       1x 1x 1x 1x 1x 1x 2x 2x 1x 1x       1x   1x    
const MAX_REMOTE_HAR_BYTES = 100 * 1024 * 1024;
 
export interface RemoteHar {
  name: string;
  text: string;
  url: string;
}
 
/** Resolve a direct HAR/NDJSON URL, including normal gist.github.com share links. */
export function remoteHarURL(value: string): URL {
  let url: URL;
  try {
    url = new URL(value);
  } catch {
    throw new Error("The har parameter is not a valid URL.");
  }
  const loopbackHTTP = url.protocol === "http:" && isLoopbackHost(url.hostname);
  if (url.protocol !== "https:" && !loopbackHTTP) {
    throw new Error("Remote capture links must use HTTPS, except for loopback CLI links.");
  }
  if (url.username || url.password) throw new Error("Remote capture links must not contain URL credentials.");
 
  if (url.hostname === "gist.github.com") {
    const parts = url.pathname.split("/").filter(Boolean);
    Iif (parts.length < 2) throw new Error("The GitHub Gist URL must include an owner and gist ID.");
    url = new URL(`https://gist.githubusercontent.com/${parts[0]}/${parts[1]}/raw`);
  }
  url.hash = "";
  return url;
}
 
function isLoopbackHost(hostname: string): boolean {
  const normalized = hostname.toLowerCase();
  return normalized === "localhost"
    || normalized === "127.0.0.1"
    || normalized === "[::1]";
}
 
export async function fetchRemoteHar(value: string, signal?: AbortSignal): Promise<RemoteHar> {
  const url = remoteHarURL(value);
  let response: Response;
  try {
    response = await fetch(url, { signal, credentials: "omit", referrerPolicy: "no-referrer" });
  } catch (error) {
    if (error instanceof DOMException && error.name === "AbortError") throw error;
    throw new Error("The remote capture could not be downloaded. The host may not allow browser CORS requests.", { cause: error });
  }
  Iif (!response.ok) throw new Error(`The remote capture returned HTTP ${response.status}.`);
 
  const declaredSize = Number(response.headers.get("content-length"));
  Iif (Number.isFinite(declaredSize) && declaredSize > MAX_REMOTE_HAR_BYTES) {
    throw new Error("The remote capture is larger than the 100 MiB limit.");
  }
  const text = await readLimitedText(response);
 
  const pathName = new URL(response.url || url).pathname.split("/").filter(Boolean).at(-1);
  return { name: pathName && pathName !== "raw" ? pathName : `remote capture (${url.hostname})`, text, url: url.href };
}
 
async function readLimitedText(response: Response): Promise<string> {
  Iif (!response.body) return "";
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let bytes = 0;
  let text = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    bytes += value.byteLength;
    Iif (bytes > MAX_REMOTE_HAR_BYTES) {
      await reader.cancel();
      throw new Error("The remote capture is larger than the 100 MiB limit.");
    }
    text += decoder.decode(value, { stream: true });
  }
  return text + decoder.decode();
}