1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-23 15:16:54 -05:00

Redesign of http server module (#188)

This commit is contained in:
Yusuke Sakurai 2019-02-16 01:03:57 +09:00 committed by Ryan Dahl
parent 34ca60376b
commit 8569f15207
11 changed files with 646 additions and 376 deletions

View file

@ -5,13 +5,22 @@ A framework for creating HTTP/HTTPS server.
## Example ## Example
```typescript ```typescript
import { serve } from "https://deno.land/x/http/server.ts"; import { createServer } from "https://deno.land/x/http/server.ts";
const s = serve("0.0.0.0:8000"); import { encode } from "https://deno.land/x/strings/strings.ts";
async function main() { async function main() {
for await (const req of s) { const server = createServer();
req.respond({ body: new TextEncoder().encode("Hello World\n") }); server.handle("/", async (req, res) => {
} await res.respond({
status: 200,
body: encode("ok")
});
});
server.handle(new RegExp("/foo/(?<id>.+)"), async (req, res) => {
const { id } = req.match.groups;
await res.respondJson({ id });
});
server.listen("127.0.0.1:8080");
} }
main(); main();

View file

@ -10,7 +10,7 @@ import {
listenAndServe, listenAndServe,
ServerRequest, ServerRequest,
setContentLength, setContentLength,
Response ServerResponse
} from "./server.ts"; } from "./server.ts";
import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno"; import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno";
import { extname } from "../fs/path.ts"; import { extname } from "../fs/path.ts";
@ -195,14 +195,14 @@ async function serveFallback(req: ServerRequest, e: Error) {
} }
} }
function serverLog(req: ServerRequest, res: Response) { function serverLog(req: ServerRequest, res: ServerResponse) {
const d = new Date().toISOString(); const d = new Date().toISOString();
const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`;
const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`;
console.log(s); console.log(s);
} }
function setCORS(res: Response) { function setCORS(res: ServerResponse) {
if (!res.headers) { if (!res.headers) {
res.headers = new Headers(); res.headers = new Headers();
} }
@ -213,11 +213,11 @@ function setCORS(res: Response) {
); );
} }
listenAndServe(addr, async req => { listenAndServe(addr, async (req, res) => {
const fileName = req.url.replace(/\/$/, ""); const fileName = req.url.replace(/\/$/, "");
const filePath = currentDir + fileName; const filePath = currentDir + fileName;
let response: Response; let response: ServerResponse;
try { try {
const fileInfo = await stat(filePath); const fileInfo = await stat(filePath);
@ -235,7 +235,7 @@ listenAndServe(addr, async req => {
setCORS(response); setCORS(response);
} }
serverLog(req, response); serverLog(req, response);
req.respond(response); res.respond(response);
} }
}); });

View file

@ -1,6 +1,6 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import * as deno from "deno"; import * as deno from "deno";
import { serve } from "./mod.ts"; import { serve } from "./server.ts";
const addr = deno.args[1] || "127.0.0.1:4500"; const addr = deno.args[1] || "127.0.0.1:4500";
const server = serve(addr); const server = serve(addr);
@ -8,8 +8,13 @@ const server = serve(addr);
const body = new TextEncoder().encode("Hello World"); const body = new TextEncoder().encode("Hello World");
async function main(): Promise<void> { async function main(): Promise<void> {
for await (const request of server) { try {
await request.respond({ status: 200, body }); for await (const request of server) {
await request.responder.respond({ status: 200, body });
}
} catch (e) {
console.log(e.stack);
console.error(e);
} }
} }

78
http/readers.ts Normal file
View file

