describe("fetch", () => {
it("should fetch IP from test server", async () => {
const url = new URL("/ip", TEST_SERVER_URL);
const response = await fetch(url);
expect(response instanceof Response).toBe(true);
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json");
const data = await response.json();
console.log(data);
expect(typeof data.origin).toBe("string");
expect(data.origin).toBe("127.0.0.1");
});
it("should send and receive custom headers", async () => {
const url = new URL("/headers", TEST_SERVER_URL);
const customHeaders = {
"X-Custom-Header": "custom value",
"X-Test-Header": "test value",
"User-Agent": "RongJS Test Client",
};
const response = await fetch(url, {
headers: customHeaders,
});
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json");
const data = await response.json();
console.log("Received headers:", data);
for (const [key, value] of Object.entries(customHeaders)) {
assert.equal(data[key.toLowerCase()], value);
}
});
it("should handle gzipped response", async () => {
const url = new URL("/gzip", TEST_SERVER_URL);
const response = await fetch(url);
expect(response instanceof Response).toBe(true);
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
expect(response.headers.get("content-type")).toBe("application/json");
expect(response.headers.get("content-encoding")).toBe("gzip");
const data = await response.json();
expect(data.gzipped).toBe(true);
expect(typeof data.method).toBe("string");
expect(data.method).toBe("GET");
});
it("should handle network errors", async () => {
try {
await fetch("https://invalid.example.com");
} catch (error) {
console.log(error);
expect(error instanceof TypeError).toBe(true);
}
});
it("should not allow multiple body reads", async () => {
const url = new URL("/ip", TEST_SERVER_URL);
const response = await fetch(url);
const a = await response.json();
expect(typeof a.origin).toBe("string");
let threw = false;
try {
await response.text();
} catch (err) {
threw = true;
expect(err instanceof TypeError).toBe(true);
expect(/body used already/.test(String(err))).toBe(true);
}
if (!threw) {
throw new Error("Expected second body read to fail");
}
});
it("should stream upload with ReadableStream body", async () => {
const url = new URL("/upload", TEST_SERVER_URL);
const total = 100 * 1024 + 5;
const chunk = new Uint8Array(4096).fill(0x61); let sent = 0;
const rs = new ReadableStream({
start(controller) {
(async () => {
while (sent < total) {
const n = Math.min(chunk.length, total - sent);
controller.enqueue(chunk.subarray(0, n));
sent += n;
await new Promise((r) => setTimeout(r, 1));
}
controller.close();
})();
},
});
const response = await fetch(url, { method: "PUT", body: rs });
expect(response.ok).toBe(true);
const data = await response.json();
expect(data.received).toBe(total);
});
it("should read streaming response via Response.body", async () => {
const url = new URL("/large", TEST_SERVER_URL);
const response = await fetch(url);
expect(response instanceof Response).toBe(true);
const body = response.body;
const decoder = new TextDecoder();
let seenStart = false;
let seenEnd = false;
let total = 0;
let text = "";
for await (const chunk of body) {
total += chunk.byteLength;
text += decoder.decode(chunk);
if (text.includes("chunk_0000")) seenStart = true;
if (text.includes("chunk_0099")) seenEnd = true;
}
expect(total > 0).toBe(true);
expect(seenStart).toBe(true);
expect(seenEnd).toBe(true);
});
it("should download to file via WritableStream", async () => {
const tmpDir = `${WORKSPACE_ROOT}/target/test-tmp`;
try {
await Rong.mkdir(tmpDir, { recursive: true });
} catch {}
const outPath = `${tmpDir}/fetch_download_stream.txt`;
const file = await Rong.open(outPath, {
write: true,
create: true,
truncate: true,
});
const ws = file.writable;
const writer = ws.getWriter();
const url = new URL("/large", TEST_SERVER_URL);
const response = await fetch(url);
const body = response.body;
const reader = body.getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
await writer.write(value);
}
await writer.close();
await file.close();
const data = new Uint8Array(await Rong.readFile(outPath));
const text = new TextDecoder().decode(data);
assert(text.includes("chunk_0000"));
assert(text.includes("chunk_0099"));
await Rong.remove(outPath);
});
it("should pipeTo file.writable (download)", async () => {
const tmpDir = `${WORKSPACE_ROOT}/target/test-tmp`;
try {
await Rong.mkdir(tmpDir, { recursive: true });
} catch {}
const outPath = `${tmpDir}/fetch_download_pipeTo.txt`;
const file = await Rong.open(outPath, {
write: true,
create: true,
truncate: true,
});
const ws = file.writable;
const url = new URL("/large", TEST_SERVER_URL);
const response = await fetch(url);
await response.body.pipeTo(ws);
await file.close();
const data = new Uint8Array(await Rong.readFile(outPath));
const text = new TextDecoder().decode(data);
assert(text.includes("chunk_0000"));
assert(text.includes("chunk_0099"));
await Rong.remove(outPath);
});
describe("redirect", () => {
it("should follow redirects by default", async () => {
const url = new URL("/redirect?n=2", TEST_SERVER_URL);
const response = await fetch(url);
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
expect(response.redirected).toBe(true);
const data = await response.json();
expect(data.origin).toBe("127.0.0.1");
});
it("should handle redirect: manual", async () => {
const url = new URL("/redirect?n=1", TEST_SERVER_URL);
const response = await fetch(url, { redirect: "manual" });
expect(response.type).toBe("basic");
expect(response.status).toBe(302);
expect(response.headers.has("location")).toBe(true);
});
it("should handle redirect: error", async () => {
const url = new URL("/redirect?n=1", TEST_SERVER_URL);
try {
await fetch(url, { redirect: "error" });
throw new Error("Should have thrown");
} catch (e) {
expect(e instanceof TypeError).toBe(true);
}
});
it("should change POST/PUT to GET on 303 redirect", async () => {
const url = new URL("/303", TEST_SERVER_URL);
const response = await fetch(url, { method: "PUT" });
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
const data = await response.json();
expect(data.origin).toBe("127.0.0.1");
});
it("should limit redirects", async () => {
const url = new URL("/redirect?n=25", TEST_SERVER_URL);
try {
await fetch(url);
throw new Error("Should have thrown");
} catch (e) {
expect(e.name).toBe("NetworkError");
}
});
});
});
describe("Abort to fetch", () => {
let controller;
let signal;
beforeEach(() => {
controller = new AbortController();
signal = controller.signal;
});
const waitForFetchStart = async (startFlag) => {
while (!startFlag) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
};
it("should abort fetch request", async () => {
let fetchStarted = false;
const fetchPromise = (async () => {
fetchStarted = true;
return await fetch(`${TEST_SERVER_URL}/delay`, { signal });
})();
await waitForFetchStart(fetchStarted);
await new Promise((resolve) => setTimeout(resolve, 50));
controller.abort();
try {
await fetchPromise;
assert.fail("fetch should have been aborted");
} catch (error) {
assert.ok(error instanceof DOMException);
assert.equal(error.name, "AbortError");
console.log("##Got:", error.name);
}
});
it("should abort during response body read", async () => {
const response = await fetch(`${TEST_SERVER_URL}/large`, { signal });
const readPromise = response.arrayBuffer();
let abortCaught = false;
await new Promise((resolve) => setTimeout(resolve, 10));
controller.abort();
try {
await readPromise;
console.log("##Body read completed without being aborted");
} catch (error) {
assert.ok(error instanceof DOMException);
assert.equal(error.name, "AbortError");
abortCaught = true; console.log("##Got: ", error.name);
}
if (!abortCaught) {
throw new Error("Body read was not aborted as expected");
}
});
it("should abort with custom reason", async () => {
const reason = new Error("Custom abort reason");
assert.equal(signal.aborted, false);
assert.equal(signal.reason, undefined);
let fetchStarted = false;
const fetchPromise = (async () => {
fetchStarted = true;
return await fetch(`${TEST_SERVER_URL}/delay`, { signal });
})();
await waitForFetchStart(fetchStarted);
await new Promise((resolve) => setTimeout(resolve, 50));
controller.abort(reason);
assert.equal(signal.aborted, true);
assert.equal(signal.reason, reason);
try {
await fetchPromise;
assert.fail("fetch should have been aborted");
} catch (error) {
assert.equal(error, reason);
console.log("##Got: ", error);
}
});
it("should abort immediately if signal is already aborted", async () => {
assert.equal(signal.aborted, false);
assert.equal(signal.reason, undefined);
controller.abort();
assert.equal(signal.aborted, true);
assert.ok(signal.reason instanceof DOMException);
assert.equal(signal.reason.name, "AbortError");
try {
await fetch(`${TEST_SERVER_URL}/ip`, { signal });
assert.fail("fetch should have been aborted");
} catch (error) {
console.log("##Got: ", error.name);
assert.ok(error instanceof DOMException);
assert.equal(error.name, "AbortError");
}
});
});