Skip to content

Commit f105800

Browse files
authored
feat: Native http binding (#158)
1 parent 4838953 commit f105800

11 files changed

Lines changed: 285 additions & 38 deletions

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ types:
44
deno run -A tools/gen_types.ts
55
deno fmt types/**/*.ts
66
test:
7-
deno test -A *_test.ts
7+
deno test -A --unstable
88
build:
99
docker build -t servest/site .
1010
bench:

_adapter.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright 2019-2020 Yusuke Sakurai. All rights reserved. MIT license.
2+
import { createBodyParser } from "./body_parser.ts";
3+
import { readRequest, setupBodyInit, writeResponse } from "./serveio.ts";
4+
import {
5+
BodyReader,
6+
IncomingRequest,
7+
ServeOptions,
8+
ServerResponse,
9+
} from "./server.ts";
10+
import { BufReader, BufWriter } from "./vendor/https/deno.land/std/io/bufio.ts";
11+
import { closableBodyReader, noopReader, streamReader } from "./_readers.ts";
12+
13+
export interface HttpApiAdapter {
14+
next(opts: ServeOptions): Promise<IncomingRequest | undefined>;
15+
respond(resp: ServerResponse): Promise<void>;
16+
close(): void;
17+
}
18+
19+
export function classicAdapter({ conn, bufReader, bufWriter }: {
20+
conn: Deno.Conn;
21+
bufReader: BufReader;
22+
bufWriter: BufWriter;
23+
}): HttpApiAdapter {
24+
return {
25+
async next(opts) {
26+
return readRequest(bufReader, opts);
27+
},
28+
async respond(resp) {
29+
await writeResponse(bufWriter, resp);
30+
},
31+
close() {
32+
conn.close();
33+
},
34+
};
35+
}
36+
37+
export interface RequestEvent {
38+
readonly request: Request;
39+
respondWith(r: Response | Promise<Response>): void;
40+
}
41+
42+
export interface HttpConn extends AsyncIterable<RequestEvent> {
43+
readonly rid: number;
44+
45+
nextRequest(): Promise<RequestEvent | null>;
46+
close(): void;
47+
}
48+
49+
export function nativeAdapter(conn: Deno.Conn): HttpApiAdapter {
50+
// @ts-ignore
51+
const http: HttpConn = Deno.serveHttp(conn);
52+
let ev: RequestEvent | null;
53+
let closed = false;
54+
return {
55+
async next() {
56+
ev = await http.nextRequest();
57+
if (!ev) {
58+
closed = true;
59+
return;
60+
}
61+
return requestFromEvent(ev);
62+
},
63+
async respond(resp) {
64+
if (!ev) throw new Error("Unexpected respond");
65+
const headers = resp.headers ?? new Headers();
66+
let body: BodyInit | undefined;
67+
if (resp.body) {
68+
const [_body, contentType] = setupBodyInit(resp.body);
69+
body = _body;
70+
if (!headers.has("content-type")) {
71+
headers.set("content-type", contentType);
72+
}
73+
}
74+
// TODO: trailer
75+
try {
76+
await ev.respondWith(
77+
new Response(body, {
78+
status: resp.status,
79+
headers,
80+
}),
81+
);
82+
} finally {
83+
ev = null;
84+
}
85+
},
86+
close() {
87+
if (!closed) {
88+
http.close();
89+
}
90+
},
91+
};
92+
}
93+
94+
function requestFromEvent(ev: RequestEvent): IncomingRequest {
95+
const { pathname, search, searchParams } = new URL(
96+
ev.request.url,
97+
"http://dummy",
98+
);
99+
const { method, headers } = ev.request;
100+
const contentType = headers.get("content-type") ?? "";
101+
let body: BodyReader;
102+
if (ev.request.body) {
103+
body = closableBodyReader(streamReader(ev.request.body));
104+
} else {
105+
body = closableBodyReader(noopReader());
106+
}
107+
const bodyParser = createBodyParser({
108+
reader: body,
109+
contentType,
110+
});
111+
return {
112+
url: pathname + search,
113+
path: pathname,
114+
query: searchParams,
115+
method,
116+
proto: "HTTP/1.1",
117+
headers,
118+
cookies: new Map(),
119+
body,
120+
...bodyParser,
121+
};
122+
}

_adapter_test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2019-2020 Yusuke Sakurai. All rights reserved. MIT license.
2+
import { BufReader, BufWriter } from "./vendor/https/deno.land/std/io/bufio.ts";
3+
import { assertEquals } from "./vendor/https/deno.land/std/testing/asserts.ts";
4+
import { classicAdapter, nativeAdapter } from "./_adapter.ts";
5+
import { group } from "./_test_util.ts";
6+
7+
group("adapter", ({ test }) => {
8+
async function doTest() {
9+
const resp = await fetch("http://localhost:8899", {
10+
method: "POST",
11+
body: "hello",
12+
});
13+
assertEquals(resp.status, 200);
14+
assertEquals(resp.headers.get("content-type"), "text/html");
15+
assertEquals(await resp.text(), "hello");
16+
}
17+
test("classic", async () => {
18+
async function serve() {
19+
const l = Deno.listen({ port: 8899 });
20+
const conn = await l.accept();
21+
const bufReader = new BufReader(conn);
22+
const bufWriter = new BufWriter(conn);
23+
const adapter = classicAdapter({ conn, bufReader, bufWriter });
24+
const req = await adapter.next({});
25+
await adapter.respond({
26+
status: 200,
27+
headers: new Headers({
28+
"content-type": "text/html",
29+
}),
30+
body: req!.body,
31+
});
32+
adapter.close();
33+
l.close();
34+
}
35+
serve();
36+
await doTest();
37+
});
38+
test("native", async () => {
39+
async function serve() {
40+
const l = Deno.listen({ port: 8899 });
41+
const conn = await l.accept();
42+
const adapter = nativeAdapter(conn);
43+
const req = await adapter.next({});
44+
await adapter.respond({
45+
status: 200,
46+
headers: new Headers({
47+
"content-type": "text/html",
48+
}),
49+
body: req!.body,
50+
});
51+
adapter.close();
52+
l.close();
53+
}
54+
serve();
55+
await doTest();
56+
});
57+
});

_readers.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,11 @@ export function streamReader(stream: ReadableStream<Uint8Array>): Deno.Reader {
7979
};
8080
return { read };
8181
}
82+
83+
export function noopReader(): Deno.Reader {
84+
return {
85+
async read() {
86+
return null;
87+
},
88+
};
89+
}

