1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-25 00:29:09 -05:00
denoland-deno/cli/tests/unit/http_test.ts
Asher Gomez 947ce41e99
feat: deprecate Deno.resources() (#22059)
Most uses of `Deno.resources()` within tests, as they previously checked
for leaked resources. This is not needed as the test runner does this
automatically. Other internal uses of this API have been replaced with
the internal `Deno[Deno.internal].core.resources()`.
2024-01-24 00:27:29 +01:00

2801 lines
82 KiB
TypeScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { Buffer, BufReader, BufWriter } from "../../../test_util/std/io/mod.ts";
import { TextProtoReader } from "../testdata/run/textproto.ts";
import {
assert,
assertEquals,
assertRejects,
assertStrictEquals,
assertThrows,
delay,
fail,
} from "./test_util.ts";
import { join } from "../../../test_util/std/path/mod.ts";
const listenPort = 4507;
const listenPort2 = 4508;
const {
buildCaseInsensitiveCommaValueFinder,
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
} = Deno[Deno.internal];
async function writeRequestAndReadResponse(conn: Deno.Conn): Promise<string> {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const w = new BufWriter(conn);
const r = new BufReader(conn);
const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
const writeResult = await w.write(encoder.encode(body));
assertEquals(body.length, writeResult);
await w.flush();
const tpr = new TextProtoReader(r);
const statusLine = await tpr.readLine();
assert(statusLine !== null);
const headers = await tpr.readMimeHeader();
assert(headers !== null);
const chunkedReader = chunkedBodyReader(headers, r);
const buf = new Uint8Array(5);
const dest = new Buffer();
let result: number | null;
while ((result = await chunkedReader.read(buf)) !== null) {
const len = Math.min(buf.byteLength, result);
await dest.write(buf.subarray(0, len));
}
return decoder.decode(dest.bytes());
}
Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
let httpConn: Deno.HttpConn;
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
assertEquals(new URL(request.url).href, `http://127.0.0.1:${listenPort}/`);
assertEquals(await request.text(), "");
await respondWith(
new Response("Hello World", { headers: { "foo": "bar" } }),
);
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
headers: { "connection": "close" },
});
const clone = resp.clone();
const text = await resp.text();
assertEquals(text, "Hello World");
assertEquals(resp.headers.get("foo"), "bar");
const cloneText = await clone.text();
assertEquals(cloneText, "Hello World");
await promise;
httpConn!.close();
});
// https://github.com/denoland/deno/issues/15107
Deno.test(
{ permissions: { net: true } },
async function httpLazyHeadersIssue15107() {
let headers: Headers;
const promise = (async () => {
const listener = Deno.listen({ port: 2333 });
const conn = await listener.accept();
listener.close();
const httpConn = Deno.serveHttp(conn);
const e = await httpConn.nextRequest();
assert(e);
const { request } = e;
request.text();
headers = request.headers;
httpConn!.close();
})();
const conn = await Deno.connect({ port: 2333 });
// Send GET request with a body + content-length.
const encoder = new TextEncoder();
const body =
`GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`;
const writeResult = await conn.write(encoder.encode(body));
assertEquals(body.length, writeResult);
await promise;
conn.close();
assertEquals(headers!.get("content-length"), "5");
},
);
Deno.test(
{ permissions: { net: true } },
async function httpReadHeadersAfterClose() {
const promise = (async () => {
const listener = Deno.listen({ port: 2334 });
const conn = await listener.accept();
listener.close();
const httpConn = Deno.serveHttp(conn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
await request.text(); // Read body
await respondWith(new Response("Hello World")); // Closes request
assertThrows(() => request.headers, TypeError, "request closed");
httpConn!.close();
})();
const conn = await Deno.connect({ port: 2334 });
// Send GET request with a body + content-length.
const encoder = new TextEncoder();
const body =
`GET / HTTP/1.1\r\nHost: 127.0.0.1:2333\r\nContent-Length: 5\r\n\r\n12345`;
const writeResult = await conn.write(encoder.encode(body));
assertEquals(body.length, writeResult);
await promise;
conn.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerGetRequestBody() {
let httpConn: Deno.HttpConn;
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.body, null);
await respondWith(new Response("", { headers: {} }));
})();
const conn = await Deno.connect({ port: listenPort });
// Send GET request with a body + content-length.
const encoder = new TextEncoder();
const body =
`GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nContent-Length: 5\r\n\r\n12345`;
const writeResult = await conn.write(encoder.encode(body));
assertEquals(body.length, writeResult);
const resp = new Uint8Array(200);
const readResult = await conn.read(resp);
assertEquals(readResult, 138);
conn.close();
await promise;
httpConn!.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerStreamResponse() {
const stream = new TransformStream();
const writer = stream.writable.getWriter();
writer.write(new TextEncoder().encode("hello "));
writer.write(new TextEncoder().encode("world"));
writer.close();
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
httpConn = Deno.serveHttp(conn);
const evt = await httpConn.nextRequest();
assert(evt);
const { request, respondWith } = evt;
assert(!request.body);
await respondWith(new Response(stream.readable));
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
const respBody = await resp.text();
assertEquals("hello world", respBody);
await promise;
httpConn!.close();
listener.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerStreamRequest() {
const stream = new TransformStream();
const writer = stream.writable.getWriter();
writer.write(new TextEncoder().encode("hello "));
writer.write(new TextEncoder().encode("world"));
writer.close();
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
const httpConn = Deno.serveHttp(conn);
const evt = await httpConn.nextRequest();
assert(evt);
const { request, respondWith } = evt;
const reqBody = await request.text();
assertEquals("hello world", reqBody);
await respondWith(new Response(""));
// TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending
// request for url (https://localhost:${listenPort}/): connection closed before
// message completed".
assertEquals(await httpConn.nextRequest(), null);
listener.close();
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
body: stream.readable,
method: "POST",
headers: { "connection": "close" },
});
await resp.arrayBuffer();
await promise;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerStreamDuplex() {
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
httpConn = Deno.serveHttp(conn);
const evt = await httpConn.nextRequest();
assert(evt);
const { request, respondWith } = evt;
assert(request.body);
await respondWith(new Response(request.body));
})();
const ts = new TransformStream();
const writable = ts.writable.getWriter();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
method: "POST",
body: ts.readable,
});
assert(resp.body);
const reader = resp.body.getReader();
await writable.write(new Uint8Array([1]));
const chunk1 = await reader.read();
assert(!chunk1.done);
assertEquals(chunk1.value, new Uint8Array([1]));
await writable.write(new Uint8Array([2]));
const chunk2 = await reader.read();
assert(!chunk2.done);
assertEquals(chunk2.value, new Uint8Array([2]));
await writable.close();
const chunk3 = await reader.read();
assert(chunk3.done);
await promise;
httpConn!.close();
listener.close();
},
);
Deno.test({ permissions: { net: true } }, async function httpServerClose() {
const listener = Deno.listen({ port: listenPort });
const client = await Deno.connect({ port: listenPort });
const httpConn = Deno.serveHttp(await listener.accept());
client.close();
const evt = await httpConn.nextRequest();
assertEquals(evt, null);
// Note httpConn is automatically closed when "done" is reached.
listener.close();
});
Deno.test(
{ permissions: { net: true } },
async function httpServerInvalidMethod() {
const listener = Deno.listen({ port: listenPort });
const client = await Deno.connect({ port: listenPort });
const httpConn = Deno.serveHttp(await listener.accept());
await client.write(new Uint8Array([1, 2, 3]));
await assertRejects(
async () => {
await httpConn.nextRequest();
},
Deno.errors.Http,
"invalid HTTP method parsed",
);
// Note httpConn is automatically closed when it errors.
client.close();
listener.close();
},
);
Deno.test(
{ permissions: { read: true, net: true } },
async function httpServerWithTls() {
const hostname = "localhost";
const port = listenPort;
const promise = (async () => {
const listener = Deno.listenTls({
hostname,
port,
cert: Deno.readTextFileSync("cli/tests/testdata/tls/localhost.crt"),
key: Deno.readTextFileSync("cli/tests/testdata/tls/localhost.key"),
});
const conn = await listener.accept();
const httpConn = Deno.serveHttp(conn);
const evt = await httpConn.nextRequest();
assert(evt);
const { respondWith } = evt;
await respondWith(new Response("Hello World"));
// TODO(ry) If we don't call httpConn.nextRequest() here we get "error sending
// request for url (https://localhost:${listenPort}/): connection closed before
// message completed".
assertEquals(await httpConn.nextRequest(), null);
listener.close();
})();
const caCert = Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem");
const client = Deno.createHttpClient({ caCerts: [caCert] });
const resp = await fetch(`https://${hostname}:${port}/`, {
headers: { "connection": "close" },
client,
});
client.close();
const respBody = await resp.text();
assertEquals("Hello World", respBody);
await promise;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerRegressionHang() {
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
httpConn = Deno.serveHttp(conn);
const event = await httpConn.nextRequest();
assert(event);
const { request, respondWith } = event;
const reqBody = await request.text();
assertEquals("request", reqBody);
await respondWith(new Response("response"));
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
method: "POST",
body: "request",
});
const respBody = await resp.text();
assertEquals("response", respBody);
await promise;
httpConn!.close();
listener.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerCancelBodyOnResponseFailure() {
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
const httpConn = Deno.serveHttp(conn);
const event = await httpConn.nextRequest();
assert(event);
const { respondWith } = event;
let cancelReason: string;
await assertRejects(
async () => {
let interval = 0;
await respondWith(
new Response(
new ReadableStream({
start(controller) {
interval = setInterval(() => {
const message = `data: ${Date.now()}\n\n`;
controller.enqueue(new TextEncoder().encode(message));
}, 200);
},
cancel(reason) {
cancelReason = reason;
clearInterval(interval);
},
}),
),
);
},
Deno.errors.Http,
cancelReason!,
);
assert(cancelReason!);
httpConn!.close();
listener.close();
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
await resp.body!.cancel();
await promise;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerNextRequestErrorExposedInResponse() {
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
const httpConn = Deno.serveHttp(conn);
const event = await httpConn.nextRequest();
assert(event);
// Start polling for the next request before awaiting response.
const nextRequestPromise = httpConn.nextRequest();
const { respondWith } = event;
await assertRejects(
async () => {
let interval = 0;
await respondWith(
new Response(
new ReadableStream({
start(controller) {
interval = setInterval(() => {
const message = `data: ${Date.now()}\n\n`;
controller.enqueue(new TextEncoder().encode(message));
}, 200);
},
cancel() {
clearInterval(interval);
},
}),
),
);
},
Deno.errors.Http,
"connection closed",
);
// The error from `op_http_accept` reroutes to `respondWith()`.
assertEquals(await nextRequestPromise, null);
listener.close();
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
await resp.body!.cancel();
await promise;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerEmptyBlobResponse() {
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
httpConn = Deno.serveHttp(conn);
const event = await httpConn.nextRequest();
assert(event);
const { respondWith } = event;
await respondWith(new Response(new Blob([])));
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
const respBody = await resp.text();
assertEquals("", respBody);
await promise;
httpConn!.close();
listener.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerNextRequestResolvesOnClose() {
const httpConnList: Deno.HttpConn[] = [];
async function serve(l: Deno.Listener) {
for await (const conn of l) {
(async () => {
const c = Deno.serveHttp(conn);
httpConnList.push(c);
for await (const { respondWith } of c) {
respondWith(new Response("hello"));
}
})();
}
}
const l = Deno.listen({ port: listenPort });
serve(l);
await delay(300);
const res = await fetch(`http://localhost:${listenPort}/`);
const _text = await res.text();
// Close connection and listener.
httpConnList.forEach((conn) => conn.close());
l.close();
await delay(300);
},
);
Deno.test(
{ permissions: { net: true } },
// Issue: https://github.com/denoland/deno/issues/10870
async function httpServerHang() {
// Quick and dirty way to make a readable stream from a string. Alternatively,
// `readableStreamFromReader(file)` could be used.
function stream(s: string): ReadableStream<Uint8Array> {
return new Response(s).body!;
}
const httpConns: Deno.HttpConn[] = [];
const promise = (async () => {
let count = 0;
const listener = Deno.listen({ port: listenPort });
for await (const conn of listener) {
(async () => {
const httpConn = Deno.serveHttp(conn);
httpConns.push(httpConn);
for await (const { respondWith } of httpConn) {
respondWith(new Response(stream("hello")));
count++;
if (count >= 2) {
listener.close();
}
}
})();
}
})();
const clientConn = await Deno.connect({ port: listenPort });
const r1 = await writeRequestAndReadResponse(clientConn);
assertEquals(r1, "hello");
const r2 = await writeRequestAndReadResponse(clientConn);
assertEquals(r2, "hello");
clientConn.close();
await promise;
for (const conn of httpConns) {
conn.close();
}
},
);
Deno.test(
{ permissions: { net: true } },
// Issue: https://github.com/denoland/deno/issues/10930
async function httpServerStreamingResponse() {
// This test enqueues a single chunk for readable
// stream and waits for client to read that chunk and signal
// it before enqueueing subsequent chunk. Issue linked above
// presented a situation where enqueued chunks were not
// written to the HTTP connection until the next chunk was enqueued.
let counter = 0;
const deferreds = [
Promise.withResolvers<void>(),
Promise.withResolvers<void>(),
Promise.withResolvers<void>(),
];
async function writeRequest(conn: Deno.Conn) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
const w = new BufWriter(conn);
const r = new BufReader(conn);
const body = `GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
const writeResult = await w.write(encoder.encode(body));
assertEquals(body.length, writeResult);
await w.flush();
const tpr = new TextProtoReader(r);
const statusLine = await tpr.readLine();
assert(statusLine !== null);
const headers = await tpr.readMimeHeader();
assert(headers !== null);
const chunkedReader = chunkedBodyReader(headers, r);
const buf = new Uint8Array(5);
const dest = new Buffer();
let result: number | null;
while ((result = await chunkedReader.read(buf)) !== null) {
const len = Math.min(buf.byteLength, result);
await dest.write(buf.subarray(0, len));
// Resolve a deferred - this will make response stream to
// enqueue next chunk.
deferreds[counter - 1].resolve();
}
return decoder.decode(dest.bytes());
}
function periodicStream() {
return new ReadableStream({
start(controller) {
controller.enqueue(`${counter}\n`);
counter++;
},
async pull(controller) {
if (counter >= 3) {
return controller.close();
}
await deferreds[counter - 1].promise;
controller.enqueue(`${counter}\n`);
counter++;
},
}).pipeThrough(new TextEncoderStream());
}
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const finished = (async () => {
const conn = await listener.accept();
httpConn = Deno.serveHttp(conn);
const requestEvent = await httpConn.nextRequest();
const { respondWith } = requestEvent!;
await respondWith(new Response(periodicStream()));
})();
// start a client
const clientConn = await Deno.connect({ port: listenPort });
const r1 = await writeRequest(clientConn);
assertEquals(r1, "0\n1\n2\n");
await finished;
clientConn.close();
httpConn!.close();
listener.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpRequestLatin1Headers() {
let httpConn: Deno.HttpConn;
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
assertEquals(request.headers.get("X-Header-Test"), "á");
await respondWith(
new Response("", { headers: { "X-Header-Test": "Æ" } }),
);
})();
const clientConn = await Deno.connect({ port: listenPort });
const requestText =
`GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\nX-Header-Test: á\r\n\r\n`;
const requestBytes = new Uint8Array(requestText.length);
for (let i = 0; i < requestText.length; i++) {
requestBytes[i] = requestText.charCodeAt(i);
}
let written = 0;
while (written < requestBytes.byteLength) {
written += await clientConn.write(requestBytes.slice(written));
}
let responseText = "";
const buf = new Uint8Array(1024);
let read;
while ((read = await clientConn.read(buf)) !== null) {
httpConn!.close();
for (let i = 0; i < read; i++) {
responseText += String.fromCharCode(buf[i]);
}
}
clientConn.close();
assert(/\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/.test(responseText));
await promise;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerRequestWithoutPath() {
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
assertEquals(
new URL(request.url).href,
`http://127.0.0.1:${listenPort}/`,
);
assertEquals(await request.text(), "");
await respondWith(new Response());
})();
const clientConn = await Deno.connect({ port: listenPort });
async function writeRequest(conn: Deno.Conn) {
const encoder = new TextEncoder();
const w = new BufWriter(conn);
const r = new BufReader(conn);
const body =
`CONNECT 127.0.0.1:${listenPort} HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`;
const writeResult = await w.write(encoder.encode(body));
assertEquals(body.length, writeResult);
await w.flush();
const tpr = new TextProtoReader(r);
const statusLine = await tpr.readLine();
assert(statusLine !== null);
const m = statusLine.match(/^(.+?) (.+?) (.+?)$/);
assert(m !== null, "must be matched");
const [_, _proto, status, _ok] = m;
assertEquals(status, "200");
const headers = await tpr.readMimeHeader();
assert(headers !== null);
}
await writeRequest(clientConn);
clientConn.close();
await promise;
httpConn!.close();
},
);
Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() {
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
listener.close();
const httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
const {
response,
socket,
} = Deno.upgradeWebSocket(request);
socket.onerror = () => fail();
socket.onmessage = (m) => {
socket.send(m.data);
socket.close(1001);
};
const close = new Promise<void>((resolve) => {
socket.onclose = () => resolve();
});
await respondWith(response);
await close;
})();
const def = Promise.withResolvers<void>();
const ws = new WebSocket(`ws://localhost:${listenPort}`);
ws.onmessage = (m) => assertEquals(m.data, "foo");
ws.onerror = () => fail();
ws.onclose = () => def.resolve();
ws.onopen = () => ws.send("foo");
await def.promise;
await promise;
});
Deno.test(function httpUpgradeWebSocket() {
const request = new Request("https://deno.land/", {
headers: {
connection: "Upgrade",
upgrade: "websocket",
"sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
},
});
const { response } = Deno.upgradeWebSocket(request);
assertEquals(response.status, 101);
assertEquals(response.headers.get("connection"), "Upgrade");
assertEquals(response.headers.get("upgrade"), "websocket");
assertEquals(
response.headers.get("sec-websocket-accept"),
"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=",
);
});
Deno.test(function httpUpgradeWebSocketMultipleConnectionOptions() {
const request = new Request("https://deno.land/", {
headers: {
connection: "keep-alive, upgrade",
upgrade: "websocket",
"sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
},
});
const { response } = Deno.upgradeWebSocket(request);
assertEquals(response.status, 101);
});
Deno.test(function httpUpgradeWebSocketMultipleUpgradeOptions() {
const request = new Request("https://deno.land/", {
headers: {
connection: "upgrade",
upgrade: "websocket, foo",
"sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
},
});
const { response } = Deno.upgradeWebSocket(request);
assertEquals(response.status, 101);
});
Deno.test(function httpUpgradeWebSocketCaseInsensitiveUpgradeHeader() {
const request = new Request("https://deno.land/", {
headers: {
connection: "upgrade",
upgrade: "Websocket",
"sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
},
});
const { response } = Deno.upgradeWebSocket(request);
assertEquals(response.status, 101);
});
Deno.test(function httpUpgradeWebSocketInvalidUpgradeHeader() {
assertThrows(
() => {
const request = new Request("https://deno.land/", {
headers: {
connection: "upgrade",
upgrade: "invalid",
"sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
},
});
Deno.upgradeWebSocket(request);
},
TypeError,
"Invalid Header: 'upgrade' header must contain 'websocket'",
);
});
Deno.test(function httpUpgradeWebSocketWithoutUpgradeHeader() {
assertThrows(
() => {
const request = new Request("https://deno.land/", {
headers: {
connection: "upgrade",
"sec-websocket-key": "dGhlIHNhbXBsZSBub25jZQ==",
},
});
Deno.upgradeWebSocket(request);
},
TypeError,
"Invalid Header: 'upgrade' header must contain 'websocket'",
);
});
Deno.test(
{ permissions: { net: true } },
async function httpCookieConcatenation() {
let httpConn: Deno.HttpConn;
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
assertEquals(
new URL(request.url).href,
`http://127.0.0.1:${listenPort}/`,
);
assertEquals(await request.text(), "");
assertEquals(request.headers.get("cookie"), "foo=bar; bar=foo");
await respondWith(new Response("ok"));
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
headers: [
["connection", "close"],
["cookie", "foo=bar"],
["cookie", "bar=foo"],
],
});
const text = await resp.text();
assertEquals(text, "ok");
await promise;
httpConn!.close();
},
);
// https://github.com/denoland/deno/issues/11651
Deno.test({ permissions: { net: true } }, async function httpServerPanic() {
const listener = Deno.listen({ port: listenPort });
const client = await Deno.connect({ port: listenPort });
const conn = await listener.accept();
const httpConn = Deno.serveHttp(conn);
// This message is incomplete on purpose, we'll forcefully close client connection
// after it's flushed to cause connection to error out on the server side.
const encoder = new TextEncoder();
await client.write(encoder.encode("GET / HTTP/1.1"));
httpConn.nextRequest();
await client.write(encoder.encode("\r\n\r\n"));
httpConn!.close();
client.close();
listener.close();
});
Deno.test(
{ permissions: { net: true, write: true, read: true } },
async function httpServerCorrectSizeResponse() {
const tmpFile = await Deno.makeTempFile();
using file = await Deno.open(tmpFile, { write: true, read: true });
await file.write(new Uint8Array(70 * 1024).fill(1)); // 70kb sent in 64kb + 6kb chunks
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
httpConn = Deno.serveHttp(conn);
const ev = await httpConn.nextRequest();
const { respondWith } = ev!;
const f = await Deno.open(tmpFile, { read: true });
await respondWith(new Response(f.readable, { status: 200 }));
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
const body = await resp.arrayBuffer();
assertEquals(body.byteLength, 70 * 1024);
await promise;
httpConn!.close();
listener.close();
},
);
Deno.test(
{ permissions: { net: true, write: true, read: true } },
async function httpServerClosedStream() {
const listener = Deno.listen({ port: listenPort });
const client = await Deno.connect({ port: listenPort });
await client.write(new TextEncoder().encode(
`GET / HTTP/1.0\r\n\r\n`,
));
const conn = await listener.accept();
const httpConn = Deno.serveHttp(conn);
const ev = await httpConn.nextRequest();
const { respondWith } = ev!;
const tmpFile = await Deno.makeTempFile();
const file = await Deno.open(tmpFile, { write: true, read: true });
await file.write(new TextEncoder().encode("hello"));
const reader = await file.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
assert(value);
}
let didThrow = false;
try {
await respondWith(new Response(file.readable));
} catch {
// pass
didThrow = true;
}
assert(didThrow);
httpConn!.close();
listener.close();
client.close();
},
);
// https://github.com/denoland/deno/issues/11595
Deno.test(
{ permissions: { net: true } },
async function httpServerIncompleteMessage() {
const listener = Deno.listen({ port: listenPort });
const client = await Deno.connect({ port: listenPort });
await client.write(new TextEncoder().encode(
`GET / HTTP/1.0\r\n\r\n`,
));
const conn = await listener.accept();
const httpConn = Deno.serveHttp(conn);
const ev = await httpConn.nextRequest();
const { respondWith } = ev!;
const errors: Error[] = [];
const readable = new ReadableStream({
async pull(controller) {
client.close();
await delay(1000);
controller.enqueue(new TextEncoder().encode(
"written to the writable side of a TransformStream",
));
controller.close();
},
cancel(error) {
errors.push(error);
},
});
const res = new Response(readable);
await respondWith(res).catch((error: Error) => errors.push(error));
httpConn!.close();
listener.close();
assert(errors.length >= 1);
for (const error of errors) {
assertEquals(error.name, "Http");
assert(error.message.includes("connection"));
}
},
);
// https://github.com/denoland/deno/issues/11743
Deno.test(
{ permissions: { net: true } },
async function httpServerDoesntLeakResources() {
const listener = Deno.listen({ port: listenPort });
const [conn, clientConn] = await Promise.all([
listener.accept(),
Deno.connect({ port: listenPort }),
]);
const httpConn = Deno.serveHttp(conn);
await Promise.all([
httpConn.nextRequest(),
clientConn.write(new TextEncoder().encode(
`GET / HTTP/1.1\r\nHost: 127.0.0.1:${listenPort}\r\n\r\n`,
)),
]);
httpConn!.close();
listener.close();
clientConn.close();
},
);
// https://github.com/denoland/deno/issues/11926
// verify that the only new resource is "httpConnection", to make
// sure "request" resource is closed even if its body was not read
// by server handler
Deno.test(
{ permissions: { net: true } },
async function httpServerDoesntLeakResources2() {
let listener: Deno.Listener;
let httpConn: Deno.HttpConn;
const promise = (async () => {
listener = Deno.listen({ port: listenPort });
for await (const conn of listener) {
httpConn = Deno.serveHttp(conn);
for await (const { request, respondWith } of httpConn) {
assertEquals(
new URL(request.url).href,
`http://127.0.0.1:${listenPort}/`,
);
// not reading request body on purpose
respondWith(new Response("ok"));
}
}
})();
const response = await fetch(`http://127.0.0.1:${listenPort}`, {
method: "POST",
body: "hello world",
});
await response.text();
listener!.close();
httpConn!.close();
await promise;
},
);
// https://github.com/denoland/deno/pull/12216
Deno.test(
{ permissions: { net: true } },
async function droppedConnSenderNoPanic() {
async function server() {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
const http = Deno.serveHttp(conn);
const evt = await http.nextRequest();
http.close();
try {
await evt!.respondWith(new Response("boom"));
} catch {
// Ignore error.
}
listener.close();
}
async function client() {
try {
const resp = await fetch(`http://127.0.0.1:${listenPort}/`);
await resp.body?.cancel();
} catch {
// Ignore error
}
}
await Promise.all([server(), client()]);
},
);
// https://github.com/denoland/deno/issues/12193
Deno.test(
{ permissions: { net: true } },
async function httpConnConcurrentNextRequestCalls() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ hostname, port });
async function server() {
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const promises = new Array(10).fill(null).map(async (_, i) => {
const event = await httpConn.nextRequest();
assert(event);
const { pathname } = new URL(event.request.url);
assertStrictEquals(pathname, `/${i}`);
const response = new Response(`Response #${i}`);
await event.respondWith(response);
});
await Promise.all(promises);
}
async function client() {
for (let i = 0; i < 10; i++) {
const response = await fetch(`http://${hostname}:${port}/${i}`);
const body = await response.text();
assertStrictEquals(body, `Response #${i}`);
}
}
await Promise.all([server(), delay(100).then(client)]);
httpConn!.close();
listener.close();
},
);
// https://github.com/denoland/deno/pull/12704
// https://github.com/denoland/deno/pull/12732
Deno.test(
{ permissions: { net: true } },
async function httpConnAutoCloseDelayedOnUpgrade() {
const hostname = "localhost";
const port = listenPort;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
const httpConn = Deno.serveHttp(tcpConn);
const event1 = await httpConn.nextRequest() as Deno.RequestEvent;
const event2Promise = httpConn.nextRequest();
const { socket, response } = Deno.upgradeWebSocket(event1.request);
socket.onmessage = (event) => socket.send(event.data);
const socketClosed = new Promise<void>((resolve) => {
socket.onclose = () => resolve();
});
event1.respondWith(response);
const event2 = await event2Promise;
assertStrictEquals(event2, null);
listener.close();
await socketClosed;
}
async function client() {
const socket = new WebSocket(`ws://${hostname}:${port}/`);
socket.onopen = () => socket.send("bla bla");
const closed = new Promise<void>((resolve) => {
socket.onclose = () => resolve();
});
const { data } = await new Promise<MessageEvent<string>>((res) =>
socket.onmessage = res
);
assertStrictEquals(data, "bla bla");
socket.close();
await closed;
}
await Promise.all([server(), client()]);
},
);
// https://github.com/denoland/deno/issues/12741
// https://github.com/denoland/deno/pull/12746
// https://github.com/denoland/deno/pull/12798
Deno.test(
{ permissions: { net: true, run: true } },
async function httpServerDeleteRequestHasBody() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ hostname, port });
async function server() {
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const event = await httpConn.nextRequest() as Deno.RequestEvent;
assert(event.request.body);
const response = new Response();
await event.respondWith(response);
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = ["-X", "DELETE", url];
const { success } = await new Deno.Command("curl", {
args,
stdout: "null",
stderr: "null",
}).output();
assert(success);
}
await Promise.all([server(), client()]);
httpConn!.close();
listener.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerRespondNonAsciiUint8Array() {
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.body, null);
await respondWith(
new Response(new Uint8Array([128]), {}),
);
})();
const resp = await fetch(`http://localhost:${listenPort}/`);
assertEquals(resp.status, 200);
const body = await resp.arrayBuffer();
assertEquals(new Uint8Array(body), new Uint8Array([128]));
await promise;
httpConn!.close();
},
);
function tmpUnixSocketPath(): string {
const folder = Deno.makeTempDirSync();
return join(folder, "socket");
}
// https://github.com/denoland/deno/pull/13628
Deno.test(
{
ignore: Deno.build.os === "windows",
permissions: { read: true, write: true },
},
async function httpServerOnUnixSocket() {
const filePath = tmpUnixSocketPath();
let httpConn: Deno.HttpConn;
const promise = (async () => {
const listener = Deno.listen({ path: filePath, transport: "unix" });
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
const url = new URL(request.url);
assertEquals(url.protocol, "http+unix:");
assertEquals(decodeURIComponent(url.host), filePath);
assertEquals(url.pathname, "/path/name");
await respondWith(new Response("", { headers: {} }));
})();
// fetch() does not supports unix domain sockets yet https://github.com/denoland/deno/issues/8821
const conn = await Deno.connect({ path: filePath, transport: "unix" });
const encoder = new TextEncoder();
// The Host header must be present and empty if it is not a Internet host name (RFC2616, Section 14.23)
const body = `GET /path/name HTTP/1.1\r\nHost:\r\n\r\n`;
const writeResult = await conn.write(encoder.encode(body));
assertEquals(body.length, writeResult);
const resp = new Uint8Array(200);
const readResult = await conn.read(resp);
assertEquals(readResult, 138);
conn.close();
await promise;
httpConn!.close();
},
);
/* Automatic Body Compression */
const decoder = new TextDecoder();
Deno.test({
name: "http server compresses body - check headers",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
const listener = Deno.listen({ hostname, port });
const data = { hello: "deno", now: "with", compressed: "body" };
let httpConn: Deno.HttpConn;
async function server() {
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const response = new Response(JSON.stringify(data), {
headers: { "content-type": "application/json" },
});
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(output.includes("content-encoding: gzip\r\n"));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server compresses body - check body",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
const listener = Deno.listen({ hostname, port });
const data = { hello: "deno", now: "with", compressed: "body" };
let httpConn: Deno.HttpConn;
async function server() {
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const response = new Response(JSON.stringify(data), {
headers: { "content-type": "application/json" },
});
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const proc = new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).spawn();
const status = await proc.status;
assert(status.success);
const stdout = proc.stdout
.pipeThrough(new DecompressionStream("gzip"))
.pipeThrough(new TextDecoderStream());
let body = "";
for await (const chunk of stdout) {
body += chunk;
}
assertEquals(JSON.parse(body), data);
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server doesn't compress small body",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const response = new Response(
JSON.stringify({ hello: "deno" }),
{
headers: { "content-type": "application/json" },
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout).toLocaleLowerCase();
assert(output.includes("vary: accept-encoding\r\n"));
assert(!output.includes("content-encoding: "));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server respects accept-encoding weights",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(
request.headers.get("Accept-Encoding"),
"gzip;q=0.8, br;q=1.0, *;q=0.1",
);
const response = new Response(
JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
{
headers: { "content-type": "application/json" },
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip;q=0.8, br;q=1.0, *;q=0.1",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(output.includes("content-encoding: br\r\n"));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server augments vary header",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const response = new Response(
JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
{
headers: { "content-type": "application/json", vary: "Accept" },
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding, Accept\r\n"));
assert(output.includes("content-encoding: gzip\r\n"));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server weakens etag header",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const response = new Response(
JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
{
headers: {
"content-type": "application/json",
etag: "33a64df551425fcc55e4d42a148795d9f25f89d4",
},
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"curl",
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(
output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"),
);
assert(output.includes("content-encoding: gzip\r\n"));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server passes through weak etag header",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const response = new Response(
JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
{
headers: {
"content-type": "application/json",
etag: "W/33a64df551425fcc55e4d42a148795d9f25f89d4",
},
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(
output.includes("etag: W/33a64df551425fcc55e4d42a148795d9f25f89d4\r\n"),
);
assert(output.includes("content-encoding: gzip\r\n"));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server doesn't compress body when no-transform is set",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const response = new Response(
JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
{
headers: {
"content-type": "application/json",
"cache-control": "no-transform",
},
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(!output.includes("content-encoding: "));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server doesn't compress body when content-range is set",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const response = new Response(
JSON.stringify({ hello: "deno", now: "with", compressed: "body" }),
{
headers: {
"content-type": "application/json",
"content-range": "bytes 200-100/67589",
},
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(!output.includes("content-encoding: "));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server compresses streamed bodies - check headers",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
const encoder = new TextEncoder();
const listener = Deno.listen({ hostname, port });
const data = { hello: "deno", now: "with", compressed: "body" };
let httpConn: Deno.HttpConn;
async function server() {
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const bodyInit = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(JSON.stringify(data)));
controller.close();
},
});
const response = new Response(
bodyInit,
{ headers: { "content-type": "application/json" } },
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"curl",
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(output.includes("content-encoding: gzip\r\n"));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server compresses streamed bodies - check body",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
const encoder = new TextEncoder();
const listener = Deno.listen({ hostname, port });
const data = { hello: "deno", now: "with", compressed: "body" };
let httpConn: Deno.HttpConn;
async function server() {
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const bodyInit = new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(JSON.stringify(data)));
controller.close();
},
});
const response = new Response(
bodyInit,
{ headers: { "content-type": "application/json" } },
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const proc = new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).spawn();
const status = await proc.status;
assert(status.success);
const stdout = proc.stdout
.pipeThrough(new DecompressionStream("gzip"))
.pipeThrough(new TextDecoderStream());
let body = "";
for await (const chunk of stdout) {
body += chunk;
}
assertEquals(JSON.parse(body), data);
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server updates content-length header if compression is applied",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let contentLength: string;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const body = JSON.stringify({
hello: "deno",
now: "with",
compressed: "body",
});
contentLength = String(body.length);
const response = new Response(
body,
{
headers: {
"content-type": "application/json",
"content-length": contentLength,
},
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"-i",
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
];
const { success, stdout } = await new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).output();
assert(success);
const output = decoder.decode(stdout);
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(output.includes("content-encoding: gzip\r\n"));
// Ensure the content-length header is updated (but don't check the exact length).
assert(!output.includes(`content-length: ${contentLength}\r\n`));
assert(output.includes("content-length: "));
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server compresses when accept-encoding is deflate, gzip",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let contentLength: string;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip");
const body = "x".repeat(10000);
contentLength = String(body.length);
const response = new Response(
body,
{
headers: {
"content-length": contentLength,
},
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const cmd = [
"curl",
"-i",
"--request",
"GET",
"--url",
url,
// "--compressed", // Windows curl does not support --compressed
"--header",
"Accept-Encoding: deflate, gzip",
];
// deno-lint-ignore no-deprecated-deno-api
const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" });
const status = await proc.status();
assert(status.success);
const output = decoder.decode(await proc.output());
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(output.includes("content-encoding: gzip\r\n"));
// Ensure the content-length header is updated.
assert(!output.includes(`content-length: ${contentLength}\r\n`));
assert(output.includes("content-length: "));
proc.close();
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test({
name: "http server custom content-encoding is left untouched",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
let contentLength: string;
let httpConn: Deno.HttpConn;
async function server() {
const listener = Deno.listen({ hostname, port });
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "deflate, gzip");
const body = new Uint8Array([3, 1, 4, 1]);
contentLength = String(body.length);
const response = new Response(
body,
{
headers: {
"content-length": contentLength,
"content-encoding": "arbitrary",
},
},
);
await respondWith(response);
listener.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const cmd = [
"curl",
"-i",
"--request",
"GET",
"--url",
url,
// "--compressed", // Windows curl does not support --compressed
"--header",
"Accept-Encoding: deflate, gzip",
];
// deno-lint-ignore no-deprecated-deno-api
const proc = Deno.run({ cmd, stdout: "piped", stderr: "null" });
const status = await proc.status();
assert(status.success);
const output = decoder.decode(await proc.output());
assert(output.includes("vary: Accept-Encoding\r\n"));
assert(output.includes("content-encoding: arbitrary\r\n"));
proc.close();
}
await Promise.all([server(), client()]);
httpConn!.close();
},
});
Deno.test(
{ permissions: { net: true } },
async function httpServerReadLargeBodyWithContentLength() {
const TLS_PACKET_SIZE = 16 * 1024 + 256;
// We want the body to be read in multiple packets
const body = "aa\n" + "deno.land large body\n".repeat(TLS_PACKET_SIZE) +
"zz";
let httpConn: Deno.HttpConn;
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
assertEquals(await request.text(), body);
await respondWith(new Response(body));
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
method: "POST",
headers: { "connection": "close" },
body,
});
const text = await resp.text();
assertEquals(text, body);
await promise;
httpConn!.close();
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerReadLargeBodyWithTransferChunked() {
const TLS_PACKET_SIZE = 16 * 1024 + 256;
// We want the body to be read in multiple packets
const chunks = [
"aa\n",
"deno.land large body\n".repeat(TLS_PACKET_SIZE),
"zz",
];
const body = chunks.join("");
const stream = new TransformStream();
const writer = stream.writable.getWriter();
for (const chunk of chunks) {
writer.write(new TextEncoder().encode(chunk));
}
writer.close();
let httpConn: Deno.HttpConn;
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
assertEquals(await request.text(), body);
await respondWith(new Response(body));
})();
const resp = await fetch(`http://127.0.0.1:${listenPort}/`, {
method: "POST",
headers: { "connection": "close" },
body: stream.readable,
});
const text = await resp.text();
assertEquals(text, body);
await promise;
httpConn!.close();
},
);
Deno.test(
{
permissions: { net: true },
},
async function httpServerWithoutExclusiveAccessToTcp() {
const port = listenPort;
const listener = Deno.listen({ port });
const [clientConn, serverConn] = await Promise.all([
Deno.connect({ port }),
listener.accept(),
]);
const buf = new Uint8Array(128);
const readPromise = serverConn.read(buf);
assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
clientConn.close();
listener.close();
await readPromise;
},
);
Deno.test(
{
permissions: { net: true, read: true },
},
async function httpServerWithoutExclusiveAccessToTls() {
const hostname = "localhost";
const port = listenPort;
const listener = Deno.listenTls({
hostname,
port,
cert: await Deno.readTextFile("cli/tests/testdata/tls/localhost.crt"),
key: await Deno.readTextFile("cli/tests/testdata/tls/localhost.key"),
});
const caCerts = [
await Deno.readTextFile("cli/tests/testdata/tls/RootCA.pem"),
];
const [clientConn, serverConn] = await Promise.all([
Deno.connectTls({ hostname, port, caCerts }),
listener.accept(),
]);
await Promise.all([clientConn.handshake(), serverConn.handshake()]);
const buf = new Uint8Array(128);
const readPromise = serverConn.read(buf);
assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
clientConn.close();
listener.close();
await readPromise;
},
);
Deno.test(
{
ignore: Deno.build.os === "windows",
permissions: { read: true, write: true },
},
async function httpServerWithoutExclusiveAccessToUnixSocket() {
const filePath = tmpUnixSocketPath();
const listener = Deno.listen({ path: filePath, transport: "unix" });
const [clientConn, serverConn] = await Promise.all([
Deno.connect({ path: filePath, transport: "unix" }),
listener.accept(),
]);
const buf = new Uint8Array(128);
const readPromise = serverConn.read(buf);
assertThrows(() => Deno.serveHttp(serverConn), Deno.errors.BadResource);
clientConn.close();
listener.close();
await readPromise;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpServerRequestResponseClone() {
const body = "deno".repeat(64 * 1024);
let httpConn: Deno.HttpConn;
const listener = Deno.listen({ port: listenPort });
const promise = (async () => {
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const reqEvent = await httpConn.nextRequest();
assert(reqEvent);
const { request, respondWith } = reqEvent;
const clone = request.clone();
const reader = clone.body!.getReader();
// get first chunk from branch2
const clonedChunks = [];
const { value, done } = await reader.read();
assert(!done);
clonedChunks.push(value);
// consume request after first chunk single read
// readAll should read correctly the rest of the body.
// firstChunk should be in the stream internal buffer
const body1 = await request.text();
while (true) {
const { value, done } = await reader.read();
if (done) break;
clonedChunks.push(value);
}
let offset = 0;
const body2 = new Uint8Array(body.length);
for (const chunk of clonedChunks) {
body2.set(chunk, offset);
offset += chunk.byteLength;
}
assertEquals(body1, body);
assertEquals(body1, new TextDecoder().decode(body2));
await respondWith(new Response(body));
})();
const response = await fetch(`http://localhost:${listenPort}`, {
body,
method: "POST",
});
const clone = response.clone();
assertEquals(await response.text(), await clone.text());
await promise;
httpConn!.close();
},
);
Deno.test({
name: "http server compresses and flushes each chunk of a streamed resource",
permissions: { net: true, run: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
const port2 = listenPort2;
const encoder = new TextEncoder();
const listener = Deno.listen({ hostname, port });
const listener2 = Deno.listen({ hostname, port: port2 });
let httpConn: Deno.HttpConn;
async function server() {
const tcpConn = await listener.accept();
httpConn = Deno.serveHttp(tcpConn);
const e = await httpConn.nextRequest();
assert(e);
const { request, respondWith } = e;
assertEquals(request.headers.get("Accept-Encoding"), "gzip, deflate, br");
const resp = await fetch(`http://${hostname}:${port2}/`);
await respondWith(resp);
listener.close();
}
const ts = new TransformStream();
const writer = ts.writable.getWriter();
writer.write(encoder.encode("hello"));
let httpConn2: Deno.HttpConn;
async function server2() {
const tcpConn = await listener2.accept();
httpConn2 = Deno.serveHttp(tcpConn);
const e = await httpConn2.nextRequest();
assert(e);
await e.respondWith(
new Response(ts.readable, {
headers: { "Content-Type": "text/plain" },
}),
);
listener2.close();
}
async function client() {
const url = `http://${hostname}:${port}/`;
const args = [
"--request",
"GET",
"--url",
url,
"--header",
"Accept-Encoding: gzip, deflate, br",
"--no-buffer",
];
const proc = new Deno.Command("curl", {
args,
stderr: "null",
stdout: "piped",
}).spawn();
const stdout = proc.stdout
.pipeThrough(new DecompressionStream("gzip"))
.pipeThrough(new TextDecoderStream());
let body = "";
for await (const chunk of stdout) {
body += chunk;
if (body === "hello") {
writer.write(encoder.encode(" world"));
writer.close();
}
}
assertEquals(body, "hello world");
const status = await proc.status;
assert(status.success);
}
await Promise.all([server(), server2(), client()]);
httpConn!.close();
httpConn2!.close();
},
});
Deno.test("case insensitive comma value finder", async (t) => {
const cases = /** @type {[string, boolean][]} */ ([
["websocket", true],
["wEbSOcKET", true],
[",wEbSOcKET", true],
[",wEbSOcKET,", true],
[", wEbSOcKET ,", true],
["test, wEbSOcKET ,", true],
["test ,\twEbSOcKET\t\t ,", true],
["test , wEbSOcKET", true],
["test, asdf,web,wEbSOcKET", true],
["test, asdf,web,wEbSOcKETs", false],
["test, asdf,awebsocket,wEbSOcKETs", false],
]);
const findValue = buildCaseInsensitiveCommaValueFinder("websocket");
for (const [input, expected] of cases) {
await t.step(input.toString(), () => {
const actual = findValue(input);
assertEquals(actual, expected);
});
}
});
async function httpServerWithErrorBody(
listener: Deno.Listener,
compression: boolean,
): Promise<Deno.HttpConn> {
const conn = await listener.accept();
listener.close();
const httpConn = Deno.serveHttp(conn);
const e = await httpConn.nextRequest();
assert(e);
const { respondWith } = e;
const originalErr = new Error("boom");
const rs = new ReadableStream({
async start(controller) {
controller.enqueue(new Uint8Array([65]));
await delay(1000);
controller.error(originalErr);
},
});
const init = compression ? { headers: { "content-type": "text/plain" } } : {};
const response = new Response(rs, init);
const err = await assertRejects(() => respondWith(response));
assert(err === originalErr);
return httpConn;
}
for (const compression of [true, false]) {
Deno.test({
name: `http server errors stream if response body errors (http/1.1${
compression ? " + compression" : ""
})`,
permissions: { net: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
const listener = Deno.listen({ hostname, port });
const server = httpServerWithErrorBody(listener, compression);
const conn = await Deno.connect({ hostname, port });
const msg = new TextEncoder().encode(
`GET / HTTP/1.1\r\nHost: ${hostname}:${port}\r\n\r\n`,
);
const nwritten = await conn.write(msg);
assertEquals(nwritten, msg.byteLength);
const buf = new Uint8Array(1024);
const nread = await conn.read(buf);
assert(nread);
const data = new TextDecoder().decode(buf.subarray(0, nread));
assert(data.endsWith("1\r\nA\r\n"));
const nread2 = await conn.read(buf); // connection should be closed now because the stream errored
assertEquals(nread2, null);
conn.close();
const httpConn = await server;
httpConn.close();
},
});
Deno.test({
name: `http server errors stream if response body errors (http/1.1 + fetch${
compression ? " + compression" : ""
})`,
permissions: { net: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
const listener = Deno.listen({ hostname, port });
const server = httpServerWithErrorBody(listener, compression);
const resp = await fetch(`http://${hostname}:${port}/`);
assert(resp.body);
const reader = resp.body.getReader();
const result = await reader.read();
assert(!result.done);
assertEquals(result.value, new Uint8Array([65]));
const err = await assertRejects(() => reader.read());
assert(err instanceof TypeError);
assert(err.message.includes("unexpected EOF"));
const httpConn = await server;
httpConn.close();
},
});
Deno.test({
name: `http server errors stream if response body errors (http/2 + fetch${
compression ? " + compression" : ""
}))`,
permissions: { net: true, read: true },
async fn() {
const hostname = "localhost";
const port = listenPort;
const listener = Deno.listenTls({
hostname,
port,
cert: await Deno.readTextFile("cli/tests/testdata/tls/localhost.crt"),
key: await Deno.readTextFile("cli/tests/testdata/tls/localhost.key"),
alpnProtocols: ["h2"],
});
const server = httpServerWithErrorBody(listener, compression);
const caCert = Deno.readTextFileSync("cli/tests/testdata/tls/RootCA.pem");
const client = Deno.createHttpClient({ caCerts: [caCert] });
const resp = await fetch(`https://${hostname}:${port}/`, { client });
client.close();
assert(resp.body);
const reader = resp.body.getReader();
const result = await reader.read();
assert(!result.done);
assertEquals(result.value, new Uint8Array([65]));
const err = await assertRejects(() => reader.read());
assert(err instanceof TypeError);
assert(err.message.includes("unexpected internal error encountered"));
const httpConn = await server;
httpConn.close();
},
});
}
Deno.test({
name: "request signal is aborted when response errors",
permissions: { net: true },
async fn() {
let httpConn: Deno.HttpConn;
const promise = (async () => {
const listener = Deno.listen({ port: listenPort });
const conn = await listener.accept();
listener.close();
httpConn = Deno.serveHttp(conn);
const ev = await httpConn.nextRequest();
const { request, respondWith } = ev!;
await delay(300);
await assertRejects(() => respondWith(new Response("Hello World")));
assert(request.signal.aborted);
})();
const abortController = new AbortController();
fetch(`http://127.0.0.1:${listenPort}/`, {
signal: abortController.signal,
}).catch(() => {
// ignore
});
await delay(100);
abortController.abort();
await promise;
httpConn!.close();
},
});
Deno.test(
async function httpConnExplicitResourceManagement() {
let promise;
{
const listen = Deno.listen({ port: listenPort });
promise = fetch(`http://localhost:${listenPort}/`).catch(() => null);
const serverConn = await listen.accept();
listen.close();
using _httpConn = Deno.serveHttp(serverConn);
}
const response = await promise;
assertEquals(response, null);
},
);
function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
// Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
const tp = new TextProtoReader(r);
let finished = false;
const chunks: Array<{
offset: number;
data: Uint8Array;
}> = [];
async function read(buf: Uint8Array): Promise<number | null> {
if (finished) return null;
const [chunk] = chunks;
if (chunk) {
const chunkRemaining = chunk.data.byteLength - chunk.offset;
const readLength = Math.min(chunkRemaining, buf.byteLength);
for (let i = 0; i < readLength; i++) {
buf[i] = chunk.data[chunk.offset + i];
}
chunk.offset += readLength;
if (chunk.offset === chunk.data.byteLength) {
chunks.shift();
// Consume \r\n;
if ((await tp.readLine()) === null) {
throw new Deno.errors.UnexpectedEof();
}
}
return readLength;
}
const line = await tp.readLine();
if (line === null) throw new Deno.errors.UnexpectedEof();
// TODO(bartlomieju): handle chunk extension
const [chunkSizeString] = line.split(";");
const chunkSize = parseInt(chunkSizeString, 16);
if (Number.isNaN(chunkSize) || chunkSize < 0) {
throw new Deno.errors.InvalidData("Invalid chunk size");
}
if (chunkSize > 0) {
if (chunkSize > buf.byteLength) {
let eof = await r.readFull(buf);
if (eof === null) {
throw new Deno.errors.UnexpectedEof();
}
const restChunk = new Uint8Array(chunkSize - buf.byteLength);
eof = await r.readFull(restChunk);
if (eof === null) {
throw new Deno.errors.UnexpectedEof();
} else {
chunks.push({
offset: 0,
data: restChunk,
});
}
return buf.byteLength;
} else {
const bufToFill = buf.subarray(0, chunkSize);
const eof = await r.readFull(bufToFill);
if (eof === null) {
throw new Deno.errors.UnexpectedEof();
}
// Consume \r\n
if ((await tp.readLine()) === null) {
throw new Deno.errors.UnexpectedEof();
}
return chunkSize;
}
} else {
assert(chunkSize === 0);
// Consume \r\n
if ((await r.readLine()) === null) {
throw new Deno.errors.UnexpectedEof();
}
await readTrailers(h, r);
finished = true;
return null;
}
}
return { read };
}
async function readTrailers(
headers: Headers,
r: BufReader,
) {
const trailers = parseTrailer(headers.get("trailer"));
if (trailers == null) return;
const trailerNames = [...trailers.keys()];
const tp = new TextProtoReader(r);
const result = await tp.readMimeHeader();
if (result == null) {
throw new Deno.errors.InvalidData("Missing trailer header.");
}
const undeclared = [...result.keys()].filter(
(k) => !trailerNames.includes(k),
);
if (undeclared.length > 0) {
throw new Deno.errors.InvalidData(
`Undeclared trailers: ${Deno.inspect(undeclared)}.`,
);
}
for (const [k, v] of result) {
headers.append(k, v);
}
const missingTrailers = trailerNames.filter((k) => !result.has(k));
if (missingTrailers.length > 0) {
throw new Deno.errors.InvalidData(
`Missing trailers: ${Deno.inspect(missingTrailers)}.`,
);
}
headers.delete("trailer");
}
function parseTrailer(field: string | null): Headers | undefined {
if (field == null) {
return undefined;
}
const trailerNames = field.split(",").map((v) => v.trim().toLowerCase());
if (trailerNames.length === 0) {
throw new Deno.errors.InvalidData("Empty trailer header.");
}
const prohibited = trailerNames.filter((k) => isProhibitedForTrailer(k));
if (prohibited.length > 0) {
throw new Deno.errors.InvalidData(
`Prohibited trailer names: ${Deno.inspect(prohibited)}.`,
);
}
return new Headers(trailerNames.map((key) => [key, ""]));
}
function isProhibitedForTrailer(key: string): boolean {
const s = new Set(["transfer-encoding", "content-length", "trailer"]);
return s.has(key.toLowerCase());
}