@ -0,0 +1,78 @@
import { Reader, ReadResult } from "deno";
import { BufReader } from "../io/bufio.ts";
import { TextProtoReader } from "../textproto/mod.ts";
import { assert } from "../testing/mod.ts";
export class BodyReader implements Reader {
total: number;
bufReader: BufReader;
constructor(reader: Reader, private contentLength: number) {
this.total = 0;
this.bufReader = new BufReader(reader);
}
async read(p: Uint8Array): Promise<ReadResult> {
if (p.length > this.contentLength - this.total) {
const buf = new Uint8Array(this.contentLength - this.total);
const [nread, err] = await this.bufReader.readFull(buf);
if (err && err !== "EOF") {
throw err;
}
p.set(buf);
this.total += nread;
assert.assert(
this.total === this.contentLength,
`${this.total}, ${this.contentLength}`
);
return { nread, eof: true };
} else {
const { nread } = await this.bufReader.read(p);
this.total += nread;
return { nread, eof: false };
}
}
}
export class ChunkedBodyReader implements Reader {
bufReader = new BufReader(this.reader);
tpReader = new TextProtoReader(this.bufReader);
constructor(private reader: Reader) {}
chunks: Uint8Array[] = [];
crlfBuf = new Uint8Array(2);
finished: boolean = false;
async read(p: Uint8Array): Promise<ReadResult> {
const [line, sizeErr] = await this.tpReader.readLine();
if (sizeErr) {
throw sizeErr;
}
const len = parseInt(line, 16);
if (len === 0) {
this.finished = true;
await this.bufReader.readFull(this.crlfBuf);
return { nread: 0, eof: true };
} else {
const buf = new Uint8Array(len);
await this.bufReader.readFull(buf);
await this.bufReader.readFull(this.crlfBuf);
this.chunks.push(buf);
}
const buf = this.chunks[0];
if (buf) {
if (buf.byteLength <= p.byteLength) {
p.set(buf);
this.chunks.shift();
return { nread: buf.byteLength, eof: false };
} else {
p.set(buf.slice(0, p.byteLength));
this.chunks[0] = buf.slice(p.byteLength, buf.byteLength);
return { nread: p.byteLength, eof: false };
}
} else {
return { nread: 0, eof: true };
}
}
}

12
http/readers_test.ts Normal file
View file

@ -0,0 +1,12 @@
import { assert, runTests, test } from "../testing/mod.ts";
import { ChunkedBodyReader } from "./readers.ts";
import { StringReader } from "../io/readers.ts";
import { Buffer, copy } from "deno";
test(async function httpChunkedBodyReader() {
const chunked = "3\r\nabc\r\n5\r\ndefgh\r\n0\r\n\r\n";
const r = new ChunkedBodyReader(new StringReader(chunked));
const w = new Buffer();
await copy(w, r);
assert.equal(w.toString(), "abcdefgh");
});

View file