_version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
// Copyright 2019-2020 Yusuke Sakurai. All rights reserved. MIT license.
2-
export const Version = "v1.1.7";
2+
export const Version = "v1.3.0";

responder.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Copyright 2019-2020 Yusuke Sakurai. All rights reserved. MIT license.
2-
import Writer = Deno.Writer;
3-
import { HttpBody, ServerResponse } from "./server.ts";
2+
import { ServerResponse } from "./server.ts";
43
import { CookieSetter, cookieSetter } from "./cookie.ts";
5-
import { writeResponse } from "./serveio.ts";
64
import { basename, extname } from "./vendor/https/deno.land/std/path/mod.ts";
75
import { contentTypeByExt } from "./media_types.ts";
86
/** Basic responder for http response */
@@ -45,9 +43,7 @@ export interface Responder extends CookieSetter {
4543

4644
/** create ServerResponder object */
4745
export function createResponder(
48-
w: Writer,
49-
onResponse: (r: ServerResponse) => Promise<void> = (resp) =>
50-
writeResponse(w, resp),
46+
onResponse: (resp: ServerResponse) => Promise<void>,
5147
): Responder {
5248
const responseHeaders = new Headers();
5349
const cookie = cookieSetter(responseHeaders);

responder_test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ import {
66
assertThrowsAsync,
77
} from "./vendor/https/deno.land/std/testing/asserts.ts";
88
import { StringReader } from "./vendor/https/deno.land/std/io/readers.ts";
9-
import { readResponse } from "./serveio.ts";
9+
import { readResponse, writeResponse } from "./serveio.ts";
1010
import { group } from "./_test_util.ts";
1111

1212
group("responder", (t) => {
13+
function _createResponder(w: Deno.Writer) {
14+
return createResponder((resp) => writeResponse(w, resp));
15+
}
1316
t.test("basic", async function () {
1417
const w = new Deno.Buffer();
15-
const res = createResponder(w);
18+
const res = _createResponder(w);
1619
assert(!res.isResponded());
1720
await res.respond({
1821
status: 200,
@@ -30,7 +33,7 @@ group("responder", (t) => {
3033

3134
t.test("respond() should throw if already responded", async function () {
3235
const w = new Deno.Buffer();
33-
const res = createResponder(w);
36+
const res = _createResponder(w);
3437
await res.respond({
3538
status: 200,
3639
headers: new Headers(),
@@ -48,7 +51,7 @@ group("responder", (t) => {
4851

4952
t.test("sendFile() basic", async function () {
5053
const w = new Deno.Buffer();
51-
const res = createResponder(w);
54+
const res = _createResponder(w);
5255
await res.sendFile("./fixtures/sample.txt");
5356
const resp = await readResponse(w);
5457
assertEquals(resp.status, 200);
@@ -58,7 +61,7 @@ group("responder", (t) => {
5861

5962
t.test("sendFile() should throw if file not found", async () => {
6063
const w = new Deno.Buffer();
61-
const res = createResponder(w);
64+
const res = _createResponder(w);
6265
await assertThrowsAsync(
6366
() => res.sendFile("./fixtures/not-found"),
6467
Deno.errors.NotFound,
@@ -67,7 +70,7 @@ group("responder", (t) => {
6770

6871
t.test("sendFile() with attachment", async () => {
6972
const w = new Deno.Buffer();
70-
const res = createResponder(w);
73+
const res = _createResponder(w);
7174
await res.sendFile("./fixtures/sample.txt", {
7275
contentDisposition: "inline",
7376
});
@@ -79,7 +82,7 @@ group("responder", (t) => {
7982

8083
t.test("sendFile() with attachment", async () => {
8184
const w = new Deno.Buffer();
82-
const res = createResponder(w);
85+
const res = _createResponder(w);
8386
await res.sendFile("./fixtures/sample.txt", {
8487
contentDisposition: "attachment",
8588
});
@@ -94,7 +97,7 @@ group("responder", (t) => {
9497

9598
t.test("redirect() should set Location header", async () => {
9699
const w = new Deno.Buffer();
97-
const res = createResponder(w);
100+
const res = _createResponder(w);
98101
await res.redirect("/index.html");
99102
const { status, headers } = await readResponse(w);
100103
assertEquals(status, 302);
@@ -103,7 +106,7 @@ group("responder", (t) => {
103106

104107
t.test("redirect() should use partial body for response", async () => {
105108
const w = new Deno.Buffer();
106-
const res = createResponder(w);
109+
const res = _createResponder(w);
107110
await res.redirect("/", {
108111
status: 303,
109112
headers: new Headers({ "content-type": "text/plain" }),
@@ -117,7 +120,7 @@ group("responder", (t) => {
117120

118121
t.test("resirect() should throw error if status code is not in 300~399", async () => {
119122
const w = new Deno.Buffer();
120-
const res = createResponder(w);
123+
const res = _createResponder(w);
121124
await assertThrowsAsync(
122125
async () => {
123126
await res.redirect("/", { status: 200 });
@@ -129,7 +132,7 @@ group("responder", (t) => {
129132

130133
t.test("markResponded()", async () => {
131134
const w = new Deno.Buffer();
132-
const res = createResponder(w);
135+
const res = _createResponder(w);
133136
res.markAsResponded(200);
134137
assertEquals(res.isResponded(), true);
135138
assertEquals(res.respondedStatus(), 200);

serveio.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export function initServeOptions(opts: ServeOptions = {}): ServeOptions {
3838
let cancel = opts.cancel;
3939
let keepAliveTimeout = kDefaultKeepAliveTimeout;
4040
let readTimeout = kDefaultKeepAliveTimeout;
41+
let useNative = false;
4142
if (opts.keepAliveTimeout !== void 0) {
4243
keepAliveTimeout = opts.keepAliveTimeout;
4344
}
@@ -46,7 +47,7 @@ export function initServeOptions(opts: ServeOptions = {}): ServeOptions {
4647
}
4748
assert(keepAliveTimeout >= 0, "keepAliveTimeout must be >= 0");
4849
assert(readTimeout >= 0, "readTimeout must be >= 0");
49-
return { cancel, keepAliveTimeout, readTimeout };
50+
return { cancel, keepAliveTimeout, readTimeout, useNative };
5051
}
5152

5253
/**
@@ -264,6 +265,32 @@ export function setupBody(
264265
}
265266
return [r, chunked ? undefined : len];
266267
}
268+
269+
export function setupBodyInit(body: HttpBody): [BodyInit, string] {
270+
if (typeof body === "string") {
271+
return [body, "text/plain; charset=UTF-8"];
272+
} else if (body instanceof Uint8Array) {
273+
return [body, "application/octet-stream"];
274+
} else if (body instanceof ReadableStream) {
275+
return [body, "application/octet-stream"];
276+
} else {
277+
const buf = new Uint8Array(2048);
278+
return [
279+
new ReadableStream<Uint8Array>({
280+
async pull(ctrl) {
281+
const len = await body.read(buf);
282+
if (len != null) {
283+
ctrl.enqueue(buf.subarray(0, len));
284+
} else {
285+
ctrl.close();
286+
}
287+
},
288+
}),
289+
"application/octet-stream",
290+
];
291+
}
292+
}
293+
267294
/** write http response to writer. Content-Length, Transfer-Encoding headers are set if needed */
268295
export async function writeResponse(
269296
w: Writer,

0 commit comments

Comments
 (0)