feat(ext/http): Rework Deno.serve using hyper 1.0-rc3 (#18619)

This is a rewrite of the `Deno.serve` API to live on top of hyper
1.0-rc3. The code should be more maintainable long-term, and avoids some
of the slower mpsc patterns that made the older code less efficient than
it could have been.

Missing features:

- `upgradeHttp` and `upgradeHttpRaw` (`upgradeWebSocket` is available,
however).
- Automatic compression is unavailable on responses.
This commit is contained in:
Matt Mastracci 2023-04-22 11:48:21 -06:00 committed by GitHub
parent d137501a63
commit bdffcb409f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 2912 additions and 336 deletions

View file

@ -2,6 +2,7 @@
// deno-lint-ignore-file
import { assertMatch } from "https://deno.land/std@v0.42.0/testing/asserts.ts";
import { Buffer, BufReader, BufWriter } from "../../../test_util/std/io/mod.ts";
import { TextProtoReader } from "../testdata/run/textproto.ts";
import {
@ -31,6 +32,27 @@ function onListen<T>(
};
}
Deno.test(async function httpServerShutsDownPortBeforeResolving() {
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: (_req) => new Response("ok"),
port: 4501,
signal: ac.signal,
onListen: onListen(listeningPromise),
});
await listeningPromise;
assertThrows(() => Deno.listen({ port: 4501 }));
ac.abort();
await server;
const listener = Deno.listen({ port: 4501 });
listener!.close();
});
Deno.test(async function httpServerCanResolveHostnames() {
const ac = new AbortController();
const listeningPromise = deferred();
@ -120,6 +142,71 @@ Deno.test({ permissions: { net: true } }, async function httpServerBasic() {
await server;
});
Deno.test({ permissions: { net: true } }, async function httpServerOnError() {
const ac = new AbortController();
const promise = deferred();
const listeningPromise = deferred();
let requestStash: Request | null;
const server = Deno.serve({
handler: async (request: Request) => {
requestStash = request;
await new Promise((r) => setTimeout(r, 100));
throw "fail";
},
port: 4501,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: () => {
return new Response("failed: " + requestStash!.url, { status: 500 });
},
});
await listeningPromise;
const resp = await fetch("http://127.0.0.1:4501/", {
headers: { "connection": "close" },
});
const text = await resp.text();
ac.abort();
await server;
assertEquals(text, "failed: http://127.0.0.1:4501/");
});
Deno.test(
{ permissions: { net: true } },
async function httpServerOnErrorFails() {
const ac = new AbortController();
const promise = deferred();
const listeningPromise = deferred();
let requestStash: Request | null;
const server = Deno.serve({
handler: async (request: Request) => {
requestStash = request;
await new Promise((r) => setTimeout(r, 100));
throw "fail";
},
port: 4501,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: () => {
throw "again";
},
});
await listeningPromise;
const resp = await fetch("http://127.0.0.1:4501/", {
headers: { "connection": "close" },
});
const text = await resp.text();
ac.abort();
await server;
assertEquals(text, "Internal Server Error");
},
);
Deno.test({ permissions: { net: true } }, async function httpServerOverload1() {
const ac = new AbortController();
const promise = deferred();
@ -238,7 +325,7 @@ Deno.test(
console.log = (msg) => {
try {
const match = msg.match(/Listening on http:\/\/localhost:(\d+)\//);
assert(!!match);
assert(!!match, `Didn't match ${msg}`);
const port = +match[1];
assert(port > 0 && port < 65536);
} finally {
@ -301,6 +388,109 @@ Deno.test(
},
);
function createUrlTest(
name: string,
methodAndPath: string,
host: string | null,
expected: string,
) {
Deno.test(`httpServerUrl${name}`, async () => {
const listeningPromise: Deferred<number> = deferred();
const urlPromise = deferred();
const ac = new AbortController();
const server = Deno.serve({
handler: async (request: Request) => {
urlPromise.resolve(request.url);
return new Response("");
},
port: 0,
signal: ac.signal,
onListen: ({ port }: { port: number }) => {
listeningPromise.resolve(port);
},
onError: createOnErrorCb(ac),
});
const port = await listeningPromise;
const conn = await Deno.connect({ port });
const encoder = new TextEncoder();
const body = `${methodAndPath} HTTP/1.1\r\n${
host ? ("Host: " + host + "\r\n") : ""
}Content-Length: 5\r\n\r\n12345`;
const writeResult = await conn.write(encoder.encode(body));
assertEquals(body.length, writeResult);
try {
const expectedResult = expected.replace("HOST", "localhost").replace(
"PORT",
`${port}`,
);
assertEquals(await urlPromise, expectedResult);
} finally {
ac.abort();
await server;
conn.close();
}
});
}
createUrlTest("WithPath", "GET /path", null, "http://HOST:PORT/path");
createUrlTest(
"WithPathAndHost",
"GET /path",
"deno.land",
"http://deno.land/path",
);
createUrlTest(
"WithAbsolutePath",
"GET http://localhost/path",
null,
"http://localhost/path",
);
createUrlTest(
"WithAbsolutePathAndHost",
"GET http://localhost/path",
"deno.land",
"http://localhost/path",
);
createUrlTest(
"WithPortAbsolutePath",
"GET http://localhost:1234/path",
null,
"http://localhost:1234/path",
);
createUrlTest(
"WithPortAbsolutePathAndHost",
"GET http://localhost:1234/path",
"deno.land",
"http://localhost:1234/path",
);
createUrlTest(
"WithPortAbsolutePathAndHostWithPort",
"GET http://localhost:1234/path",
"deno.land:9999",
"http://localhost:1234/path",
);
createUrlTest("WithAsterisk", "OPTIONS *", null, "*");
createUrlTest(
"WithAuthorityForm",
"CONNECT deno.land:80",
null,
"deno.land:80",
);
// TODO(mmastrac): These should probably be 400 errors
createUrlTest("WithInvalidAsterisk", "GET *", null, "*");
createUrlTest("WithInvalidNakedPath", "GET path", null, "path");
createUrlTest(
"WithInvalidNakedAuthority",
"GET deno.land:1234",
null,
"deno.land:1234",
);
Deno.test(
{ permissions: { net: true } },
async function httpServerGetRequestBody() {
@ -536,7 +726,10 @@ Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() {
response,
socket,
} = Deno.upgradeWebSocket(request);
socket.onerror = () => fail();
socket.onerror = (e) => {
console.error(e);
fail();
};
socket.onmessage = (m) => {
socket.send(m.data);
socket.close(1001);
@ -553,7 +746,10 @@ Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() {
const def = deferred();
const ws = new WebSocket("ws://localhost:4501");
ws.onmessage = (m) => assertEquals(m.data, "foo");
ws.onerror = () => fail();
ws.onerror = (e) => {
console.error(e);
fail();
};
ws.onclose = () => def.resolve();
ws.onopen = () => ws.send("foo");
@ -562,6 +758,50 @@ Deno.test({ permissions: { net: true } }, async function httpServerWebSocket() {
await server;
});
Deno.test(
{ permissions: { net: true } },
async function httpServerWebSocketCanAccessRequest() {
const ac = new AbortController();
const listeningPromise = deferred();
const server = Deno.serve({
handler: async (request) => {
const {
response,
socket,
} = Deno.upgradeWebSocket(request);
socket.onerror = (e) => {
console.error(e);
fail();
};
socket.onmessage = (m) => {
socket.send(request.url.toString());
socket.close(1001);
};
return response;
},
port: 4501,
signal: ac.signal,
onListen: onListen(listeningPromise),
onError: createOnErrorCb(ac),
});
await listeningPromise;
const def = deferred();
const ws = new WebSocket("ws://localhost:4501");
ws.onmessage = (m) => assertEquals(m.data, "http://localhost:4501/");
ws.onerror = (e) => {
console.error(e);
fail();
};
ws.onclose = () => def.resolve();
ws.onopen = () => ws.send("foo");
await def;
ac.abort();
await server;
},
);
Deno.test(
{ permissions: { net: true } },
async function httpVeryLargeRequest() {
@ -682,47 +922,46 @@ Deno.test(
},
);
// FIXME: auto request body reading is intefering with passing it as response.
// Deno.test(
// { permissions: { net: true } },
// async function httpServerStreamDuplex() {
// const promise = deferred();
// const ac = new AbortController();
Deno.test(
{ permissions: { net: true } },
async function httpServerStreamDuplex() {
const promise = deferred();
const ac = new AbortController();
// const server = Deno.serve(request => {
// assert(request.body);
const server = Deno.serve((request) => {
assert(request.body);
// promise.resolve();
// return new Response(request.body);
// }, { port: 2333, signal: ac.signal });
promise.resolve();
return new Response(request.body);
}, { port: 2333, signal: ac.signal });
// const ts = new TransformStream();
// const writable = ts.writable.getWriter();
const ts = new TransformStream();
const writable = ts.writable.getWriter();
// const resp = await fetch("http://127.0.0.1:2333/", {
// method: "POST",
// body: ts.readable,
// });
const resp = await fetch("http://127.0.0.1:2333/", {
method: "POST",
body: ts.readable,
});
// await promise;
// 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;
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);
// ac.abort();
// await server;
// },
// );
ac.abort();
await server;
},
);
Deno.test(
{ permissions: { net: true } },
@ -867,10 +1106,10 @@ Deno.test(
let responseText = new TextDecoder("iso-8859-1").decode(buf);
clientConn.close();
assert(/\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/.test(responseText));
ac.abort();
await server;
assertMatch(responseText, /\r\n[Xx]-[Hh]eader-[Tt]est: Æ\r\n/);
},
);
@ -1355,12 +1594,11 @@ createServerLengthTest("autoResponseWithKnownLengthEmpty", {
expects_con_len: true,
});
// FIXME: https://github.com/denoland/deno/issues/15892
// createServerLengthTest("autoResponseWithUnknownLengthEmpty", {
// body: stream(""),
// expects_chunked: true,
// expects_con_len: false,
// });
createServerLengthTest("autoResponseWithUnknownLengthEmpty", {
body: stream(""),
expects_chunked: true,
expects_con_len: false,
});
Deno.test(
{ permissions: { net: true } },
@ -1841,6 +2079,7 @@ Deno.test(
method: "GET",
headers: { "connection": "close" },
});
assertEquals(resp.status, 204);
assertEquals(resp.headers.get("Content-Length"), null);
} finally {
ac.abort();
@ -2162,11 +2401,11 @@ Deno.test(
count++;
return new Response(`hello world ${count}`);
}, {
async onListen() {
const res1 = await fetch("http://localhost:9000/");
async onListen({ port }: { port: number }) {
const res1 = await fetch(`http://localhost:${port}/`);
assertEquals(await res1.text(), "hello world 1");
const res2 = await fetch("http://localhost:9000/");
const res2 = await fetch(`http://localhost:${port}/`);
assertEquals(await res2.text(), "hello world 2");
promise.resolve();
@ -2199,13 +2438,13 @@ Deno.test(
return new Response("ok");
},
signal: ac.signal,
onListen: onListen(listeningPromise),
onListen: ({ port }: { port: number }) => listeningPromise.resolve(port),
onError: createOnErrorCb(ac),
});
try {
await listeningPromise;
const resp = await fetch("http://localhost:9000/", {
const port = await listeningPromise;
const resp = await fetch(`http://localhost:${port}/`, {
headers: { connection: "close" },
method: "POST",
body: '{"sus":true}',
@ -2238,8 +2477,8 @@ Deno.test(
},
}),
), {
async onListen() {
const res1 = await fetch("http://localhost:9000/");
async onListen({ port }) {
const res1 = await fetch(`http://localhost:${port}/`);
assertEquals((await res1.text()).length, 40 * 50_000);
promise.resolve();