@ -1,63 +1,90 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { listen, Conn, toAsyncIterator, Reader, Writer, copy } from "deno";
import { BufReader, BufState, BufWriter } from "../io/bufio.ts"; import { Conn, copy, listen, Reader, toAsyncIterator, Writer } from "deno";
import { BufReader, BufWriter } from "../io/bufio.ts";
import { TextProtoReader } from "../textproto/mod.ts"; import { TextProtoReader } from "../textproto/mod.ts";
import { STATUS_TEXT } from "./http_status.ts"; import { STATUS_TEXT } from "./http_status.ts";
import { assert } from "../testing/mod.ts"; import { assert } from "../testing/mod.ts";
import { defer, Deferred } from "../util/deferred.ts";
import { BodyReader, ChunkedBodyReader } from "./readers.ts";
import { encode } from "../strings/strings.ts";
interface Deferred { /** basic handler for http request */
promise: Promise<{}>; export type HttpHandler = (req: ServerRequest, res: ServerResponder) => unknown;
resolve: () => void;
reject: () => void; export type ServerRequest = {
/** request path with queries. always begin with / */
url: string;
/** HTTP method */
method: string;
/** requested protocol. like HTTP/1.1 */
proto: string;
/** HTTP Headers */
headers: Headers;
/** matched result for path pattern */
match: RegExpMatchArray;
/** body stream. body with "transfer-encoding: chunked" will automatically be combined into original data */
body: Reader;
};
/** basic responder for http response */
export interface ServerResponder {
respond(response: ServerResponse): Promise<void>;
respondJson(obj: any, headers?: Headers): Promise<void>;
respondText(text: string, headers?: Headers): Promise<void>;
readonly isResponded: boolean;
} }
function deferred(): Deferred { export interface ServerResponse {
let resolve, reject; /**
const promise = new Promise((res, rej) => { * HTTP status code
resolve = res; * @default 200 */
reject = rej; status?: number;
}); headers?: Headers;
return { body?: Uint8Array | Reader;
promise,
resolve,
reject
};
} }
interface ServeEnv { interface ServeEnv {
reqQueue: ServerRequest[]; reqQueue: { req: ServerRequest; conn: Conn }[];
serveDeferred: Deferred; serveDeferred: Deferred;
} }
/** Continuously read more requests from conn until EOF /** Continuously read more requests from conn until EOF
* Calls maybeHandleReq. * Calls maybeHandleReq.
* bufr is empty on a fresh TCP connection.
* Would be passed around and reused for later request on same conn
* TODO: make them async function after this change is done * TODO: make them async function after this change is done
* https://github.com/tc39/ecma262/pull/1250 * https://github.com/tc39/ecma262/pull/1250
* See https://v8.dev/blog/fast-async * See https://v8.dev/blog/fast-async
*/ */
function serveConn(env: ServeEnv, conn: Conn, bufr?: BufReader) { function serveConn(env: ServeEnv, conn: Conn) {
readRequest(conn, bufr).then(maybeHandleReq.bind(null, env, conn)); readRequest(conn)
.then(maybeHandleReq.bind(null, env, conn))
.catch(e => {
conn.close();
});
} }
function maybeHandleReq(env: ServeEnv, conn: Conn, maybeReq: any) { function maybeHandleReq(env: ServeEnv, conn: Conn, req: ServerRequest) {
const [req, _err] = maybeReq; env.reqQueue.push({ conn, req }); // push req to queue
if (_err) {
conn.close(); // assume EOF for now...
return;
}
env.reqQueue.push(req); // push req to queue
env.serveDeferred.resolve(); // signal while loop to process it env.serveDeferred.resolve(); // signal while loop to process it
} }
export async function* serve(addr: string) { /**
* iterate new http request asynchronously
* @param addr listening address. like 127.0.0.1:80
* @param cancel deferred object for cancellation of serving
* */
export async function* serve(
addr: string,
cancel: Deferred = defer()
): AsyncIterableIterator<{ req: ServerRequest; res: ServerResponder }> {
const listener = listen("tcp", addr); const listener = listen("tcp", addr);
const env: ServeEnv = { const env: ServeEnv = {
reqQueue: [], // in case multiple promises are ready reqQueue: [], // in case multiple promises are ready
serveDeferred: deferred() serveDeferred: defer()
}; };
// Routine that keeps calling accept // Routine that keeps calling accept
const acceptRoutine = () => { const acceptRoutine = () => {
const handleConn = (conn: Conn) => { const handleConn = (conn: Conn) => {
@ -65,47 +92,148 @@ export async function* serve(addr: string) {
scheduleAccept(); // schedule next accept scheduleAccept(); // schedule next accept
}; };
const scheduleAccept = () => { const scheduleAccept = () => {
listener.accept().then(handleConn); Promise.race([cancel.promise, listener.accept().then(handleConn)]);
}; };
scheduleAccept(); scheduleAccept();
}; };
acceptRoutine(); acceptRoutine();
// Loop hack to allow yield (yield won't work in callbacks)
while (true) { while (true) {
await env.serveDeferred.promise; // do race between accept, serveDeferred and cancel
env.serveDeferred = deferred(); // use a new deferred await Promise.race([env.serveDeferred.promise, cancel.promise]);
let queueToProcess = env.reqQueue; // cancellation deferred resolved
if (cancel.handled) {
break;
}
// next serve deferred
env.serveDeferred = defer();
const queueToProcess = env.reqQueue;
env.reqQueue = []; env.reqQueue = [];
for (const result of queueToProcess) { for (const { req, conn } of queueToProcess) {
yield result; if (req) {
// Continue read more from conn when user is done with the current req const res = createResponder(conn);
// Moving this here makes it easier to manage yield { req, res };
serveConn(env, result.conn, result.r); }
serveConn(env, conn);
} }
} }
listener.close(); listener.close();
} }
export async function listenAndServe( export async function listenAndServe(addr: string, handler: HttpHandler) {
addr: string,
handler: (req: ServerRequest) => void
) {
const server = serve(addr); const server = serve(addr);
for await (const request of server) { for await (const { req, res } of server) {
await handler(request); await handler(req, res);
} }
} }
export interface Response { export interface HttpServer {
status?: number; handle(pattern: string | RegExp, handler: HttpHandler);
headers?: Headers;
body?: Uint8Array | Reader; listen(addr: string, cancel?: Deferred): Promise<void>;
} }
export function setContentLength(r: Response): void { /** create HttpServer object */
export function createServer(): HttpServer {
return new HttpServerImpl();
}
/** create ServerResponder object */
export function createResponder(w: Writer): ServerResponder {
return new ServerResponderImpl(w);
}
class HttpServerImpl implements HttpServer {
private handlers: { pattern: string | RegExp; handler: HttpHandler }[] = [];
handle(pattern: string | RegExp, handler: HttpHandler) {
this.handlers.push({ pattern, handler });
}
async listen(addr: string, cancel: Deferred = defer()) {
for await (const { req, res } of serve(addr, cancel)) {
let lastMatch: RegExpMatchArray;
let lastHandler: HttpHandler;
for (const { pattern, handler } of this.handlers) {
const match = req.url.match(pattern);
if (!match) {
continue;
}
if (!lastMatch) {
lastMatch = match;
lastHandler = handler;
} else if (match[0].length > lastMatch[0].length) {
// use longest match
lastMatch = match;
lastHandler = handler;
}
}
req.match = lastMatch;
if (lastHandler) {
await lastHandler(req, res);
if (!res.isResponded) {
await res.respond({
status: 500,
body: encode("Not Responded")
});
}
} else {
await res.respond({
status: 404,
body: encode("Not Found")
});
}
}
}
}
class ServerResponderImpl implements ServerResponder {
constructor(private w: Writer) {}
private _responded: boolean = false;
get isResponded() {
return this._responded;
}
private checkIfResponded() {
if (this.isResponded) {
throw new Error("http: already responded");
}
}
respond(response: ServerResponse): Promise<void> {
this.checkIfResponded();
this._responded = true;
return writeResponse(this.w, response);
}
respondJson(obj: any, headers: Headers = new Headers()): Promise<void> {
const body = encode(JSON.stringify(obj));
if (!headers.has("content-type")) {
headers.set("content-type", "application/json");
}
return this.respond({
status: 200,
body,
headers
});
}
respondText(text: string, headers: Headers = new Headers()): Promise<void> {
const body = encode(text);
if (!headers.has("content-type")) {
headers.set("content-type", "text/plain");
}
return this.respond({
status: 200,
headers,
body
});
}
}
export function setContentLength(r: ServerResponse): void {
if (!r.headers) { if (!r.headers) {
r.headers = new Headers(); r.headers = new Headers();
} }
@ -122,100 +250,6 @@ export function setContentLength(r: Response): void {
} }
} }
export class ServerRequest {
url: string;
method: string;
proto: string;
headers: Headers;
conn: Conn;
r: BufReader;
w: BufWriter;
public async *bodyStream() {
if (this.headers.has("content-length")) {
const len = +this.headers.get("content-length");
if (Number.isNaN(len)) {
return new Uint8Array(0);
}
let buf = new Uint8Array(1024);
let rr = await this.r.read(buf);
let nread = rr.nread;
while (!rr.eof && nread < len) {
yield buf.subarray(0, rr.nread);
buf = new Uint8Array(1024);
rr = await this.r.read(buf);
nread += rr.nread;
}
yield buf.subarray(0, rr.nread);
} else {
if (this.headers.has("transfer-encoding")) {
const transferEncodings = this.headers
.get("transfer-encoding")
.split(",")
.map(e => e.trim().toLowerCase());
if (transferEncodings.includes("chunked")) {
// Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
const tp = new TextProtoReader(this.r);
let [line, _] = await tp.readLine();
// TODO: handle chunk extension
let [chunkSizeString, optExt] = line.split(";");
let chunkSize = parseInt(chunkSizeString, 16);
if (Number.isNaN(chunkSize) || chunkSize < 0) {
throw new Error("Invalid chunk size");
}
while (chunkSize > 0) {
let data = new Uint8Array(chunkSize);
let [nread, err] = await this.r.readFull(data);
if (nread !== chunkSize) {
throw new Error("Chunk data does not match size");
}
yield data;
await this.r.readLine(); // Consume \r\n
[line, _] = await tp.readLine();
chunkSize = parseInt(line, 16);
}
const [entityHeaders, err] = await tp.readMIMEHeader();
if (!err) {
for (let [k, v] of entityHeaders) {
this.headers.set(k, v);
}
}
/* Pseudo code from https://tools.ietf.org/html/rfc2616#section-19.4.6
length := 0
read chunk-size, chunk-extension (if any) and CRLF
while (chunk-size > 0) {
read chunk-data and CRLF
append chunk-data to entity-body
length := length + chunk-size
read chunk-size and CRLF
}
read entity-header
while (entity-header not empty) {
append entity-header to existing header fields
read entity-header
}
Content-Length := length
Remove "chunked" from Transfer-Encoding
*/
return; // Must return here to avoid fall through
}
// TODO: handle other transfer-encoding types
}
// Otherwise...
yield new Uint8Array(0);
}
}
// Read the body of the request into a single Uint8Array
public async body(): Promise<Uint8Array> {
return readAllIterator(this.bodyStream());
}
async respond(r: Response): Promise<void> {
return writeResponse(this.w, r);
}
}
function bufWriter(w: Writer): BufWriter { function bufWriter(w: Writer): BufWriter {
if (w instanceof BufWriter) { if (w instanceof BufWriter) {
return w; return w;
@ -224,7 +258,10 @@ function bufWriter(w: Writer): BufWriter {
} }
} }
export async function writeResponse(w: Writer, r: Response): Promise<void> { export async function writeResponse(
w: Writer,
r: ServerResponse
): Promise<void> {
const protoMajor = 1; const protoMajor = 1;
const protoMinor = 1; const protoMinor = 1;
const statusCode = r.status || 200; const statusCode = r.status || 200;
@ -282,53 +319,52 @@ async function writeChunkedBody(w: Writer, r: Reader) {
await writer.write(endChunk); await writer.write(endChunk);
} }
async function readRequest( export async function readRequest(conn: Reader): Promise<ServerRequest> {
c: Conn, const bufr = new BufReader(conn);
bufr?: BufReader
): Promise<[ServerRequest, BufState]> {
if (!bufr) {
bufr = new BufReader(c);
}
const bufw = new BufWriter(c);
const req = new ServerRequest();
req.conn = c;
req.r = bufr!;
req.w = bufw;
const tp = new TextProtoReader(bufr!); const tp = new TextProtoReader(bufr!);
let s: string;
let err: BufState;
// First line: GET /index.html HTTP/1.0 // First line: GET /index.html HTTP/1.0
[s, err] = await tp.readLine(); const [line, lineErr] = await tp.readLine();
if (err) { if (lineErr) {
return [null, err]; throw lineErr;
} }
[req.method, req.url, req.proto] = s.split(" ", 3); const [method, url, proto] = line.split(" ", 3);
const [headers, headersErr] = await tp.readMIMEHeader();
[req.headers, err] = await tp.readMIMEHeader(); if (headersErr) {
throw headersErr;
return [req, err]; }
const contentLength = headers.get("content-length");
const body =
headers.get("transfer-encoding") === "chunked"
? new ChunkedBodyReader(bufr)
: new BodyReader(bufr, parseInt(contentLength));
return {
method,
url,
proto,
headers,
body,
match: null
};
} }
async function readAllIterator( export async function readResponse(conn: Reader): Promise<ServerResponse> {
it: AsyncIterableIterator<Uint8Array> const bufr = new BufReader(conn);
): Promise<Uint8Array> { const tp = new TextProtoReader(bufr!);
const chunks = []; // First line: HTTP/1,1 200 OK
let len = 0; const [line, lineErr] = await tp.readLine();
for await (const chunk of it) { if (lineErr) {
chunks.push(chunk); throw lineErr;
len += chunk.length;
} }
if (chunks.length === 0) { const [proto, status, statusText] = line.split(" ", 3);
// No need for copy const [headers, headersErr] = await tp.readMIMEHeader();
return chunks[0]; if (headersErr) {
throw headersErr;
} }
const collected = new Uint8Array(len); const contentLength = headers.get("content-length");
let offset = 0; const body =
for (let chunk of chunks) { headers.get("transfer-encoding") === "chunked"
collected.set(chunk, offset); ? new ChunkedBodyReader(bufr)
offset += chunk.length; : new BodyReader(bufr, parseInt(contentLength));
} return { status: parseInt(status), headers, body };
return collected;
} }

View file

@ -5,19 +5,26 @@
// Ported from // Ported from
// https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go // https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go
import { Buffer } from "deno"; import { Buffer, copy, Reader } from "deno";
import { assertEqual, test } from "../testing/mod.ts"; import { assert, assertEqual, test } from "../testing/mod.ts";
import { Response, ServerRequest } from "./server.ts"; import {
import { BufReader, BufWriter } from "../io/bufio.ts"; createResponder,
createServer,
readRequest,
readResponse,
ServerResponse,
writeResponse
} from "./server.ts";
import { encode } from "../strings/strings.ts";
import { StringReader } from "../io/readers.ts";
import { StringWriter } from "../io/writers.ts";
import { defer } from "../util/deferred.ts";
interface ResponseTest { interface ResponseTest {
response: Response; response: ServerResponse;
raw: string; raw: string;
} }
const enc = new TextEncoder();
const dec = new TextDecoder();
const responseTests: ResponseTest[] = [ const responseTests: ResponseTest[] = [
// Default response // Default response
{ {
@ -28,7 +35,7 @@ const responseTests: ResponseTest[] = [
{ {
response: { response: {
status: 200, status: 200,
body: new Buffer(new TextEncoder().encode("abcdef")) body: new Buffer(encode("abcdef"))
}, },
raw: raw:
@ -38,181 +45,241 @@ const responseTests: ResponseTest[] = [
} }
]; ];
test(async function responseWrite() { test(async function httpWriteResponse() {
for (const testCase of responseTests) { for (const { raw, response } of responseTests) {
const buf = new Buffer(); const buf = new Buffer();
const bufw = new BufWriter(buf); await writeResponse(buf, response);
const request = new ServerRequest(); assertEqual(buf.toString(), raw);
request.w = bufw;
await request.respond(testCase.response);
assertEqual(buf.toString(), testCase.raw);
} }
}); });
test(async function requestBodyWithContentLength() { test(async function httpReadRequest() {
{ const body = "0123456789";
const req = new ServerRequest(); const lines = [
req.headers = new Headers(); "GET /index.html?deno=land HTTP/1.1",
req.headers.set("content-length", "5"); "Host: deno.land",
const buf = new Buffer(enc.encode("Hello")); "Content-Type: text/plain",
req.r = new BufReader(buf); `Content-Length: ${body.length}`,
const body = dec.decode(await req.body()); "\r\n"
assertEqual(body, "Hello"); ];
} let msg = lines.join("\r\n");
msg += body;
const req = await readRequest(new StringReader(`${msg}`));
assert.equal(req.url, "/index.html?deno=land");
assert.equal(req.method, "GET");
assert.equal(req.proto, "HTTP/1.1");
assert.equal(req.headers.get("host"), "deno.land");
assert.equal(req.headers.get("content-type"), "text/plain");
assert.equal(req.headers.get("content-length"), `${body.length}`);
const w = new StringWriter();
await copy(w, req.body);
assert.equal(w.toString(), body);
});
// Larger than internal buf test(async function httpReadRequestChunkedBody() {
{ const lines = [
const longText = "1234\n".repeat(1000); "GET /index.html?deno=land HTTP/1.1",
const req = new ServerRequest(); "Host: deno.land",
req.headers = new Headers(); "Content-Type: text/plain",
req.headers.set("Content-Length", "5000"); `Transfer-Encoding: chunked`,
const buf = new Buffer(enc.encode(longText)); "\r\n"
req.r = new BufReader(buf); ];
const body = dec.decode(await req.body()); const hd = lines.join("\r\n");
assertEqual(body, longText); const buf = new Buffer();
await buf.write(encode(hd));
await buf.write(encode("4\r\ndeno\r\n"));
await buf.write(encode("5\r\n.land\r\n"));
await buf.write(encode("0\r\n\r\n"));
const req = await readRequest(buf);
assert.equal(req.url, "/index.html?deno=land");
assert.equal(req.method, "GET");
assert.equal(req.proto, "HTTP/1.1");
assert.equal(req.headers.get("host"), "deno.land");
assert.equal(req.headers.get("content-type"), "text/plain");
assert.equal(req.headers.get("transfer-encoding"), `chunked`);
const dest = new Buffer();
await copy(dest, req.body);
assert.equal(dest.toString(), "deno.land");
});
test(async function httpServer() {
const server = createServer();
server.handle("/index", async (req, res) => {
await res.respond({
status: 200,
body: encode("ok")
});
});
server.handle(new RegExp("/foo/(?<id>.+)"), async (req, res) => {
const { id } = req.match.groups;
await res.respond({
status: 200,
headers: new Headers({
"content-type": "application/json"
}),
body: encode(JSON.stringify({ id }))
});
});
server.handle("/no-response", async (req, res) => {});
const cancel = defer<void>();
try {
server.listen("127.0.0.1:8080", cancel);
{
const res1 = await fetch("http://127.0.0.1:8080/index");
const text = await res1.body.text();
assert.equal(res1.status, 200);
assert.equal(text, "ok");
}
{
const res2 = await fetch("http://127.0.0.1:8080/foo/123");
const json = await res2.body.json();
assert.equal(res2.status, 200);
assert.equal(res2.headers.get("content-type"), "application/json");
assert.equal(json["id"], "123");
}
{
const res = await fetch("http://127.0.0.1:8080/no-response");
assert.equal(res.status, 500);
const text = await res.body.text();
assert.assert(!!text.match("Not Responded"));
}
{
const res = await fetch("http://127.0.0.1:8080/not-found");
const text = await res.body.text();
assert.equal(res.status, 404);
assert.assert(!!text.match("Not Found"));
}
} finally {
cancel.resolve();
} }
}); });
test(async function requestBodyWithTransferEncoding() { test(async function httpServerResponder() {
{ const w = new Buffer();
const shortText = "Hello"; const res = createResponder(w);
const req = new ServerRequest(); assert.assert(!res.isResponded);
req.headers = new Headers(); await res.respond({
req.headers.set("transfer-encoding", "chunked"); status: 200,
let chunksData = ""; headers: new Headers({
let chunkOffset = 0; "content-type": "text/plain"
const maxChunkSize = 70; }),
while (chunkOffset < shortText.length) { body: encode("ok")
const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); });
chunksData += `${chunkSize.toString(16)}\r\n${shortText.substr( assert.assert(res.isResponded);
chunkOffset, const resp = await readResponse(w);
chunkSize assert.equal(resp.status, 200);
)}\r\n`; assert.equal(resp.headers.get("content-type"), "text/plain");
chunkOffset += chunkSize; const sw = new StringWriter();
} await copy(sw, resp.body as Reader);
chunksData += "0\r\n\r\n"; assert.equal(sw.toString(), "ok");
const buf = new Buffer(enc.encode(chunksData));
req.r = new BufReader(buf);
const body = dec.decode(await req.body());
assertEqual(body, shortText);
}
// Larger than internal buf
{
const longText = "1234\n".repeat(1000);
const req = new ServerRequest();
req.headers = new Headers();
req.headers.set("transfer-encoding", "chunked");
let chunksData = "";
let chunkOffset = 0;
const maxChunkSize = 70;
while (chunkOffset < longText.length) {
const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset);
chunksData += `${chunkSize.toString(16)}\r\n${longText.substr(
chunkOffset,
chunkSize
)}\r\n`;
chunkOffset += chunkSize;
}
chunksData += "0\r\n\r\n";
const buf = new Buffer(enc.encode(chunksData));
req.r = new BufReader(buf);
const body = dec.decode(await req.body());
assertEqual(body, longText);
}
}); });
test(async function requestBodyStreamWithContentLength() { test(async function httpServerResponderRespondJson() {
{ const w = new Buffer();
const shortText = "Hello"; const res = createResponder(w);
const req = new ServerRequest(); const json = {
req.headers = new Headers(); id: 1,
req.headers.set("content-length", "" + shortText.length); deno: {
const buf = new Buffer(enc.encode(shortText)); is: "land"
req.r = new BufReader(buf);
const it = await req.bodyStream();
let offset = 0;
for await (const chunk of it) {
const s = dec.decode(chunk);
assertEqual(shortText.substr(offset, s.length), s);
offset += s.length;
} }
} };
assert.assert(!res.isResponded);
// Larger than internal buf await res.respondJson(
{ json,
const longText = "1234\n".repeat(1000); new Headers({
const req = new ServerRequest(); deno: "land"
req.headers = new Headers(); })
req.headers.set("Content-Length", "5000"); );
const buf = new Buffer(enc.encode(longText)); assert.assert(res.isResponded);
req.r = new BufReader(buf); const resp = await readResponse(w);
const it = await req.bodyStream(); assert.equal(resp.status, 200);
let offset = 0; assert.equal(resp.headers.get("content-type"), "application/json");
for await (const chunk of it) { const sw = new StringWriter();
const s = dec.decode(chunk); await copy(sw, resp.body as Reader);
assertEqual(longText.substr(offset, s.length), s); const resJson = JSON.parse(sw.toString());
offset += s.length; assert.equal(resJson, json);
} assert.equal(resp.headers.get("deno"), "land");
}
}); });
test(async function requestBodyStreamWithTransferEncoding() { test(async function httpServerResponderRespondText() {
{ const w = new Buffer();
const shortText = "Hello"; const res = createResponder(w);
const req = new ServerRequest(); assert.assert(!res.isResponded);
req.headers = new Headers(); await res.respondText(
req.headers.set("transfer-encoding", "chunked"); "deno.land",
let chunksData = ""; new Headers({
let chunkOffset = 0; deno: "land"
const maxChunkSize = 70; })
while (chunkOffset < shortText.length) { );
const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); assert.assert(res.isResponded);
chunksData += `${chunkSize.toString(16)}\r\n${shortText.substr( const resp = await readResponse(w);
chunkOffset, assert.equal(resp.status, 200);
chunkSize assert.equal(resp.headers.get("content-type"), "text/plain");
)}\r\n`; const sw = new StringWriter();
chunkOffset += chunkSize; await copy(sw, resp.body as Reader);
} assert.equal(sw.toString(), "deno.land");
chunksData += "0\r\n\r\n"; assert.equal(resp.headers.get("deno"), "land");
const buf = new Buffer(enc.encode(chunksData)); });
req.r = new BufReader(buf);
const it = await req.bodyStream();
let offset = 0;
for await (const chunk of it) {
const s = dec.decode(chunk);
assertEqual(shortText.substr(offset, s.length), s);
offset += s.length;
}
}
// Larger than internal buf test(async function httpServerResponderShouldThrow() {
const w = new Buffer();
{ {
const longText = "1234\n".repeat(1000); const res = createResponder(w);
const req = new ServerRequest(); await res.respond({
req.headers = new Headers(); body: null
req.headers.set("transfer-encoding", "chunked"); });
let chunksData = ""; await assert.throwsAsync(
let chunkOffset = 0; async () => res.respond({ body: null }),
const maxChunkSize = 70; Error,
while (chunkOffset < longText.length) { "responded"
const chunkSize = Math.min(maxChunkSize, longText.length - chunkOffset); );
chunksData += `${chunkSize.toString(16)}\r\n${longText.substr( await assert.throwsAsync(
chunkOffset, async () => res.respondJson({}),
chunkSize Error,
)}\r\n`; "responded"
chunkOffset += chunkSize; );
} await assert.throwsAsync(
chunksData += "0\r\n\r\n"; async () => res.respondText(""),
const buf = new Buffer(enc.encode(chunksData)); Error,
req.r = new BufReader(buf); "responded"
const it = await req.bodyStream(); );
let offset = 0; }
for await (const chunk of it) { {
const s = dec.decode(chunk); const res = createResponder(w);
assertEqual(longText.substr(offset, s.length), s); await res.respondText("");
offset += s.length; await assert.throwsAsync(
} async () => res.respond({ body: null }),
Error,
"responded"
);
await assert.throwsAsync(
async () => res.respondJson({}),
Error,
"responded"
);
await assert.throwsAsync(
async () => res.respondText(""),
Error,
"responded"
);
}
{
const res = createResponder(w);
await res.respondJson({});
await assert.throwsAsync(
async () => res.respond({ body: null }),
Error,
"responded"
);
await assert.throwsAsync(
async () => res.respondJson({}),
Error,
"responded"
);
await assert.throwsAsync(
async () => res.respondText(""),
Error,
"responded"
);
} }
}); });

