Developer SDK

JavaScript examples

Install the SDK · View source on GitHub ↗

socket/browser/http.mjs

// A deliberately small GET-only HTTP/1.1 transport for the browser example.
// It supports Content-Length, chunked transfer encoding, and connection-close bodies.
// General HTTP clients also need redirects, cookies, compression and pooling policy.
export function parseResponse(data) {
  const text = new TextDecoder("latin1").decode(data);
  const split = text.indexOf("\r\n\r\n");
  if (split < 0) throw new Error("Incomplete HTTP response");
  const lines = text.slice(0, split).split("\r\n");
  const status = /^HTTP\/1\.[01] ([0-9]{3})(?: (.*))?$/.exec(lines.shift());
  if (!status) throw new Error("Invalid HTTP status line");
  const headers = {};
  for (const line of lines) {
    const colon = line.indexOf(":");
    if (colon <= 0) throw new Error("Invalid HTTP header");
    const name = line.slice(0, colon).toLowerCase();
    headers[name] = line.slice(colon + 1).trim();
  }
  let body = data.slice(split + 4);
  if (headers["transfer-encoding"]?.toLowerCase() === "chunked") {
    const chunks = []; let offset = 0;
    for (;;) {
      let end = offset;
      while (end + 1 < body.length && !(body[end] === 13 && body[end + 1] === 10)) end++;
      if (end + 1 >= body.length) throw new Error("Incomplete chunk header");
      const sizeText = new TextDecoder().decode(body.slice(offset, end)).split(";")[0];
      if (!/^[0-9a-f]+$/i.test(sizeText)) throw new Error("Invalid chunk size");
      const size = parseInt(sizeText, 16); offset = end + 2;
      if (!Number.isSafeInteger(size) || size > body.length - offset) throw new Error("Incomplete chunk");
      if (size === 0) break;
      chunks.push(body.slice(offset, offset + size)); offset += size;
      if (body[offset] !== 13 || body[offset + 1] !== 10) throw new Error("Invalid chunk terminator");
      offset += 2;
    }
    body = concatenate(chunks);
  } else if (headers["content-length"] !== undefined) {
    const length = Number(headers["content-length"]);
    if (!Number.isSafeInteger(length) || length < 0 || length > body.length) throw new Error("Incomplete HTTP body");
    body = body.slice(0, length);
  }
  return {status: Number(status[1]), statusText: status[2] || "", headers, data: new TextDecoder().decode(body)};
}

function concatenate(chunks) {
  const out = new Uint8Array(chunks.reduce((n, c) => n + c.length, 0)); let offset = 0;
  for (const chunk of chunks) {out.set(chunk, offset); offset += chunk.length;}
  return out;
}

export async function get(device, address, signal) {
  const url = new URL(address);
  if (url.protocol !== "https:" || url.username || url.password) throw new Error("This example accepts HTTPS URLs without embedded credentials.");
  const hostname = url.hostname.replace(/^\[|\]$/g, "");
  const target = (hostname.includes(":") ? "[" + hostname + "]" : hostname) + ":" + (url.port || 443);
  const conn = await device.dialTls("tcp", target, {serverName: hostname, nextProtos: ["http/1.1"]}, {timeoutMillis: 30000, signal});
  const abort = () => {void conn.close();};
  signal?.addEventListener("abort", abort, {once: true});
  try {
    signal?.throwIfAborted();
    await conn.setDeadline(Date.now() + 30000);
    await conn.write(new TextEncoder().encode("GET " + url.pathname + url.search + " HTTP/1.1\r\nHost: " + url.host + "\r\nConnection: close\r\nAccept-Encoding: identity\r\n\r\n"));
    const chunks = []; let total = 0;
    for (;;) {
      const data = await conn.read(); if (data === null) break;
      total += data.length; if (total > 1024 * 1024) throw new Error("Example response exceeds 1 MiB");
      chunks.push(data);
    }
    return parseResponse(concatenate(chunks));
  } finally {signal?.removeEventListener("abort", abort); await conn.close();}
}