1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 15:24:46 -05:00
denoland-deno/tests/unit_node/http_test.ts
Yoshiya Hinosawa d59599fc18
fix(ext/node): fix dns.lookup result ordering (#26264)
partially unblocks #25470

This PR aligns the resolution of `localhost` hostname to Node.js
behavior.

In Node.js `dns.lookup("localhost", (_, addr) => console.log(addr))`
prints ipv6 address `::1`, but it prints ipv4 address `127.0.0.1` in
Deno. That difference causes some errors in the work of enabling
`createConnection` option in `http.request` (#25470). This PR fixes the
issue by aligning `dns.lookup` behavior to Node.js.

This PR also changes the following behaviors (resolving TODOs):
- `http.createServer` now listens on ipv6 address `[::]` by default on
linux/mac
- `net.createServer` now listens on ipv6 address `[::]` by default on
linux/mac

These changes are also alignments to Node.js behaviors.
2024-10-16 20:58:44 +09:00

1678 lines
46 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// deno-lint-ignore-file no-console
import EventEmitter from "node:events";
import http, { type RequestOptions, type ServerResponse } from "node:http";
import url from "node:url";
import https from "node:https";
import net from "node:net";
import fs from "node:fs";
import { text } from "node:stream/consumers";
import { assert, assertEquals, fail } from "@std/assert";
import { assertSpyCalls, spy } from "@std/testing/mock";
import { fromFileUrl, relative } from "@std/path";
import { retry } from "@std/async/retry";
import { gzip } from "node:zlib";
import { Buffer } from "node:buffer";
import { execCode } from "../unit/test_util.ts";
Deno.test("[node/http listen]", async () => {
{
const server = http.createServer();
assertEquals(0, EventEmitter.listenerCount(server, "request"));
}
{
const server = http.createServer(() => {});
assertEquals(1, EventEmitter.listenerCount(server, "request"));
}
{
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer();
server.listen(42453, "localhost", () => {
// @ts-ignore address() is not a string
assertEquals(server.address()!.address, "127.0.0.1");
server.close();
});
server.on("close", () => {
resolve();
});
await promise;
}
{
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer();
server.listen().on("listening", () => {
server.close();
});
server.on("close", () => {
resolve();
});
await promise;
}
for (const port of [0, -0, 0.0, "0", null, undefined]) {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer();
server.listen(port, () => {
server.close();
});
server.on("close", () => {
resolve();
});
await promise;
}
});
Deno.test("[node/http close]", async () => {
{
const deferred1 = Promise.withResolvers<void>();
const deferred2 = Promise.withResolvers<void>();
// Node quirk: callback gets exception object, event listener does not.
// deno-lint-ignore no-explicit-any
const server = http.createServer().close((err: any) => {
assertEquals(err.code, "ERR_SERVER_NOT_RUNNING");
deferred1.resolve();
});
// deno-lint-ignore no-explicit-any
server.on("close", (err: any) => {
assertEquals(err, undefined);
deferred2.resolve();
});
server.on("listening", () => {
throw Error("unreachable");
});
await deferred1.promise;
await deferred2.promise;
}
{
const deferred1 = Promise.withResolvers<void>();
const deferred2 = Promise.withResolvers<void>();
const server = http.createServer().listen().close((err) => {
assertEquals(err, undefined);
deferred1.resolve();
});
// deno-lint-ignore no-explicit-any
server.on("close", (err: any) => {
assertEquals(err, undefined);
deferred2.resolve();
});
server.on("listening", () => {
throw Error("unreachable");
});
await deferred1.promise;
await deferred2.promise;
}
});
Deno.test("[node/http] chunked response", async () => {
for (
const body of [undefined, "", "ok"]
) {
const expected = body ?? "";
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.writeHead(200, { "transfer-encoding": "chunked" });
res.end(body);
});
server.listen(async () => {
const res = await fetch(
// deno-lint-ignore no-explicit-any
`http://127.0.0.1:${(server.address() as any).port}/`,
);
assert(res.ok);
const actual = await res.text();
assertEquals(actual, expected);
server.close(() => resolve());
});
await promise;
}
});
Deno.test("[node/http] .writeHead()", async (t) => {
async function testWriteHead(
onRequest: (res: ServerResponse) => void,
onResponse: (res: Response) => void,
) {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
onRequest(res);
res.end();
});
server.listen(async () => {
const res = await fetch(
// deno-lint-ignore no-explicit-any
`http://127.0.0.1:${(server.address() as any).port}/`,
);
await res.body?.cancel();
onResponse(res);
server.close(() => resolve());
});
await promise;
}
await t.step("send status code", async () => {
await testWriteHead(
(res) => res.writeHead(404),
(res) => {
assertEquals(res.status, 404);
},
);
});
// TODO(@marvinhagemeister): hyper doesn't support custom status text
// await t.step("send status + custom status text", async () => {
// await testWriteHead(
// (res) => res.writeHead(404, "some text"),
// (res) => {
// assertEquals(res.status, 404);
// assertEquals(res.statusText, "some text");
// },
// );
// });
await t.step("send status + custom status text + headers obj", async () => {
await testWriteHead(
(res) => res.writeHead(404, "some text", { foo: "bar" }),
(res) => {
assertEquals(res.status, 404);
// TODO(@marvinhagemeister): hyper doesn't support custom
// status text
// assertEquals(res.statusText, "some text");
assertEquals(res.headers.get("foo"), "bar");
},
);
});
await t.step("send status + headers obj", async () => {
await testWriteHead(
(res) => {
res.writeHead(200, {
foo: "bar",
bar: ["foo1", "foo2"],
foobar: 1,
});
},
(res) => {
assertEquals(res.status, 200);
assertEquals(res.headers.get("foo"), "bar");
assertEquals(res.headers.get("bar"), "foo1, foo2");
assertEquals(res.headers.get("foobar"), "1");
},
);
});
await t.step("send status + headers array", async () => {
await testWriteHead(
(res) => res.writeHead(200, [["foo", "bar"]]),
(res) => {
assertEquals(res.status, 200);
assertEquals(res.headers.get("foo"), "bar");
},
);
});
});
// Test empty chunks: https://github.com/denoland/deno/issues/17194
Deno.test("[node/http] empty chunk in the middle of response", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.write("a");
res.write("");
res.write("b");
res.end();
});
server.listen(async () => {
const res = await fetch(
// deno-lint-ignore no-explicit-any
`http://127.0.0.1:${(server.address() as any).port}/`,
);
const actual = await res.text();
assertEquals(actual, "ab");
server.close(() => resolve());
});
await promise;
});
Deno.test("[node/http] server can respond with 101, 204, 205, 304 status", async () => {
for (const status of [101, 204, 205, 304]) {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.statusCode = status;
res.end("");
});
server.listen(async () => {
const res = await fetch(
// deno-lint-ignore no-explicit-any
`http://127.0.0.1:${(server.address() as any).port}/`,
);
await res.body?.cancel();
assertEquals(res.status, status);
server.close(() => resolve());
});
await promise;
}
});
Deno.test("[node/http] multiple set-cookie headers", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.setHeader("Set-Cookie", ["foo=bar", "bar=foo"]);
assertEquals(res.getHeader("Set-Cookie"), ["foo=bar", "bar=foo"]);
res.end();
});
server.listen(async () => {
const res = await fetch(
// deno-lint-ignore no-explicit-any
`http://127.0.0.1:${(server.address() as any).port}/`,
);
assert(res.ok);
const setCookieHeaders = res.headers.getSetCookie();
assertEquals(setCookieHeaders, ["foo=bar", "bar=foo"]);
await res.body!.cancel();
server.close(() => resolve());
});
await promise;
});
Deno.test("[node/http] IncomingRequest socket has remoteAddress + remotePort", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
let remoteAddress: string | undefined;
let remotePort: number | undefined;
const server = http.createServer((req, res) => {
remoteAddress = req.socket.remoteAddress;
remotePort = req.socket.remotePort;
res.end();
});
server.listen(async () => {
// deno-lint-ignore no-explicit-any
const port = (server.address() as any).port;
const res = await fetch(
`http://localhost:${port}/`,
);
await res.arrayBuffer();
if (Deno.build.os === "windows") {
assertEquals(remoteAddress, "127.0.0.1");
} else {
assertEquals(remoteAddress, "::1");
}
assertEquals(typeof remotePort, "number");
server.close(() => resolve());
});
await promise;
});
Deno.test("[node/http] request default protocol", async () => {
const deferred1 = Promise.withResolvers<void>();
const deferred2 = Promise.withResolvers<void>();
const server = http.createServer((_, res) => {
res.end("ok");
});
// @ts-ignore IncomingMessageForClient
// deno-lint-ignore no-explicit-any
let clientRes: any;
// deno-lint-ignore no-explicit-any
let clientReq: any;
server.listen(() => {
clientReq = http.request(
// deno-lint-ignore no-explicit-any
{ host: "localhost", port: (server.address() as any).port },
(res) => {
assert(res.socket instanceof EventEmitter);
assertEquals(res.complete, false);
res.on("data", () => {});
res.on("end", () => {
server.close();
});
clientRes = res;
assertEquals(res.statusCode, 200);
deferred2.resolve();
},
);
clientReq.end();
});
server.on("close", () => {
deferred1.resolve();
});
await deferred1.promise;
await deferred2.promise;
assert(clientReq.socket instanceof EventEmitter);
assertEquals(clientRes!.complete, true);
});
Deno.test("[node/http] request with headers", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((req, res) => {
assertEquals(req.headers["x-foo"], "bar");
res.end("ok");
});
server.listen(() => {
const req = http.request(
{
host: "localhost",
// deno-lint-ignore no-explicit-any
port: (server.address() as any).port,
headers: { "x-foo": "bar" },
},
(res) => {
res.on("data", () => {});
res.on("end", () => {
server.close();
});
assertEquals(res.statusCode, 200);
},
);
req.end();
});
server.on("close", () => {
resolve();
});
await promise;
});
Deno.test("[node/http] non-string buffer response", {
// TODO(kt3k): Enable sanitizer. A "zlib" resource is leaked in this test case.
sanitizeResources: false,
}, async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_, res) => {
res.socket!.end();
gzip(
Buffer.from("a".repeat(100), "utf8"),
{},
(_err: Error | null, data: Buffer) => {
res.setHeader("Content-Encoding", "gzip");
res.end(data);
},
);
});
server.listen(async () => {
const res = await fetch(
// deno-lint-ignore no-explicit-any
`http://localhost:${(server.address() as any).port}`,
);
try {
const text = await res.text();
assertEquals(text, "a".repeat(100));
} catch (e) {
server.emit("error", e);
} finally {
server.close(() => resolve());
}
});
await promise;
});
// TODO(kt3k): Enable this test
// Currently IncomingMessage constructor has incompatible signature.
/*
Deno.test("[node/http] http.IncomingMessage can be created without url", () => {
const message = new http.IncomingMessage(
// adapted from https://github.com/dougmoscrop/serverless-http/blob/80bfb3e940057d694874a8b0bc12ad96d2abe7ab/lib/request.js#L7
{
// @ts-expect-error - non-request properties will also be passed in, e.g. by serverless-http
encrypted: true,
readable: false,
remoteAddress: "foo",
address: () => ({ port: 443 }),
// deno-lint-ignore no-explicit-any
end: Function.prototype as any,
// deno-lint-ignore no-explicit-any
destroy: Function.prototype as any,
},
);
message.url = "https://example.com";
});
*/
Deno.test("[node/http] send request with non-chunked body", async () => {
let requestHeaders: Headers;
let requestBody = "";
const hostname = "localhost";
const port = 4505;
const handler = async (req: Request) => {
requestHeaders = req.headers;
requestBody = await req.text();
return new Response("ok");
};
const abortController = new AbortController();
const servePromise = Deno.serve({
hostname,
port,
signal: abortController.signal,
onListen: undefined,
}, handler).finished;
const opts: RequestOptions = {
host: hostname,
port,
method: "POST",
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": "11",
},
};
const req = http.request(opts, (res) => {
res.on("data", () => {});
res.on("end", () => {
abortController.abort();
});
assertEquals(res.statusCode, 200);
assertEquals(requestHeaders.get("content-length"), "11");
assertEquals(requestHeaders.has("transfer-encoding"), false);
assertEquals(requestBody, "hello world");
});
req.on("socket", (socket) => {
assert(socket.writable);
assert(socket.readable);
socket.setKeepAlive();
socket.destroy();
socket.setTimeout(100);
});
req.write("hello ");
req.write("world");
req.end();
await Promise.all([
servePromise,
// wait 100ms because of the socket.setTimeout(100) above
// in order to not cause a flaky test sanitizer failure
await new Promise((resolve) => setTimeout(resolve, 100)),
]);
});
Deno.test("[node/http] send request with chunked body", async () => {
let requestHeaders: Headers;
let requestBody = "";
const hostname = "localhost";
const port = 4505;
const handler = async (req: Request) => {
requestHeaders = req.headers;
requestBody = await req.text();
return new Response("ok");
};
const abortController = new AbortController();
const servePromise = Deno.serve({
hostname,
port,
signal: abortController.signal,
onListen: undefined,
}, handler).finished;
const opts: RequestOptions = {
host: hostname,
port,
method: "POST",
headers: {
"Content-Type": "text/plain; charset=utf-8",
"Content-Length": "11",
"Transfer-Encoding": "chunked",
},
};
const req = http.request(opts, (res) => {
res.on("data", () => {});
res.on("end", () => {
abortController.abort();
});
assertEquals(res.statusCode, 200);
assertEquals(requestHeaders.has("content-length"), false);
assertEquals(requestHeaders.get("transfer-encoding"), "chunked");
assertEquals(requestBody, "hello world");
});
req.write("hello ");
req.write("world");
req.end();
await servePromise;
});
Deno.test("[node/http] send request with chunked body as default", async () => {
let requestHeaders: Headers;
let requestBody = "";
const hostname = "localhost";
const port = 4505;
const handler = async (req: Request) => {
requestHeaders = req.headers;
requestBody = await req.text();
return new Response("ok");
};
const abortController = new AbortController();
const servePromise = Deno.serve({
hostname,
port,
signal: abortController.signal,
onListen: undefined,
}, handler).finished;
const opts: RequestOptions = {
host: hostname,
port,
method: "POST",
headers: {
"Content-Type": "text/plain; charset=utf-8",
},
};
const req = http.request(opts, (res) => {
res.on("data", () => {});
res.on("end", () => {
abortController.abort();
});
assertEquals(res.statusCode, 200);
assertEquals(requestHeaders.has("content-length"), false);
assertEquals(requestHeaders.get("transfer-encoding"), "chunked");
assertEquals(requestBody, "hello world");
});
req.write("hello ");
req.write("world");
req.end();
await servePromise;
});
Deno.test("[node/http] ServerResponse _implicitHeader", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
const writeHeadSpy = spy(res, "writeHead");
// deno-lint-ignore no-explicit-any
(res as any)._implicitHeader();
assertSpyCalls(writeHeadSpy, 1);
writeHeadSpy.restore();
res.end("Hello World");
});
server.listen(async () => {
const { port } = server.address() as { port: number };
const res = await fetch(`http://localhost:${port}`);
assertEquals(await res.text(), "Hello World");
server.close(() => {
resolve();
});
});
await promise;
});
// https://github.com/denoland/deno/issues/21509
Deno.test("[node/http] ServerResponse flushHeaders", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.flushHeaders(); // no-op
res.end("Hello World");
});
server.listen(async () => {
const { port } = server.address() as { port: number };
const res = await fetch(`http://localhost:${port}`);
assertEquals(await res.text(), "Hello World");
server.close(() => {
resolve();
});
});
await promise;
});
Deno.test("[node/http] server unref", async () => {
const [statusCode, _output] = await execCode(`
import http from "node:http";
const server = http.createServer((_req, res) => {
res.statusCode = status;
res.end("");
});
// This should let the program to exit without waiting for the
// server to close.
server.unref();
server.listen(async () => {
});
`);
assertEquals(statusCode, 0);
});
Deno.test("[node/http] ClientRequest handle non-string headers", async () => {
// deno-lint-ignore no-explicit-any
let headers: any;
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = http.request("http://localhost:4545/echo_server", {
method: "POST",
headers: { 1: 2 },
}, (resp) => {
headers = resp.headers;
resp.on("data", () => {});
resp.on("end", () => {
resolve();
});
});
req.once("error", (e) => reject(e));
req.end();
await promise;
assertEquals(headers!["1"], "2");
});
Deno.test("[node/http] ClientRequest uses HTTP/1.1", async () => {
let body = "";
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = https.request("https://localhost:5545/http_version", {
method: "POST",
headers: { 1: 2 },
}, (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
});
req.once("error", (e) => reject(e));
req.end();
await promise;
assertEquals(body, "HTTP/1.1");
});
Deno.test("[node/http] ClientRequest setTimeout", async () => {
let body = "";
const { promise, resolve, reject } = Promise.withResolvers<void>();
const timer = setTimeout(() => reject("timed out"), 50000);
const req = http.request("http://localhost:4545/http_version", (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
});
req.setTimeout(120000);
req.once("error", (e) => reject(e));
req.end();
await promise;
clearTimeout(timer);
assertEquals(body, "HTTP/1.1");
});
Deno.test("[node/http] ClientRequest setNoDelay", async () => {
let body = "";
const { promise, resolve, reject } = Promise.withResolvers<void>();
const timer = setTimeout(() => reject("timed out"), 50000);
const req = http.request("http://localhost:4545/http_version", (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
});
req.setNoDelay(true);
req.once("error", (e) => reject(e));
req.end();
await promise;
clearTimeout(timer);
assertEquals(body, "HTTP/1.1");
});
Deno.test("[node/http] ClientRequest PATCH", async () => {
let body = "";
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = http.request("http://localhost:4545/echo_server", {
method: "PATCH",
}, (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
});
req.write("hello ");
req.write("world");
req.once("error", (e) => reject(e));
req.end();
await promise;
assertEquals(body, "hello world");
});
Deno.test("[node/http] ClientRequest PUT", async () => {
let body = "";
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = http.request("http://localhost:4545/echo_server", {
method: "PUT",
}, (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
});
req.write("hello ");
req.write("world");
req.once("error", (e) => reject(e));
req.end();
await promise;
assertEquals(body, "hello world");
});
Deno.test("[node/http] ClientRequest search params", async () => {
let body = "";
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = http.request({
host: "localhost:4545",
path: "search_params?foo=bar",
}, (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
});
req.once("error", (e) => reject(e));
req.end();
await promise;
assertEquals(body, "foo=bar");
});
Deno.test("[node/http] HTTPS server", async () => {
const deferred = Promise.withResolvers<void>();
const deferred2 = Promise.withResolvers<void>();
const client = Deno.createHttpClient({
caCerts: [Deno.readTextFileSync("tests/testdata/tls/RootCA.pem")],
});
const server = https.createServer({
cert: Deno.readTextFileSync("tests/testdata/tls/localhost.crt"),
key: Deno.readTextFileSync("tests/testdata/tls/localhost.key"),
}, (req, res) => {
// @ts-ignore: It exists on TLSSocket
assert(req.socket.encrypted);
res.end("success!");
});
server.listen(() => {
// deno-lint-ignore no-explicit-any
fetch(`https://localhost:${(server.address() as any).port}`, {
client,
}).then(async (res) => {
assertEquals(res.status, 200);
assertEquals(await res.text(), "success!");
server.close();
deferred2.resolve();
});
})
.on("error", () => fail());
server.on("close", () => {
deferred.resolve();
});
await Promise.all([deferred.promise, deferred2.promise]);
client.close();
});
Deno.test(
"[node/http] client upgrade",
{ permissions: { net: true } },
async () => {
const { promise: serverClosed, resolve: resolveServer } = Promise
.withResolvers<void>();
const { promise: socketClosed, resolve: resolveSocket } = Promise
.withResolvers<void>();
const server = http.createServer((req, res) => {
// @ts-ignore: It exists on TLSSocket
assert(!req.socket.encrypted);
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("okay");
});
// @ts-ignore it's a socket for real
let serverSocket;
server.on("upgrade", (req, socket, _head) => {
// https://github.com/denoland/deno/issues/21979
assert(req.socket?.write);
socket.write(
"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" +
"Upgrade: WebSocket\r\n" +
"Connection: Upgrade\r\n" +
"\r\n",
);
serverSocket = socket;
});
// Now that server is running
server.listen(1337, "127.0.0.1", () => {
// make a request
const options = {
port: 1337,
host: "127.0.0.1",
headers: {
"Connection": "Upgrade",
"Upgrade": "websocket",
},
};
const req = http.request(options);
req.end();
req.on("upgrade", (_res, socket, _upgradeHead) => {
socket.end();
// @ts-ignore it's a socket for real
serverSocket!.end();
server.close(() => {
resolveServer();
});
socket.on("close", () => {
resolveSocket();
});
});
});
await serverClosed;
await socketClosed;
},
);
Deno.test(
"[node/http] client end with callback",
{ permissions: { net: true } },
async () => {
let received = false;
const ac = new AbortController();
const server = Deno.serve({ port: 5928, signal: ac.signal }, (_req) => {
received = true;
return new Response("hello");
});
const { promise, resolve, reject } = Promise.withResolvers<void>();
let body = "";
const request = http.request(
"http://localhost:5928/",
(resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
},
);
request.on("error", reject);
request.end(() => {
assert(received);
});
await promise;
ac.abort();
await server.finished;
assertEquals(body, "hello");
},
);
Deno.test("[node/http] server emits error if addr in use", async () => {
const deferred1 = Promise.withResolvers<void>();
const deferred2 = Promise.withResolvers<Error>();
const server = http.createServer();
server.listen(9001);
const server2 = http.createServer();
server2.on("error", (e) => {
deferred2.resolve(e);
});
server2.listen(9001);
const err = await deferred2.promise;
server.close(() => deferred1.resolve());
server2.close();
await deferred1.promise;
const expectedMsg = Deno.build.os === "windows"
? "Only one usage of each socket address"
: "Address already in use";
assert(
err.message.startsWith(expectedMsg),
`Wrong error: ${err.message}`,
);
});
Deno.test(
"[node/http] client destroy doesn't leak",
{ permissions: { net: true } },
async () => {
const ac = new AbortController();
let timerId;
const server = Deno.serve(
{ port: 5929, signal: ac.signal },
async (_req) => {
await new Promise((resolve) => {
timerId = setTimeout(resolve, 5000);
});
return new Response("hello");
},
);
const { promise, resolve, reject } = Promise.withResolvers<void>();
const request = http.request("http://localhost:5929/");
request.on("error", reject);
request.on("close", () => {});
request.end();
setTimeout(() => {
request.destroy(new Error());
resolve();
}, 100);
await promise;
clearTimeout(timerId);
ac.abort();
await server.finished;
},
);
Deno.test(
"[node/http] client destroy before sending request should not error",
() => {
const request = http.request("http://localhost:5929/");
// Calling this would throw
request.destroy();
},
);
Deno.test(
"[node/http] destroyed requests should not be sent",
async () => {
let receivedRequest = false;
const server = Deno.serve(() => {
receivedRequest = true;
return new Response(null);
});
const request = http.request(`http://localhost:${server.addr.port}/`);
request.destroy();
request.end("hello");
await new Promise((r) => setTimeout(r, 500));
assertEquals(receivedRequest, false);
await server.shutdown();
},
);
Deno.test("[node/http] node:http exports globalAgent", async () => {
const http = await import("node:http");
assert(
http.globalAgent,
"node:http must export 'globalAgent' on module namespace",
);
assert(
http.default.globalAgent,
"node:http must export 'globalAgent' on module default export",
);
});
Deno.test("[node/https] node:https exports globalAgent", async () => {
const https = await import("node:https");
assert(
https.globalAgent,
"node:https must export 'globalAgent' on module namespace",
);
assert(
https.default.globalAgent,
"node:https must export 'globalAgent' on module default export",
);
});
Deno.test("[node/http] node:http request.setHeader(header, null) doesn't throw", () => {
{
const req = http.request("http://localhost:4545/");
req.on("error", () => {});
// @ts-expect-error - null is not a valid header value
req.setHeader("foo", null);
req.end();
req.destroy();
}
{
const req = https.request("https://localhost:4545/");
req.on("error", () => {});
// @ts-expect-error - null is not a valid header value
req.setHeader("foo", null);
req.end();
req.destroy();
}
});
Deno.test("[node/http] ServerResponse getHeader", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.setHeader("foo", "bar");
assertEquals(res.getHeader("foo"), "bar");
assertEquals(res.getHeader("ligma"), undefined);
res.end("Hello World");
});
server.listen(async () => {
const { port } = server.address() as { port: number };
const res = await fetch(`http://localhost:${port}`);
assertEquals(await res.text(), "Hello World");
server.close(() => {
resolve();
});
});
await promise;
});
Deno.test("[node/http] ServerResponse appendHeader", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.setHeader("foo", "bar");
res.appendHeader("foo", "baz");
res.appendHeader("foo", ["qux"]);
res.appendHeader("foo", ["quux"]);
res.appendHeader("Set-Cookie", "a=b");
res.appendHeader("Set-Cookie", ["c=d", "e=f"]);
res.end("Hello World");
});
server.listen(async () => {
const { port } = server.address() as { port: number };
const res = await fetch(`http://localhost:${port}`);
assertEquals(res.headers.get("foo"), "bar, baz, qux, quux");
assertEquals(res.headers.getSetCookie(), ["a=b", "c=d", "e=f"]);
assertEquals(await res.text(), "Hello World");
server.close(() => {
resolve();
});
});
await promise;
});
Deno.test("[node/http] ServerResponse appendHeader set-cookie", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.appendHeader("Set-Cookie", "a=b");
res.appendHeader("Set-Cookie", "c=d");
res.end("Hello World");
});
server.listen(async () => {
const { port } = server.address() as { port: number };
const res = await fetch(`http://localhost:${port}`);
assertEquals(res.headers.getSetCookie(), ["a=b", "c=d"]);
assertEquals(await res.text(), "Hello World");
server.close(() => {
resolve();
});
});
await promise;
});
Deno.test("[node/http] IncomingMessage override", () => {
const req = new http.IncomingMessage(new net.Socket());
// https://github.com/dougmoscrop/serverless-http/blob/3aaa6d0fe241109a8752efb011c242d249f32368/lib/request.js#L20-L30
Object.assign(req, {
ip: "1.1.1.1",
complete: true,
httpVersion: "1.1",
httpVersionMajor: "1",
httpVersionMinor: "1",
method: "GET",
headers: {},
body: "",
url: "https://1.1.1.1",
});
});
Deno.test("[node/http] ServerResponse assignSocket and detachSocket", () => {
const req = new http.IncomingMessage(new net.Socket());
const res = new http.ServerResponse(req);
let writtenData: string | Uint8Array | undefined = undefined;
let writtenEncoding: string | Uint8Array | undefined = undefined;
const socket = {
_writableState: {},
writable: true,
on: Function.prototype,
removeListener: Function.prototype,
destroy: Function.prototype,
cork: Function.prototype,
uncork: Function.prototype,
write: (
data: string | Uint8Array,
encoding: string,
_cb?: (err?: Error) => void,
) => {
writtenData = data;
writtenEncoding = encoding;
},
};
// @ts-ignore it's a socket mock
res.assignSocket(socket);
res.write("Hello World!", "utf8");
assertEquals(writtenData, Buffer.from("Hello World!"));
assertEquals(writtenEncoding, "buffer");
writtenData = undefined;
writtenEncoding = undefined;
// TODO(@littledivy): This test never really worked
// because there was no data being sent and it passed.
//
// @ts-ignore it's a socket mock
// res.detachSocket(socket);
// res.write("Hello World!", "utf8");
//
// assertEquals(writtenData, undefined);
// assertEquals(writtenEncoding, undefined);
});
Deno.test("[node/http] ServerResponse getHeaders", () => {
const req = new http.IncomingMessage(new net.Socket());
const res = new http.ServerResponse(req);
res.setHeader("foo", "bar");
res.setHeader("bar", "baz");
assertEquals(res.getHeaderNames(), ["foo", "bar"]);
assertEquals(res.getHeaders(), { "foo": "bar", "bar": "baz" });
});
Deno.test("[node/http] ServerResponse default status code 200", () => {
const req = new http.IncomingMessage(new net.Socket());
const res = new http.ServerResponse(req);
assertEquals(res.statusCode, 200);
});
Deno.test("[node/http] maxHeaderSize is defined", () => {
assertEquals(http.maxHeaderSize, 16_384);
});
Deno.test("[node/http] server graceful close", async () => {
const server = http.createServer(function (_, response) {
response.writeHead(200, {});
response.end("ok");
server.close();
});
const { promise, resolve } = Promise.withResolvers<void>();
server.listen(0, function () {
// deno-lint-ignore no-explicit-any
const port = (server.address() as any).port;
const testURL = url.parse(
`http://localhost:${port}`,
);
http.request(testURL, function (response) {
assertEquals(response.statusCode, 200);
response.on("data", function () {});
response.on("end", function () {
resolve();
});
}).end();
});
await promise;
});
Deno.test("[node/http] server closeAllConnections shutdown", async () => {
const server = http.createServer((_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
data: "Hello World!",
}));
});
server.listen(0);
const { promise, resolve } = Promise.withResolvers<void>();
setTimeout(() => {
server.close(() => resolve());
server.closeAllConnections();
}, 2000);
await promise;
});
Deno.test("[node/http] server closeIdleConnections shutdown", async () => {
const server = http.createServer({ keepAliveTimeout: 60000 }, (_req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
data: "Hello World!",
}));
});
server.listen(0);
const { promise, resolve } = Promise.withResolvers<void>();
setTimeout(() => {
server.close(() => resolve());
server.closeIdleConnections();
}, 2000);
await promise;
});
Deno.test("[node/http] client closing a streaming response doesn't terminate server", async () => {
let interval: number;
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
interval = setInterval(() => {
res.write("Hello, world!\n");
}, 100);
req.on("end", () => {
clearInterval(interval);
res.end();
});
req.on("error", (err) => {
console.error("Request error:", err);
clearInterval(interval);
res.end();
});
});
const deferred1 = Promise.withResolvers<void>();
server.listen(0, () => {
// deno-lint-ignore no-explicit-any
const port = (server.address() as any).port;
// Create a client connection to the server
const client = net.createConnection({ port }, () => {
console.log("Client connected to server");
// Write data to the server
client.write("GET / HTTP/1.1\r\n");
client.write("Host: localhost\r\n");
client.write("Connection: close\r\n");
client.write("\r\n");
// End the client connection prematurely while reading data
client.on("data", (data) => {
assert(data.length > 0);
client.end();
setTimeout(() => deferred1.resolve(), 100);
});
});
});
await deferred1.promise;
assertEquals(server.listening, true);
server.close();
assertEquals(server.listening, false);
clearInterval(interval!);
});
Deno.test("[node/http] client closing a streaming request doesn't terminate server", async () => {
let interval: number;
let uploadedData = "";
let requestError: Error | null = null;
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
interval = setInterval(() => {
res.write("Hello, world!\n");
}, 100);
req.on("data", (chunk) => {
uploadedData += chunk.toString();
});
req.on("end", () => {
clearInterval(interval);
});
req.on("error", (err) => {
requestError = err;
clearInterval(interval);
res.end();
});
});
const deferred1 = Promise.withResolvers<void>();
server.listen(0, () => {
// deno-lint-ignore no-explicit-any
const port = (server.address() as any).port;
// Create a client connection to the server
const client = net.createConnection({ port }, () => {
const headers = [
"POST /upload HTTP/1.1",
"Host: localhost",
"Content-Type: text/plain",
"Transfer-Encoding: chunked",
"",
"",
].join("\r\n");
client.write(headers);
const chunk = "A".repeat(100);
let sentChunks = 0;
function writeChunk() {
const chunkHeader = `${chunk.length.toString(16)}\r\n`;
client.write(chunkHeader);
client.write(chunk);
client.write("\r\n");
sentChunks++;
if (sentChunks >= 3) {
client.destroy();
setTimeout(() => {
deferred1.resolve();
}, 40);
} else {
setTimeout(writeChunk, 10);
}
}
writeChunk();
});
});
await deferred1.promise;
assert(requestError !== null, "Server should have received an error");
assert(
(requestError! as Error)?.name === "Http",
`Expected Http error, got ${(requestError! as Error)?.name}`,
);
assert(
(requestError! as Error)?.message.includes(
"error reading a body from connection",
),
);
assertEquals(server.listening, true);
server.close();
assertEquals(server.listening, false);
clearInterval(interval!);
});
Deno.test("[node/http] http.request() post streaming body works", async () => {
const server = http.createServer((req, res) => {
if (req.method === "POST") {
let receivedBytes = 0;
req.on("data", (chunk) => {
receivedBytes += chunk.length;
});
req.on("end", () => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ bytes: receivedBytes }));
});
} else {
res.writeHead(405, { "Content-Type": "text/plain" });
res.end("Method Not Allowed");
}
});
const responseEnded = Promise.withResolvers<void>();
const fileClosed = Promise.withResolvers<void>();
const timeout = setTimeout(() => {
responseEnded.reject(new Error("timeout"));
}, 5000);
server.listen(0, () => {
// deno-lint-ignore no-explicit-any
const port = (server.address() as any).port;
const filePath = relative(
Deno.cwd(),
fromFileUrl(new URL("./testdata/lorem_ipsum_512kb.txt", import.meta.url)),
);
const contentLength = 524289;
const options = {
hostname: "localhost",
port: port,
path: "/",
method: "POST",
headers: {
"Content-Type": "application/octet-stream",
"Content-Length": contentLength,
},
};
const req = http.request(options, (res) => {
let responseBody = "";
res.on("data", (chunk) => {
responseBody += chunk;
});
res.on("end", () => {
const response = JSON.parse(responseBody);
assertEquals(res.statusCode, 200);
assertEquals(response.bytes, contentLength);
responseEnded.resolve();
});
});
req.on("error", (e) => {
console.error(`Problem with request: ${e.message}`);
});
const readStream = fs.createReadStream(filePath);
readStream.pipe(req);
readStream.on("close", fileClosed.resolve);
});
await responseEnded.promise;
await fileClosed.promise;
assertEquals(server.listening, true);
server.close();
clearTimeout(timeout);
assertEquals(server.listening, false);
});
// https://github.com/denoland/deno/issues/24239
Deno.test("[node/http] ServerResponse write transfer-encoding chunked", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((_req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("Transfer-Encoding", "chunked");
res.setHeader("Access-Control-Allow-Origin", "*");
res.writeHead(200, {
"Other-Header": "value",
});
res.write("");
});
server.listen(async () => {
const { port } = server.address() as { port: number };
const res = await fetch(`http://localhost:${port}`);
assertEquals(res.status, 200);
assertEquals(res.headers.get("content-type"), "text/event-stream");
assertEquals(res.headers.get("Other-Header"), "value");
await res.body!.cancel();
server.close(() => {
resolve();
});
});
await promise;
});
Deno.test("[node/http] Server.address() can be null", () => {
const server = http.createServer((_req, res) => res.end("it works"));
assertEquals(server.address(), null);
});
Deno.test("[node/http] ClientRequest PUT subarray", async () => {
const buffer = Buffer.from("hello world");
const payload = buffer.subarray(6, 11);
let body = "";
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = http.request("http://localhost:4545/echo_server", {
method: "PUT",
}, (resp) => {
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
});
req.once("error", (e) => reject(e));
req.end(payload);
await promise;
assertEquals(body, "world");
});
Deno.test("[node/http] req.url equals pathname + search", async () => {
const { promise, resolve } = Promise.withResolvers<void>();
const server = http.createServer((req, res) => res.end(req.url));
server.listen(async () => {
const { port } = server.address() as net.AddressInfo;
const res = await fetch(`http://localhost:${port}/foo/bar?baz=1`);
const text = await res.text();
assertEquals(text, "/foo/bar?baz=1");
server.close(() => {
resolve();
});
});
await promise;
});
Deno.test("[node/http] ClientRequest content-disposition header works", async () => {
const payload = Buffer.from("hello world");
let body = "";
let headers = {} as http.IncomingHttpHeaders;
const { promise, resolve, reject } = Promise.withResolvers<void>();
const req = http.request("http://localhost:4545/echo_server", {
method: "PUT",
headers: {
"content-disposition": "attachment",
},
}, (resp) => {
headers = resp.headers;
resp.on("data", (chunk) => {
body += chunk;
});
resp.on("end", () => {
resolve();
});
});
req.once("error", (e) => reject(e));
req.end(payload);
await promise;
assertEquals(body, "hello world");
assertEquals(headers["content-disposition"], "attachment");
});
Deno.test("[node/http] In ClientRequest, option.hostname has precedence over options.host", async () => {
const responseReceived = Promise.withResolvers<void>();
new http.ClientRequest({
hostname: "localhost",
host: "invalid-hostname.test",
port: 4545,
path: "/http_version",
}).on("response", async (res) => {
assertEquals(res.statusCode, 200);
assertEquals(await text(res), "HTTP/1.1");
responseReceived.resolve();
}).end();
await responseReceived.promise;
});
Deno.test("[node/http] upgraded socket closes when the server closed without closing handshake", async () => {
const clientSocketClosed = Promise.withResolvers<void>();
const serverProcessClosed = Promise.withResolvers<void>();
// Uses the server in different process to shutdown it without closing handshake
const server = `
Deno.serve({ port: 1337 }, (req) => {
if (req.headers.get("upgrade") != "websocket") {
return new Response("ok");
}
console.log("upgrade on server");
const { socket, response } = Deno.upgradeWebSocket(req);
socket.addEventListener("message", (event) => {
console.log("server received", event.data);
socket.send("pong");
});
return response;
});
`;
const p = new Deno.Command("deno", { args: ["eval", server] }).spawn();
// Wait for the server to respond
await retry(async () => {
const resp = await fetch("http://localhost:1337");
const _text = await resp.text();
});
const options = {
port: 1337,
host: "127.0.0.1",
headers: {
"Connection": "Upgrade",
"Upgrade": "websocket",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
},
};
http.request(options).on("upgrade", (_res, socket) => {
socket.on("close", () => {
console.log("client socket closed");
clientSocketClosed.resolve();
});
socket.on("data", async (data) => {
// receives pong message
assertEquals(data, Buffer.from("8104706f6e67", "hex"));
p.kill();
await p.status;
console.log("process closed");
serverProcessClosed.resolve();
// sending some additional message
socket.write(Buffer.from("81847de88e01", "hex"));
socket.write(Buffer.from("0d81e066", "hex"));
});
// sending ping message
socket.write(Buffer.from("81847de88e01", "hex"));
socket.write(Buffer.from("0d81e066", "hex"));
}).end();
await clientSocketClosed.promise;
await serverProcessClosed.promise;
});