It's just text
Under every fetch() is plain text over a TCP connection: a request line, headers, a blank line, then the body. Lines end in \r\n (CRLF). Once you can read and write it yourself, HTTP has no secrets.
Parsing and building
const REASONS = { 200: "OK", 201: "Created", 301: "Moved Permanently", 302: "Found", 400: "Bad Request", 404: "Not Found", 500: "Internal Server Error" };
function parseRequest(raw) {
const [head, ...rest] = raw.split("\r\n\r\n");
const [requestLine, ...headerLines] = head.split("\r\n");
const [method, path, version] = requestLine.split(" ");
const headers = {};
for (const line of headerLines) {
const i = line.indexOf(":");
headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
}
return { method, path, version, headers, body: rest.join("\r\n\r\n") };
}
function buildResponse(status, headers, body = "") {
const all = { ...headers, "Content-Length": String(new TextEncoder().encode(body).length) };
const lines = Object.entries(all).map(([k, v]) => `${k}: ${v}`);
return [`HTTP/1.1 ${status} ${REASONS[status] ?? "Unknown"}`, ...lines, "", body].join("\r\n");
}
function parseResponse(raw) {
const [head, ...rest] = raw.split("\r\n\r\n");
const [statusLine, ...headerLines] = head.split("\r\n");
const headers = {};
for (const line of headerLines) {
const i = line.indexOf(":");
headers[line.slice(0, i).trim().toLowerCase()] = line.slice(i + 1).trim();
}
return { status: Number(statusLine.split(" ")[1]), headers, body: rest.join("\r\n\r\n") };
}