View file

@ -7,9 +7,11 @@ import { decode } from "../strings/strings.ts";
test(async function ioStringReader() { test(async function ioStringReader() {
const r = new StringReader("abcdef"); const r = new StringReader("abcdef");
const { nread, eof } = await r.read(new Uint8Array(6)); const buf = new Uint8Array(6);
const { nread, eof } = await r.read(buf);
assert.equal(nread, 6); assert.equal(nread, 6);
assert.equal(eof, true); assert.equal(eof, true);
assert.equal(decode(buf), "abcdef");
}); });
test(async function ioStringReader() { test(async function ioStringReader() {

View file

@ -1,6 +1,8 @@
#!/usr/bin/env deno -A #!/usr/bin/env deno -A
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import "benching/test.ts"; import "benching/test.ts";
import "util/deferred_test.ts";
import "colors/test.ts"; import "colors/test.ts";
import "datetime/test.ts"; import "datetime/test.ts";
import "examples/test.ts"; import "examples/test.ts";
@ -14,6 +16,7 @@ import "fs/path/test.ts";
import "io/test.ts"; import "io/test.ts";
import "http/server_test.ts"; import "http/server_test.ts";
import "http/file_server_test.ts"; import "http/file_server_test.ts";
import "http/readers_test.ts";
import "log/test.ts"; import "log/test.ts";
import "media_types/test.ts"; import "media_types/test.ts";
import "multipart/formfile_test.ts"; import "multipart/formfile_test.ts";

42
util/deferred.ts Normal file
View file

@ -0,0 +1,42 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
export type Deferred<T = any, R = Error> = {
promise: Promise<T>;
resolve: (t?: T) => void;
reject: (r?: R) => void;
readonly handled: boolean;
};
/** Create deferred promise that can be resolved and rejected by outside */
export function defer<T>(): Deferred<T> {
let handled = false;
let resolve;
let reject;
const promise = new Promise<T>((res, rej) => {
resolve = r => {
handled = true;
res(r);
};
reject = r => {
handled = true;
rej(r);
};
});
return {
promise,
resolve,
reject,
get handled() {
return handled;
}
};
}
export function isDeferred(x): x is Deferred {
return (
typeof x === "object" &&
x.promise instanceof Promise &&
typeof x["resolve"] === "function" &&
typeof x["reject"] === "function"
);
}

16
util/deferred_test.ts Normal file
View file

@ -0,0 +1,16 @@
// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
import { assert, test } from "../testing/mod.ts";
import { defer, isDeferred } from "./deferred.ts";
test(async function asyncIsDeferred() {
const d = defer();
assert.assert(isDeferred(d));
assert.assert(
isDeferred({
promise: null,
resolve: () => {},
reject: () => {}
}) === false
);
});