diff --git a/src/puter-js/src/modules/networking/requests.js b/src/puter-js/src/modules/networking/requests.js index f3c8245d3..be78ebd52 100644 --- a/src/puter-js/src/modules/networking/requests.js +++ b/src/puter-js/src/modules/networking/requests.js @@ -33,188 +33,211 @@ function parseHTTPHead(head) { // TODO optional redirect handling export function pFetch(...args) { - return new Promise((res, rej) => { - const reqObj = new Request(...args); - const parsedURL = new URL(reqObj.url); - let headers = new Headers(reqObj.headers); // Make a headers object we can modify + return new Promise(async (res, rej) => { + try { + const reqObj = new Request(...args); + const parsedURL = new URL(reqObj.url); + let headers = new Headers(reqObj.headers); // Make a headers object we can modify - // Socket creation: regular for HTTP, TLS for https - let socket; - if (parsedURL.protocol === "http:") { - socket = new puter.net.Socket( - parsedURL.hostname, - parsedURL.port || 80, - ); - } else if (parsedURL.protocol === "https:") { - socket = new puter.net.tls.TLSSocket( - parsedURL.hostname, - parsedURL.port || 443, - ); - } else { - rej( - `Failed to fetch. URL scheme "${parsedURL.protocol}" is not supported.`, - ); - } + // Socket creation: regular for HTTP, TLS for https + let socket; + if (parsedURL.protocol === "http:") { + socket = new puter.net.Socket( + parsedURL.hostname, + parsedURL.port || 80, + ); + } else if (parsedURL.protocol === "https:") { + socket = new puter.net.tls.TLSSocket( + parsedURL.hostname, + parsedURL.port || 443, + ); + } else { + rej( + `Failed to fetch. URL scheme "${parsedURL.protocol}" is not supported.`, + ); + } - // Sending default UA - if (!headers.get("user-agent")) { - headers.set("user-agent", navigator.userAgent); - } + // Sending default UA + if (!headers.get("user-agent")) { + headers.set("user-agent", navigator.userAgent); + } - let reqHead = `${reqObj.method} ${parsedURL.pathname}${parsedURL.search} HTTP/1.1\r\nHost: ${parsedURL.host}\r\nConnection: close\r\n`; - for (const [key, value] of headers) { - reqHead += `${key}: ${value}\r\n`; - } - reqHead += "\r\n"; + let reqHead = `${reqObj.method} ${parsedURL.pathname}${parsedURL.search} HTTP/1.1\r\nHost: ${parsedURL.host}\r\nConnection: close\r\n`; + for (const [key, value] of headers) { + reqHead += `${key}: ${value}\r\n`; + } + let requestBody; + if (reqObj.body) { + requestBody = new Uint8Array(await reqObj.arrayBuffer()); + // If we have a body, we need to set the content length + if (!headers.has("content-length")) { + headers.set("content-length", requestBody.length); + } else if ( + headers.get("content-length") !== String(requestBody.length) + ) { + return rej( + "Content-Length header does not match the body length. Please check your request.", + ); + } + reqHead += `Content-Length: ${requestBody.length}\r\n`; + } - socket.on("open", () => { - socket.write(reqHead); - }); - const decoder = new TextDecoder(); - let responseHead = ""; - let dataOffset = -1; - const fullDataParts = []; - let responseReturned = false; - let contentLength = -1; - let ingestedContent = 0; - let chunkedTransfer = false; - let currentChunkLeft = -1; - let buffer = new Uint8Array(0); + reqHead += "\r\n"; - const outStream = new ReadableStream({ - start(controller) { - // This is annoyingly long - function parseIncomingChunk(data) { - // append new data to our rolling buffer - const tmp = new Uint8Array(buffer.length + data.length); - tmp.set(buffer, 0); - tmp.set(data, buffer.length); - buffer = tmp; + socket.on("open", async () => { + socket.write(reqHead); // Send headers + if (requestBody) { + console.log("Sending body", requestBody); + socket.write(requestBody); // Send body if present + } + }); + const decoder = new TextDecoder(); + let responseHead = ""; + let dataOffset = -1; + const fullDataParts = []; + let responseReturned = false; + let contentLength = -1; + let ingestedContent = 0; + let chunkedTransfer = false; + let currentChunkLeft = -1; + let buffer = new Uint8Array(0); - // pull out as many complete chunks (or headers) as we can - while (true) { - if (currentChunkLeft > 0) { - // we’re in the middle of reading a chunk body - // need size + 2 bytes (for trailing \r\n) - if (buffer.length >= currentChunkLeft + 2) { - // full body + CRLF available - const chunk = buffer.slice(0, currentChunkLeft); - controller.enqueue(chunk); + const outStream = new ReadableStream({ + start(controller) { + // This is annoyingly long + function parseIncomingChunk(data) { + // append new data to our rolling buffer + const tmp = new Uint8Array(buffer.length + data.length); + tmp.set(buffer, 0); + tmp.set(data, buffer.length); + buffer = tmp; - // strip body + CRLF and reset for next header - buffer = buffer.slice(currentChunkLeft + 2); - currentChunkLeft = 0; + // pull out as many complete chunks (or headers) as we can + while (true) { + if (currentChunkLeft > 0) { + // we’re in the middle of reading a chunk body + // need size + 2 bytes (for trailing \r\n) + if (buffer.length >= currentChunkLeft + 2) { + // full body + CRLF available + const chunk = buffer.slice(0, currentChunkLeft); + controller.enqueue(chunk); + + // strip body + CRLF and reset for next header + buffer = buffer.slice(currentChunkLeft + 2); + currentChunkLeft = 0; + } else { + // only a partial body available + controller.enqueue(buffer); + currentChunkLeft -= buffer.length; + buffer = new Uint8Array(0); + break; // wait for more data + } } else { - // only a partial body available - controller.enqueue(buffer); - currentChunkLeft -= buffer.length; - buffer = new Uint8Array(0); - break; // wait for more data - } - } else { - // we need to parse the next size line - // find the first "\r\n" - let idx = -1; - for (let i = 0; i + 1 < buffer.length; i++) { - if ( - buffer[i] === 0x0d && - buffer[i + 1] === 0x0a - ) { - idx = i; + // we need to parse the next size line + // find the first "\r\n" + let idx = -1; + for (let i = 0; i + 1 < buffer.length; i++) { + if ( + buffer[i] === 0x0d && + buffer[i + 1] === 0x0a + ) { + idx = i; + break; + } + } + if (idx < 0) { + // we don’t yet have a full size line break; } - } - if (idx < 0) { - // we don’t yet have a full size line - break; - } - // decode just the size line as ASCII hex - const sizeText = decoder - .decode(buffer.slice(0, idx)) - .trim(); - currentChunkLeft = parseInt(sizeText, 16); - if (isNaN(currentChunkLeft)) { - controller.error( - "Invalid chunk length from server", - ); - } - // strip off the size line + CRLF - buffer = buffer.slice(idx + 2); + // decode just the size line as ASCII hex + const sizeText = decoder + .decode(buffer.slice(0, idx)) + .trim(); + currentChunkLeft = parseInt(sizeText, 16); + if (isNaN(currentChunkLeft)) { + controller.error( + "Invalid chunk length from server", + ); + } + // strip off the size line + CRLF + buffer = buffer.slice(idx + 2); - // zero-length => end of stream - if (currentChunkLeft === 0) { + // zero-length => end of stream + if (currentChunkLeft === 0) { + responseReturned = true; + controller.close(); + return; + } + } + } + } + socket.on("data", (data) => { + // Dataoffset is set to another value once head is returned, its safe to assume all remaining data is body + if (dataOffset !== -1 && !chunkedTransfer) { + controller.enqueue(data); + ingestedContent += data.length; + } + + // We dont have the full responseHead yet + if (dataOffset === -1) { + fullDataParts.push(data); + responseHead += decoder.decode(data, { stream: true }); + } + if (chunkedTransfer) { + parseIncomingChunk(data); + } + + // See if we have the HEAD of an HTTP/1.1 yet + if (responseHead.indexOf("\r\n\r\n") !== -1) { + dataOffset = responseHead.indexOf("\r\n\r\n"); + responseHead = responseHead.slice(0, dataOffset); + const parsedHead = parseHTTPHead(responseHead); + contentLength = Number( + parsedHead.headers.get("content-length"), + ); + chunkedTransfer = + parsedHead.headers.get("transfer-encoding") === + "chunked"; + // Return initial response object + res(new Response(outStream, parsedHead)); + + const residualBody = mergeUint8Arrays( + ...fullDataParts, + ).slice(dataOffset + 4); + if (!chunkedTransfer) { + // Add any content we have but isn't part of the head into the body stream + ingestedContent += residualBody.length; + controller.enqueue(residualBody); + } else { + parseIncomingChunk(residualBody); + } + } + + if ( + contentLength !== -1 && + ingestedContent === contentLength && + !chunkedTransfer + ) { + // Work around for the close bug for compliant HTTP/1.1 servers + if (!responseReturned) { responseReturned = true; controller.close(); - return; } } - } - } - socket.on("data", (data) => { - // Dataoffset is set to another value once head is returned, its safe to assume all remaining data is body - if (dataOffset !== -1 && !chunkedTransfer) { - controller.enqueue(data); - ingestedContent += data.length; - } - - // We dont have the full responseHead yet - if (dataOffset === -1) { - fullDataParts.push(data); - responseHead += decoder.decode(data, { stream: true }); - } - if (chunkedTransfer) { - parseIncomingChunk(data); - } - - // See if we have the HEAD of an HTTP/1.1 yet - if (responseHead.indexOf("\r\n\r\n") !== -1) { - dataOffset = responseHead.indexOf("\r\n\r\n"); - responseHead = responseHead.slice(0, dataOffset); - const parsedHead = parseHTTPHead(responseHead); - contentLength = Number( - parsedHead.headers.get("content-length"), - ); - chunkedTransfer = - parsedHead.headers.get("transfer-encoding") === - "chunked"; - // Return initial response object - res(new Response(outStream, parsedHead)); - - const residualBody = mergeUint8Arrays( - ...fullDataParts, - ).slice(dataOffset + 4); - if (!chunkedTransfer) { - // Add any content we have but isn't part of the head into the body stream - ingestedContent += residualBody.length; - controller.enqueue(residualBody); - } else { - parseIncomingChunk(residualBody); - } - } - - if ( - contentLength !== -1 && - ingestedContent === contentLength && - !chunkedTransfer - ) { - // Work around for the close bug for compliant HTTP/1.1 servers + }); + socket.on("close", () => { if (!responseReturned) { responseReturned = true; controller.close(); } - } - }); - socket.on("close", () => { - if (!responseReturned) { - responseReturned = true; - controller.close(); - } - }); - socket.on("error", (reason) => { - rej("Socket errored with the following reason: " + reason); - }); - }, - }); - }); + }); + socket.on("error", (reason) => { + rej("Socket errored with the following reason: " + reason); + }); + }, + }); + } catch (e) { + rej(e); + }}); }