diff options
author | 2023-10-17 14:10:25 -0700 | |
---|---|---|
committer | 2023-10-17 14:10:25 -0700 | |
commit | 7458b969c5d9971e89d187b687e1924e78da427e (patch) | |
tree | ee3dbf95c728cf407bf49a27826b541e9264a8bd /test/js/web/fetch | |
parent | d4a2c29131ec154f5e4db897d4deedab2002cbc4 (diff) | |
parent | e91436e5248d947b50f90b4a7402690be8a41f39 (diff) | |
download | bun-7458b969c5d9971e89d187b687e1924e78da427e.tar.gz bun-7458b969c5d9971e89d187b687e1924e78da427e.tar.zst bun-7458b969c5d9971e89d187b687e1924e78da427e.zip |
Merge branch 'main' into postinstall_3
Diffstat (limited to 'test/js/web/fetch')
-rw-r--r-- | test/js/web/fetch/blob.test.ts | 29 | ||||
-rw-r--r-- | test/js/web/fetch/body-stream.test.ts | 29 | ||||
-rw-r--r-- | test/js/web/fetch/fetch.stream.test.ts | 143 | ||||
-rw-r--r-- | test/js/web/fetch/fetch.test.ts | 425 | ||||
-rw-r--r-- | test/js/web/fetch/response.test.ts | 32 |
5 files changed, 656 insertions, 2 deletions
diff --git a/test/js/web/fetch/blob.test.ts b/test/js/web/fetch/blob.test.ts index ba44f8c1b..ea8d86a7a 100644 --- a/test/js/web/fetch/blob.test.ts +++ b/test/js/web/fetch/blob.test.ts @@ -1,6 +1,6 @@ import { test, expect } from "bun:test"; -test("Blob.slice", () => { +test("Blob.slice", async () => { const blob = new Blob(["Bun", "Foo"]); const b1 = blob.slice(0, 3, "Text/HTML"); expect(b1 instanceof Blob).toBeTruthy(); @@ -26,6 +26,33 @@ test("Blob.slice", () => { expect(blob.slice(null, "-123").size).toBe(6); expect(blob.slice(0, 10).size).toBe(blob.size); expect(blob.slice("text/plain;charset=utf-8").type).toBe("text/plain;charset=utf-8"); + + // test Blob.slice().slice(), issue#6252 + expect(await blob.slice(0, 4).slice(0, 3).text()).toBe("Bun"); + expect(await blob.slice(0, 4).slice(1, 3).text()).toBe("un"); + expect(await blob.slice(1, 4).slice(0, 3).text()).toBe("unF"); + expect(await blob.slice(1, 4).slice(1, 3).text()).toBe("nF"); + expect(await blob.slice(1, 4).slice(2, 3).text()).toBe("F"); + expect(await blob.slice(1, 4).slice(3, 3).text()).toBe(""); + expect(await blob.slice(1, 4).slice(4, 3).text()).toBe(""); + // test negative start + expect(await blob.slice(1, 4).slice(-1, 3).text()).toBe("F"); + expect(await blob.slice(1, 4).slice(-2, 3).text()).toBe("nF"); + expect(await blob.slice(1, 4).slice(-3, 3).text()).toBe("unF"); + expect(await blob.slice(1, 4).slice(-4, 3).text()).toBe("unF"); + expect(await blob.slice(1, 4).slice(-5, 3).text()).toBe("unF"); + expect(await blob.slice(-1, 4).slice(-1, 3).text()).toBe(""); + expect(await blob.slice(-2, 4).slice(-1, 3).text()).toBe(""); + expect(await blob.slice(-3, 4).slice(-1, 3).text()).toBe("F"); + expect(await blob.slice(-4, 4).slice(-1, 3).text()).toBe("F"); + expect(await blob.slice(-5, 4).slice(-1, 3).text()).toBe("F"); + expect(await blob.slice(-5, 4).slice(-2, 3).text()).toBe("nF"); + expect(await blob.slice(-5, 4).slice(-3, 3).text()).toBe("unF"); + expect(await blob.slice(-5, 4).slice(-4, 3).text()).toBe("unF"); + expect(await blob.slice(-4, 4).slice(-3, 3).text()).toBe("nF"); + expect(await blob.slice(-5, 4).slice(-4, 3).text()).toBe("unF"); + expect(await blob.slice(-3, 4).slice(-2, 3).text()).toBe("F"); + expect(await blob.slice(-blob.size, 4).slice(-blob.size, 3).text()).toBe("Bun"); }); test("new Blob", () => { diff --git a/test/js/web/fetch/body-stream.test.ts b/test/js/web/fetch/body-stream.test.ts index 8e2baf92a..8f7675528 100644 --- a/test/js/web/fetch/body-stream.test.ts +++ b/test/js/web/fetch/body-stream.test.ts @@ -13,6 +13,35 @@ var port = 0; ]; const useRequestObjectValues = [true, false]; + test("Should not crash when not returning a promise when stream is in progress", async () => { + var called = false; + await runInServer( + { + async fetch() { + var stream = new ReadableStream({ + type: "direct", + pull(controller) { + controller.write("hey"); + setTimeout(() => { + controller.end(); + }, 100); + }, + }); + + return new Response(stream); + }, + }, + async url => { + called = true; + expect(await fetch(url).then(res => res.text())).toContain( + "Welcome to Bun! To get started, return a Response object.", + ); + }, + ); + + expect(called).toBe(true); + }); + for (let RequestPrototypeMixin of BodyMixin) { for (let useRequestObject of useRequestObjectValues) { describe(`Request.prototoype.${RequestPrototypeMixin.name}() ${ diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts index 98271ee79..cc518e397 100644 --- a/test/js/web/fetch/fetch.stream.test.ts +++ b/test/js/web/fetch/fetch.stream.test.ts @@ -28,6 +28,142 @@ const smallText = Buffer.from("Hello".repeat(16)); const empty = Buffer.alloc(0); describe("fetch() with streaming", () => { + it(`should be able to fail properly when reading from readable stream`, async () => { + let server: Server | null = null; + try { + server = Bun.serve({ + port: 0, + async fetch(req) { + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue("Hello, World!"); + await Bun.sleep(1000); + controller.enqueue("Hello, World!"); + controller.close(); + }, + }), + { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }, + ); + }, + }); + + const server_url = `http://${server.hostname}:${server.port}`; + try { + const res = await fetch(server_url, { signal: AbortSignal.timeout(20) }); + const reader = res.body?.getReader(); + while (true) { + const { done } = await reader?.read(); + if (done) break; + } + expect(true).toBe("unreachable"); + } catch (err: any) { + if (err.name !== "TimeoutError") throw err; + expect(err.message).toBe("The operation timed out."); + } + } finally { + server?.stop(); + } + }); + + it(`should be locked after start buffering`, async () => { + let server: Server | null = null; + try { + server = Bun.serve({ + port: 0, + fetch(req) { + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.close(); + }, + }), + { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }, + ); + }, + }); + + const server_url = `http://${server.hostname}:${server.port}`; + const res = await fetch(server_url); + try { + const promise = res.text(); // start buffering + res.body?.getReader(); // get a reader + const result = await promise; // should throw the right error + expect(result).toBe("unreachable"); + } catch (err: any) { + if (err.name !== "TypeError") throw err; + expect(err.message).toBe("ReadableStream is locked"); + } + } finally { + server?.stop(); + } + }); + + it(`should be locked after start buffering when calling getReader`, async () => { + let server: Server | null = null; + try { + server = Bun.serve({ + port: 0, + fetch(req) { + return new Response( + new ReadableStream({ + async start(controller) { + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.enqueue("Hello, World!"); + await Bun.sleep(10); + controller.close(); + }, + }), + { + status: 200, + headers: { + "Content-Type": "text/plain", + }, + }, + ); + }, + }); + + const server_url = `http://${server.hostname}:${server.port}`; + const res = await fetch(server_url); + try { + const body = res.body as ReadableStream<Uint8Array>; + const promise = res.text(); // start buffering + body.getReader(); // get a reader + const result = await promise; // should throw the right error + expect(result).toBe("unreachable"); + } catch (err: any) { + if (err.name !== "TypeError") throw err; + expect(err.message).toBe("ReadableStream is locked"); + } + } finally { + server?.stop(); + } + }); + it("can deflate with and without headers #4478", async () => { let server: Server | null = null; try { @@ -77,7 +213,12 @@ describe("fetch() with streaming", () => { .listen(0); const address = server.address() as AddressInfo; - const url = `http://${address.address}:${address.port}`; + let url; + if (address.family == "IPv4") { + url = `http://${address.address}:${address.port}`; + } else { + url = `http://[${address.address}]:${address.port}`; + } async function getRequestLen(url: string) { const response = await fetch(url); const hasBody = response.body; diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts index aa44ee76a..3ed20345b 100644 --- a/test/js/web/fetch/fetch.test.ts +++ b/test/js/web/fetch/fetch.test.ts @@ -3,8 +3,10 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, beforeEach } from import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs"; import { mkfifo } from "mkfifo"; import { tmpdir } from "os"; +import { gzipSync } from "zlib"; import { join } from "path"; import { gc, withoutAggressiveGC, gcTick } from "harness"; +import net from "net"; const tmp_dir = mkdtempSync(join(realpathSync(tmpdir()), "fetch.test")); @@ -93,6 +95,30 @@ describe("fetch data urls", () => { expect(blob.type).toBe("text/plain;charset=utf-8"); expect(blob.text()).resolves.toBe("helloworld!"); }); + it("unstrict parsing of invalid URL characters", async () => { + var url = "data:application/json,{%7B%7D}"; + var res = await fetch(url); + expect(res.status).toBe(200); + expect(res.statusText).toBe("OK"); + expect(res.ok).toBe(true); + + var blob = await res.blob(); + expect(blob.size).toBe(4); + expect(blob.type).toBe("application/json;charset=utf-8"); + expect(blob.text()).resolves.toBe("{{}}"); + }); + it("unstrict parsing of double percent characters", async () => { + var url = "data:application/json,{%%7B%7D%%}%%"; + var res = await fetch(url); + expect(res.status).toBe(200); + expect(res.statusText).toBe("OK"); + expect(res.ok).toBe(true); + + var blob = await res.blob(); + expect(blob.size).toBe(9); + expect(blob.type).toBe("application/json;charset=utf-8"); + expect(blob.text()).resolves.toBe("{%{}%%}%%"); + }); it("data url (invalid)", async () => { var url = "data:Hello%2C%20World!"; expect(async () => { @@ -321,6 +347,27 @@ describe("Headers", () => { expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly"]); }); + it("presence of content-encoding header(issue #5668)", async () => { + startServer({ + fetch(req) { + const content = gzipSync(JSON.stringify({ message: "Hello world" })); + return new Response(content, { + status: 200, + headers: { + "content-encoding": "gzip", + "content-type": "application/json", + }, + }); + }, + }); + const result = await fetch(`http://${server.hostname}:${server.port}/`); + const value = result.headers.get("content-encoding"); + const body = await result.json(); + expect(value).toBe("gzip"); + expect(body).toBeDefined(); + expect(body.message).toBe("Hello world"); + }); + it(".getSetCookie() with array", () => { const headers = new Headers([ ["content-length", "123"], @@ -1216,6 +1263,16 @@ describe("Request", () => { expect(req.signal.aborted).toBe(true); }); + it("copies method (#6144)", () => { + const request = new Request("http://localhost:1337/test", { + method: "POST", + }); + const new_req = new Request(request, { + body: JSON.stringify({ message: "Hello world" }), + }); + expect(new_req.method).toBe("POST"); + }); + it("cloned signal", async () => { gc(); const controller = new AbortController(); @@ -1391,3 +1448,371 @@ it("cloned response headers are independent after accessing", () => { cloned.headers.set("content-type", "text/plain"); expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8"); }); + +it("should work with http 100 continue", async () => { + let server: net.Server | undefined; + try { + server = net.createServer(socket => { + socket.on("data", data => { + const lines = data.toString().split("\r\n"); + for (const line of lines) { + if (line.length == 0) { + socket.write("HTTP/1.1 100 Continue\r\n\r\n"); + socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!"); + break; + } + } + }); + }); + + const { promise: start, resolve } = Promise.withResolvers(); + server.listen(8080, resolve); + + await start; + + const address = server.address() as net.AddressInfo; + const result = await fetch(`http://localhost:${address.port}`).then(r => r.text()); + expect(result).toBe("Hello, World!"); + } finally { + server?.close(); + } +}); + +it("should work with http 100 continue on the same buffer", async () => { + let server: net.Server | undefined; + try { + server = net.createServer(socket => { + socket.on("data", data => { + const lines = data.toString().split("\r\n"); + for (const line of lines) { + if (line.length == 0) { + socket.write( + "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!", + ); + break; + } + } + }); + }); + + const { promise: start, resolve } = Promise.withResolvers(); + server.listen(8080, resolve); + + await start; + + const address = server.address() as net.AddressInfo; + const result = await fetch(`http://localhost:${address.port}`).then(r => r.text()); + expect(result).toBe("Hello, World!"); + } finally { + server?.close(); + } +}); + +describe("should strip headers", () => { + it("status code 303", async () => { + const server = Bun.serve({ + port: 0, + async fetch(request: Request) { + if (request.url.endsWith("/redirect")) { + return new Response("hello", { + headers: { + ...request.headers, + "Location": "/redirected", + }, + status: 303, + }); + } + + return new Response("hello", { + headers: request.headers, + }); + }, + }); + + const { headers, url, redirected } = await fetch(`http://${server.hostname}:${server.port}/redirect`, { + method: "POST", + headers: { + "I-Am-Here": "yes", + "Content-Language": "This should be stripped", + }, + }); + + expect(headers.get("I-Am-Here")).toBe("yes"); + expect(headers.get("Content-Language")).toBeNull(); + expect(url).toEndWith("/redirected"); + expect(redirected).toBe(true); + server.stop(true); + }); + + it("cross-origin status code 302", async () => { + const server1 = Bun.serve({ + port: 0, + async fetch(request: Request) { + if (request.url.endsWith("/redirect")) { + return new Response("hello", { + headers: { + ...request.headers, + "Location": `http://${server2.hostname}:${server2.port}/redirected`, + }, + status: 302, + }); + } + + return new Response("hello", { + headers: request.headers, + }); + }, + }); + + const server2 = Bun.serve({ + port: 0, + async fetch(request: Request, server) { + if (request.url.endsWith("/redirect")) { + return new Response("hello", { + headers: { + ...request.headers, + "Location": `http://${server.hostname}:${server.port}/redirected`, + }, + status: 302, + }); + } + + return new Response("hello", { + headers: request.headers, + }); + }, + }); + + const { headers, url, redirected } = await fetch(`http://${server1.hostname}:${server1.port}/redirect`, { + method: "GET", + headers: { + "Authorization": "yes", + }, + }); + + expect(headers.get("Authorization")).toBeNull(); + expect(url).toEndWith("/redirected"); + expect(redirected).toBe(true); + server1.stop(true); + server2.stop(true); + }); +}); + +it("same-origin status code 302 should not strip headers", async () => { + const server = Bun.serve({ + port: 0, + async fetch(request: Request, server) { + if (request.url.endsWith("/redirect")) { + return new Response("hello", { + headers: { + ...request.headers, + "Location": `http://${server.hostname}:${server.port}/redirected`, + }, + status: 302, + }); + } + + return new Response("hello", { + headers: request.headers, + }); + }, + }); + + const { headers, url, redirected } = await fetch(`http://${server.hostname}:${server.port}/redirect`, { + method: "GET", + headers: { + "Authorization": "yes", + }, + }); + + expect(headers.get("Authorization")).toEqual("yes"); + expect(url).toEndWith("/redirected"); + expect(redirected).toBe(true); + server.stop(true); +}); + +describe("should handle relative location in the redirect, issue#5635", () => { + var server: Server; + beforeAll(async () => { + server = Bun.serve({ + port: 0, + async fetch(request: Request) { + return new Response("Not Found", { + status: 404, + }); + }, + }); + }); + afterAll(() => { + server.stop(true); + }); + + it.each([ + ["/a/b", "/c", "/c"], + ["/a/b", "c", "/a/c"], + ["/a/b", "/c/d", "/c/d"], + ["/a/b", "c/d", "/a/c/d"], + ["/a/b", "../c", "/c"], + ["/a/b", "../c/d", "/c/d"], + ["/a/b", "../../../c", "/c"], + // slash + ["/a/b/", "/c", "/c"], + ["/a/b/", "c", "/a/b/c"], + ["/a/b/", "/c/d", "/c/d"], + ["/a/b/", "c/d", "/a/b/c/d"], + ["/a/b/", "../c", "/a/c"], + ["/a/b/", "../c/d", "/a/c/d"], + ["/a/b/", "../../../c", "/c"], + ])("('%s', '%s')", async (pathname, location, expected) => { + server.reload({ + async fetch(request: Request) { + const url = new URL(request.url); + if (url.pathname == pathname) { + return new Response("redirecting", { + headers: { + "Location": location, + }, + status: 302, + }); + } else if (url.pathname == expected) { + return new Response("Fine."); + } + return new Response("Not Found", { + status: 404, + }); + }, + }); + + const resp = await fetch(`http://${server.hostname}:${server.port}${pathname}`); + expect(resp.redirected).toBe(true); + expect(new URL(resp.url).pathname).toStrictEqual(expected); + expect(resp.status).toBe(200); + expect(await resp.text()).toBe("Fine."); + }); +}); + +it("should throw RedirectURLTooLong when location is too long", async () => { + const server = Bun.serve({ + port: 0, + async fetch(request: Request) { + gc(); + const url = new URL(request.url); + if (url.pathname == "/redirect") { + return new Response("redirecting", { + headers: { + "Location": "B".repeat(8193), + }, + status: 302, + }); + } + return new Response("Not Found", { + status: 404, + }); + }, + }); + + let err = undefined; + try { + gc(); + const resp = await fetch(`http://${server.hostname}:${server.port}/redirect`); + } catch (error) { + gc(); + err = error; + } + expect(err).not.toBeUndefined(); + expect(err).toBeInstanceOf(Error); + expect(err.code).toStrictEqual("RedirectURLTooLong"); + server.stop(true); +}); + +it("304 not modified with missing content-length does not cause a request timeout", async () => { + const server = await Bun.listen({ + socket: { + open(socket) { + socket.write("HTTP/1.1 304 Not Modified\r\n\r\n"); + socket.flush(); + setTimeout(() => { + socket.end(); + }, 9999).unref(); + }, + data() {}, + close() {}, + }, + port: 0, + hostname: "localhost", + }); + + const response = await fetch(`http://${server.hostname}:${server.port}/`); + expect(response.status).toBe(304); + expect(await response.arrayBuffer()).toHaveLength(0); + server.stop(true); +}); + +it("304 not modified with missing content-length and connection close does not cause a request timeout", async () => { + const server = await Bun.listen({ + socket: { + open(socket) { + socket.write("HTTP/1.1 304 Not Modified\r\nConnection: close\r\n\r\n"); + socket.flush(); + setTimeout(() => { + socket.end(); + }, 9999).unref(); + }, + data() {}, + close() {}, + }, + port: 0, + hostname: "localhost", + }); + + const response = await fetch(`http://${server.hostname}:${server.port}/`); + expect(response.status).toBe(304); + expect(await response.arrayBuffer()).toHaveLength(0); + server.stop(true); +}); + +it("304 not modified with content-length 0 and connection close does not cause a request timeout", async () => { + const server = await Bun.listen({ + socket: { + open(socket) { + socket.write("HTTP/1.1 304 Not Modified\r\nConnection: close\r\nContent-Length: 0\r\n\r\n"); + socket.flush(); + setTimeout(() => { + socket.end(); + }, 9999).unref(); + }, + data() {}, + close() {}, + }, + port: 0, + hostname: "localhost", + }); + + const response = await fetch(`http://${server.hostname}:${server.port}/`); + expect(response.status).toBe(304); + expect(await response.arrayBuffer()).toHaveLength(0); + server.stop(true); +}); + +it("304 not modified with 0 content-length does not cause a request timeout", async () => { + const server = await Bun.listen({ + socket: { + open(socket) { + socket.write("HTTP/1.1 304 Not Modified\r\nContent-Length: 0\r\n\r\n"); + socket.flush(); + setTimeout(() => { + socket.end(); + }, 9999).unref(); + }, + data() {}, + close() {}, + }, + port: 0, + hostname: "localhost", + }); + + const response = await fetch(`http://${server.hostname}:${server.port}/`); + expect(response.status).toBe(304); + expect(await response.arrayBuffer()).toHaveLength(0); + server.stop(true); +}); diff --git a/test/js/web/fetch/response.test.ts b/test/js/web/fetch/response.test.ts new file mode 100644 index 000000000..ddfcd70f7 --- /dev/null +++ b/test/js/web/fetch/response.test.ts @@ -0,0 +1,32 @@ +import { describe, test, expect } from "bun:test"; + +test("zero args returns an otherwise empty 200 response", () => { + const response = new Response(); + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); +}); + +test("1-arg form returns a 200 response", () => { + const response = new Response("body text"); + + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); +}); + +describe("2-arg form", () => { + test("can fill in status/statusText, and it works", () => { + const response = new Response("body text", { + status: 202, + statusText: "Accepted.", + }); + + expect(response.status).toBe(202); + expect(response.statusText).toBe("Accepted."); + }); + test('empty object continues to return 200/""', () => { + const response = new Response("body text", {}); + + expect(response.status).toBe(200); + expect(response.statusText).toBe(""); + }); +}); |