mirror of
https://github.com/denoland/deno.git
synced 2025-09-29 13:44:47 +00:00
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:
parent
d137501a63
commit
bdffcb409f
22 changed files with 2912 additions and 336 deletions
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue