// @ts-nocheck import { file, gc, serve, ServeOptions } from "bun"; import { afterAll, afterEach, describe, expect, it, test } from "bun:test"; import { readFileSync } from "fs"; var port = 0; { const BodyMixin = [ Request.prototype.arrayBuffer, Request.prototype.blob, Request.prototype.text, Request.prototype.json, ]; const useRequestObjectValues = [true, false]; for (let RequestPrototypeMixin of BodyMixin) { for (let useRequestObject of useRequestObjectValues) { describe(`Request.prototoype.${RequestPrototypeMixin.name}() ${ useRequestObject ? "fetch(req)" : "fetch(url)" }`, () => { const inputFixture = [ [JSON.stringify("Hello World"), JSON.stringify("Hello World")], [JSON.stringify("Hello World 123"), Buffer.from(JSON.stringify("Hello World 123")).buffer], [JSON.stringify("Hello World 456"), Buffer.from(JSON.stringify("Hello World 456"))], [ JSON.stringify("EXTREMELY LONG VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(100)), Buffer.from( JSON.stringify("EXTREMELY LONG VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(100)), ), ], [ JSON.stringify("EXTREMELY LONG 🔥 UTF16 🔥 VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(100)), Buffer.from( JSON.stringify( "EXTREMELY LONG 🔥 UTF16 🔥 VERY LONG STRING WOW SO LONG YOU WONT BELIEVE IT! ".repeat(100), ), ), ], ]; for (const [name, input] of inputFixture) { test(`${name.slice(0, Math.min(name.length ?? name.byteLength, 64))}`, async () => { await runInServer( { async fetch(req) { var result = await RequestPrototypeMixin.call(req); if (RequestPrototypeMixin === Request.prototype.json) { result = JSON.stringify(result); } if (typeof result === "string") { expect(result.length).toBe(name.length); expect(result).toBe(name); } else if (result && result instanceof Blob) { expect(result.size).toBe(new TextEncoder().encode(name).byteLength); expect(await result.text()).toBe(name); } else { expect(result.byteLength).toBe(Buffer.from(input).byteLength); expect(Bun.SHA1.hash(result, "base64")).toBe(Bun.SHA1.hash(input, "base64")); } return new Response(result, { headers: req.headers, }); }, }, async url => { var response; // once, then batch of 5 if (useRequestObject) { response = await fetch( new Request({ body: input, method: "POST", url: url, headers: { "content-type": "text/plain", }, }), ); } else { response = await fetch(url, { body: input, method: "POST", headers: { "content-type": "text/plain", }, }); } expect(response.status).toBe(200); expect(response.headers.get("content-length")).toBe(String(Buffer.from(input).byteLength)); expect(response.headers.get("content-type")).toBe("text/plain"); expect(await response.text()).toBe(name); var promises = new Array(5); for (let i = 0; i < 5; i++) { if (useRequestObject) { promises[i] = await fetch( new Request({ body: input, method: "POST", url: url, headers: { "content-type": "text/plain", "x-counter": i, }, }), ); } else { promises[i] = await fetch(url, { body: input, method: "POST", headers: { "content-type": "text/plain", "x-counter": i, }, }); } } const results = await Promise.all(promises); for (let i = 0; i < 5; i++) { const response = results[i]; expect(response.status).toBe(200); expect(response.headers.get("content-length")).toBe(String(Buffer.from(input).byteLength)); expect(response.headers.get("content-type")).toBe("text/plain"); expect(response.headers.get("x-counter")).toBe(String(i)); expect(await response.text()).toBe(name); } }, ); }); } }); } } } var existingServer; async function runInServer(opts: ServeOptions, cb: (url: string) => void | Promise) { var server; const handler = { ...opts, port: port++, fetch(req) { try { return opts.fetch(req); } catch (e) { console.error(e.message); console.log(e.stack); throw e; } }, error(err) { console.log(err.message); console.log(err.stack); throw err; }, }; if (!existingServer) { existingServer = server = Bun.serve(handler); } else { server = existingServer; server.reload(handler); } try { await cb(`http://${server.hostname}:${server.port}`); } catch (e) { throw e; } finally { } } afterAll(() => { existingServer && existingServer.close(); existingServer = null; }); function fillRepeating(dstBuffer, start, end) { let len = dstBuffer.length, sLen = end - start, p = sLen; while (p < len) { if (p + sLen > len) sLen = len - p; dstBuffer.copyWithin(p, start, sLen); p += sLen; sLen <<= 1; } } function gc() { Bun.gc(true); } describe("reader", function () { try { // - empty // - 1 byte // - less than the InlineBlob limit // - multiple chunks // - backpressure for (let inputLength of [0, 1, 2, 12, 95, 1024, 1024 * 1024, 1024 * 1024 * 2]) { var bytes = new Uint8Array(inputLength); { const chunk = Math.min(bytes.length, 256); for (var i = 0; i < chunk; i++) { bytes[i] = 255 - i; } } if (bytes.length > 255) fillRepeating(bytes, 0, bytes.length); for (const huge_ of [ bytes, bytes.buffer, new DataView(bytes.buffer), new Int8Array(bytes), new Blob([bytes]), new Uint16Array(bytes), new Uint32Array(bytes), new Float64Array(bytes), new Int16Array(bytes), new Int32Array(bytes), new Float32Array(bytes), // make sure we handle subarray() as expected when reading // typed arrays from native code new Int16Array(bytes).subarray(1), new Int16Array(bytes).subarray(0, new Int16Array(bytes).byteLength - 1), new Int32Array(bytes).subarray(1), new Int32Array(bytes).subarray(0, new Int32Array(bytes).byteLength - 1), new Float32Array(bytes).subarray(1), new Float32Array(bytes).subarray(0, new Float32Array(bytes).byteLength - 1), new Int16Array(bytes).subarray(0, 1), new Int32Array(bytes).subarray(0, 1), new Float32Array(bytes).subarray(0, 1), ]) { gc(); const thisArray = huge_; it(`works with ${thisArray.constructor.name}(${ thisArray.byteLength ?? thisArray.size }:${inputLength}) via req.body.getReader() in chunks`, async () => { var huge = thisArray; var called = false; gc(); const expectedHash = huge instanceof Blob ? Bun.SHA1.hash(new Uint8Array(await huge.arrayBuffer()), "base64") : Bun.SHA1.hash(huge, "base64"); const expectedSize = huge instanceof Blob ? huge.size : huge.byteLength; const out = await runInServer( { async fetch(req) { try { expect(req.headers.get("x-custom")).toBe("hello"); expect(req.headers.get("content-type")).toBe("text/plain"); expect(req.headers.get("user-agent")).toBe(navigator.userAgent); gc(); expect(req.headers.get("x-custom")).toBe("hello"); expect(req.headers.get("content-type")).toBe("text/plain"); expect(req.headers.get("user-agent")).toBe(navigator.userAgent); var reader = req.body.getReader(); called = true; var buffers = []; while (true) { var { done, value } = await reader.read(); if (done) break; buffers.push(value); } const out = new Blob(buffers); gc(); expect(out.size).toBe(expectedSize); expect(Bun.SHA1.hash(await out.arrayBuffer(), "base64")).toBe(expectedHash); expect(req.headers.get("x-custom")).toBe("hello"); expect(req.headers.get("content-type")).toBe("text/plain"); expect(req.headers.get("user-agent")).toBe(navigator.userAgent); gc(); return new Response(out, { headers: req.headers, }); } catch (e) { console.error(e); throw e; } }, }, async url => { gc(); const response = await fetch(url, { body: huge, method: "POST", headers: { "content-type": "text/plain", "x-custom": "hello", "x-typed-array": thisArray.constructor.name, }, }); huge = undefined; expect(response.status).toBe(200); const response_body = new Uint8Array(await response.arrayBuffer()); expect(response_body.byteLength).toBe(expectedSize); expect(Bun.SHA1.hash(response_body, "base64")).toBe(expectedHash); gc(); expect(response.headers.get("content-type")).toBe("text/plain"); gc(); }, ); expect(called).toBe(true); gc(); return out; }); for (let isDirectStream of [true, false]) { const positions = ["begin", "end"]; const inner = thisArray => { for (let position of positions) { it(`streaming back ${thisArray.constructor.name}(${ thisArray.byteLength ?? thisArray.size }:${inputLength}) starting request.body.getReader() at ${position}`, async () => { var huge = thisArray; var called = false; gc(); const expectedHash = huge instanceof Blob ? Bun.SHA1.hash(new Uint8Array(await huge.arrayBuffer()), "base64") : Bun.SHA1.hash(huge, "base64"); const expectedSize = huge instanceof Blob ? huge.size : huge.byteLength; const out = await runInServer( { async fetch(req) { try { var reader; if (position === "begin") { reader = req.body.getReader(); } if (position === "end") { await 1; reader = req.body.getReader(); } expect(req.headers.get("x-custom")).toBe("hello"); expect(req.headers.get("content-type")).toBe("text/plain"); expect(req.headers.get("user-agent")).toBe(navigator.userAgent); gc(); expect(req.headers.get("x-custom")).toBe("hello"); expect(req.headers.get("content-type")).toBe("text/plain"); expect(req.headers.get("user-agent")).toBe(navigator.userAgent); const direct = { type: "direct", async pull(controller) { while (true) { const { done, value } = await reader.read(); if (done) { called = true; controller.end(); return; } controller.write(value); } }, }; const web = { async pull(controller) { while (true) { const { done, value } = await reader.read(); if (done) { called = true; controller.close(); return; } controller.enqueue(value); } }, }; return new Response(new ReadableStream(isDirectStream ? direct : web), { headers: req.headers, }); } catch (e) { console.error(e); throw e; } }, }, async url => { gc(); const response = await fetch(url, { body: huge, method: "POST", headers: { "content-type": "text/plain", "x-custom": "hello", "x-typed-array": thisArray.constructor.name, }, }); huge = undefined; expect(response.status).toBe(200); const response_body = new Uint8Array(await response.arrayBuffer()); expect(response_body.byteLength).toBe(expectedSize); expect(Bun.SHA1.hash(response_body, "base64")).toBe(expectedHash); gc(); if (!response.headers.has("content-type")) { console.error(Object.fromEntries(response.headers.entries())); } expect(response.headers.get("content-type")).toBe("text/plain"); gc(); }, ); expect(called).toBe(true); gc(); return out; }); } }; if (isDirectStream) { describe(" direct stream", () => inner(thisArray)); } else { describe("default stream", () => inner(thisArray)); } } } } } catch (e) { console.error(e); throw e; } });