diff --git a/http/README.md b/http/README.md index 67c578f317..2c9a908532 100644 --- a/http/README.md +++ b/http/README.md @@ -5,22 +5,13 @@ A framework for creating HTTP/HTTPS server. ## Example ```typescript -import { createServer } from "https://deno.land/x/http/server.ts"; -import { encode } from "https://deno.land/x/strings/strings.ts"; +import { serve } from "https://deno.land/x/http/server.ts"; +const s = serve("0.0.0.0:8000"); async function main() { - const server = createServer(); - server.handle("/", async (req, res) => { - await res.respond({ - status: 200, - body: encode("ok") - }); - }); - server.handle(new RegExp("/foo/(?.+)"), async (req, res) => { - const { id } = req.match.groups; - await res.respondJson({ id }); - }); - server.listen("127.0.0.1:8080"); + for await (const req of s) { + req.respond({ body: new TextEncoder().encode("Hello World\n") }); + } } main(); diff --git a/http/file_server.ts b/http/file_server.ts index 4aebd49579..1f3fdd5861 100755 --- a/http/file_server.ts +++ b/http/file_server.ts @@ -10,7 +10,7 @@ import { listenAndServe, ServerRequest, setContentLength, - ServerResponse + Response } from "./server.ts"; import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno"; import { extname } from "../fs/path.ts"; @@ -195,14 +195,14 @@ async function serveFallback(req: ServerRequest, e: Error) { } } -function serverLog(req: ServerRequest, res: ServerResponse) { +function serverLog(req: ServerRequest, res: Response) { const d = new Date().toISOString(); const dateFmt = `[${d.slice(0, 10)} ${d.slice(11, 19)}]`; const s = `${dateFmt} "${req.method} ${req.url} ${req.proto}" ${res.status}`; console.log(s); } -function setCORS(res: ServerResponse) { +function setCORS(res: Response) { if (!res.headers) { res.headers = new Headers(); } @@ -213,11 +213,11 @@ function setCORS(res: ServerResponse) { ); } -listenAndServe(addr, async (req, res) => { +listenAndServe(addr, async req => { const fileName = req.url.replace(/\/$/, ""); const filePath = currentDir + fileName; - let response: ServerResponse; + let response: Response; try { const fileInfo = await stat(filePath); @@ -235,7 +235,7 @@ listenAndServe(addr, async (req, res) => { setCORS(response); } serverLog(req, response); - res.respond(response); + req.respond(response); } }); diff --git a/http/http_bench.ts b/http/http_bench.ts index 8ca3bb33c0..d80b2b103e 100644 --- a/http/http_bench.ts +++ b/http/http_bench.ts @@ -1,6 +1,6 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. import * as deno from "deno"; -import { serve } from "./server.ts"; +import { serve } from "./mod.ts"; const addr = deno.args[1] || "127.0.0.1:4500"; const server = serve(addr); @@ -8,13 +8,8 @@ const server = serve(addr); const body = new TextEncoder().encode("Hello World"); async function main(): Promise { - try { - for await (const request of server) { - await request.responder.respond({ status: 200, body }); - } - } catch (e) { - console.log(e.stack); - console.error(e); + for await (const request of server) { + await request.respond({ status: 200, body }); } } diff --git a/http/readers.ts b/http/readers.ts deleted file mode 100644 index f14955755c..0000000000 --- a/http/readers.ts +++ /dev/null @@ -1,78 +0,0 @@ -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 { - 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 { - 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 }; - } - } -} diff --git a/http/readers_test.ts b/http/readers_test.ts deleted file mode 100644 index 4fd379febb..0000000000 --- a/http/readers_test.ts +++ /dev/null @@ -1,12 +0,0 @@ -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"); -}); diff --git a/http/server.ts b/http/server.ts index a80becbd5a..400171fc5b 100644 --- a/http/server.ts +++ b/http/server.ts @@ -1,90 +1,63 @@ // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. - -import { Conn, copy, listen, Reader, toAsyncIterator, Writer } from "deno"; -import { BufReader, BufWriter } from "../io/bufio.ts"; +import { listen, Conn, toAsyncIterator, Reader, Writer, copy } from "deno"; +import { BufReader, BufState, BufWriter } from "../io/bufio.ts"; import { TextProtoReader } from "../textproto/mod.ts"; import { STATUS_TEXT } from "./http_status.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"; -/** basic handler for http request */ -export type HttpHandler = (req: ServerRequest, res: ServerResponder) => unknown; - -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; - - respondJson(obj: any, headers?: Headers): Promise; - - respondText(text: string, headers?: Headers): Promise; - - readonly isResponded: boolean; +interface Deferred { + promise: Promise<{}>; + resolve: () => void; + reject: () => void; } -export interface ServerResponse { - /** - * HTTP status code - * @default 200 */ - status?: number; - headers?: Headers; - body?: Uint8Array | Reader; +function deferred(): Deferred { + let resolve, reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { + promise, + resolve, + reject + }; } interface ServeEnv { - reqQueue: { req: ServerRequest; conn: Conn }[]; + reqQueue: ServerRequest[]; serveDeferred: Deferred; } /** Continuously read more requests from conn until EOF * 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 * https://github.com/tc39/ecma262/pull/1250 * See https://v8.dev/blog/fast-async */ -function serveConn(env: ServeEnv, conn: Conn) { - readRequest(conn) - .then(maybeHandleReq.bind(null, env, conn)) - .catch(e => { - conn.close(); - }); +function serveConn(env: ServeEnv, conn: Conn, bufr?: BufReader) { + readRequest(conn, bufr).then(maybeHandleReq.bind(null, env, conn)); } -function maybeHandleReq(env: ServeEnv, conn: Conn, req: ServerRequest) { - env.reqQueue.push({ conn, req }); // push req to queue +function maybeHandleReq(env: ServeEnv, conn: Conn, maybeReq: any) { + const [req, _err] = maybeReq; + 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 } -/** - * 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 }> { +export async function* serve(addr: string) { const listener = listen("tcp", addr); const env: ServeEnv = { reqQueue: [], // in case multiple promises are ready - serveDeferred: defer() + serveDeferred: deferred() }; + // Routine that keeps calling accept const acceptRoutine = () => { const handleConn = (conn: Conn) => { @@ -92,168 +65,47 @@ export async function* serve( scheduleAccept(); // schedule next accept }; const scheduleAccept = () => { - Promise.race([cancel.promise, listener.accept().then(handleConn)]); + listener.accept().then(handleConn); }; scheduleAccept(); }; + acceptRoutine(); + + // Loop hack to allow yield (yield won't work in callbacks) while (true) { - // do race between accept, serveDeferred and cancel - await Promise.race([env.serveDeferred.promise, cancel.promise]); - // cancellation deferred resolved - if (cancel.handled) { - break; - } - // next serve deferred - env.serveDeferred = defer(); - const queueToProcess = env.reqQueue; + await env.serveDeferred.promise; + env.serveDeferred = deferred(); // use a new deferred + let queueToProcess = env.reqQueue; env.reqQueue = []; - for (const { req, conn } of queueToProcess) { - if (req) { - const res = createResponder(conn); - yield { req, res }; - } - serveConn(env, conn); + for (const result of queueToProcess) { + yield result; + // Continue read more from conn when user is done with the current req + // Moving this here makes it easier to manage + serveConn(env, result.conn, result.r); } } listener.close(); } -export async function listenAndServe(addr: string, handler: HttpHandler) { +export async function listenAndServe( + addr: string, + handler: (req: ServerRequest) => void +) { const server = serve(addr); - for await (const { req, res } of server) { - await handler(req, res); + for await (const request of server) { + await handler(request); } } -export interface HttpServer { - handle(pattern: string | RegExp, handler: HttpHandler); - - listen(addr: string, cancel?: Deferred): Promise; +export interface Response { + status?: number; + headers?: Headers; + body?: Uint8Array | Reader; } -/** 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 { pathname } = new URL(req.url, addr); - const { index, match } = findLongestAndNearestMatch( - pathname, - this.handlers.map(v => v.pattern) - ); - req.match = match; - if (index > -1) { - const { handler } = this.handlers[index]; - await handler(req, res); - if (!res.isResponded) { - await res.respond({ - status: 500, - body: encode("Not Responded") - }); - } - } else { - await res.respond({ - status: 404, - body: encode("Not Found") - }); - } - } - } -} - -/** - * Find the match that appeared in the nearest position to the beginning of word. - * If positions are same, the longest one will be picked. - * Return -1 and null if no match found. - * */ -export function findLongestAndNearestMatch( - pathname: string, - patterns: (string | RegExp)[] -): { index: number; match: RegExpMatchArray } { - let lastMatchIndex = pathname.length; - let lastMatchLength = 0; - let match: RegExpMatchArray = null; - let index = -1; - for (let i = 0; i < patterns.length; i++) { - const pattern = patterns[i]; - const m = pathname.match(pattern); - if (!m) continue; - if ( - m.index < lastMatchIndex || - (m.index === lastMatchIndex && m[0].length > lastMatchLength) - ) { - index = i; - match = m; - lastMatchIndex = m.index; - lastMatchLength = m[0].length; - } - } - return { index, match }; -} - -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 { - this.checkIfResponded(); - this._responded = true; - return writeResponse(this.w, response); - } - - respondJson(obj: any, headers: Headers = new Headers()): Promise { - 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 { - 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 { +export function setContentLength(r: Response): void { if (!r.headers) { r.headers = new Headers(); } @@ -270,6 +122,100 @@ export function setContentLength(r: ServerResponse): 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 { + return readAllIterator(this.bodyStream()); + } + + async respond(r: Response): Promise { + return writeResponse(this.w, r); + } +} + function bufWriter(w: Writer): BufWriter { if (w instanceof BufWriter) { return w; @@ -278,10 +224,7 @@ function bufWriter(w: Writer): BufWriter { } } -export async function writeResponse( - w: Writer, - r: ServerResponse -): Promise { +export async function writeResponse(w: Writer, r: Response): Promise { const protoMajor = 1; const protoMinor = 1; const statusCode = r.status || 200; @@ -339,52 +282,53 @@ async function writeChunkedBody(w: Writer, r: Reader) { await writer.write(endChunk); } -export async function readRequest(conn: Reader): Promise { - const bufr = new BufReader(conn); +async function readRequest( + c: 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!); + let s: string; + let err: BufState; + // First line: GET /index.html HTTP/1.0 - const [line, lineErr] = await tp.readLine(); - if (lineErr) { - throw lineErr; + [s, err] = await tp.readLine(); + if (err) { + return [null, err]; } - const [method, url, proto] = line.split(" ", 3); - const [headers, headersErr] = await tp.readMIMEHeader(); - if (headersErr) { - throw headersErr; - } - 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 - }; + [req.method, req.url, req.proto] = s.split(" ", 3); + + [req.headers, err] = await tp.readMIMEHeader(); + + return [req, err]; } -export async function readResponse(conn: Reader): Promise { - const bufr = new BufReader(conn); - const tp = new TextProtoReader(bufr!); - // First line: HTTP/1,1 200 OK - const [line, lineErr] = await tp.readLine(); - if (lineErr) { - throw lineErr; +async function readAllIterator( + it: AsyncIterableIterator +): Promise { + const chunks = []; + let len = 0; + for await (const chunk of it) { + chunks.push(chunk); + len += chunk.length; } - const [proto, status, statusText] = line.split(" ", 3); - const [headers, headersErr] = await tp.readMIMEHeader(); - if (headersErr) { - throw headersErr; + if (chunks.length === 0) { + // No need for copy + return chunks[0]; } - const contentLength = headers.get("content-length"); - const body = - headers.get("transfer-encoding") === "chunked" - ? new ChunkedBodyReader(bufr) - : new BodyReader(bufr, parseInt(contentLength)); - return { status: parseInt(status), headers, body }; + const collected = new Uint8Array(len); + let offset = 0; + for (let chunk of chunks) { + collected.set(chunk, offset); + offset += chunk.length; + } + return collected; } diff --git a/http/server_test.ts b/http/server_test.ts index 4f22e4a06b..099547d0c7 100644 --- a/http/server_test.ts +++ b/http/server_test.ts @@ -5,27 +5,19 @@ // Ported from // https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go -import { Buffer, copy, Reader } from "deno"; -import { assert, assertEqual, runTests, test } from "../testing/mod.ts"; -import { - createResponder, - createServer, - findLongestAndNearestMatch, - 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"; +import { Buffer } from "deno"; +import { assertEqual, test } from "../testing/mod.ts"; +import { Response, ServerRequest } from "./server.ts"; +import { BufReader, BufWriter } from "../io/bufio.ts"; interface ResponseTest { - response: ServerResponse; + response: Response; raw: string; } +const enc = new TextEncoder(); +const dec = new TextDecoder(); + const responseTests: ResponseTest[] = [ // Default response { @@ -36,7 +28,7 @@ const responseTests: ResponseTest[] = [ { response: { status: 200, - body: new Buffer(encode("abcdef")) + body: new Buffer(new TextEncoder().encode("abcdef")) }, raw: @@ -46,284 +38,181 @@ const responseTests: ResponseTest[] = [ } ]; -test(async function httpWriteResponse() { - for (const { raw, response } of responseTests) { +test(async function responseWrite() { + for (const testCase of responseTests) { const buf = new Buffer(); - await writeResponse(buf, response); - assertEqual(buf.toString(), raw); + const bufw = new BufWriter(buf); + const request = new ServerRequest(); + request.w = bufw; + + await request.respond(testCase.response); + assertEqual(buf.toString(), testCase.raw); } }); -test(async function httpReadRequest() { - const body = "0123456789"; - const lines = [ - "GET /index.html?deno=land HTTP/1.1", - "Host: deno.land", - "Content-Type: text/plain", - `Content-Length: ${body.length}`, - "\r\n" - ]; - 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); -}); - -test(async function httpReadRequestChunkedBody() { - const lines = [ - "GET /index.html?deno=land HTTP/1.1", - "Host: deno.land", - "Content-Type: text/plain", - `Transfer-Encoding: chunked`, - "\r\n" - ]; - const hd = lines.join("\r\n"); - 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(function httpMatchNearest() { - assert.equal( - findLongestAndNearestMatch("/foo", ["/foo", "/bar", "/f"]).index, - 0 - ); - assert.equal( - findLongestAndNearestMatch("/foo", ["/foo", "/foo/bar"]).index, - 0 - ); - assert.equal( - findLongestAndNearestMatch("/foo/bar", [ - "/", - "/foo", - "/hoo", - "/hoo/foo/bar", - "/foo/bar" - ]).index, - 4 - ); - assert.equal( - findLongestAndNearestMatch("/foo/bar/foo", ["/foo", "/foo/bar", "/bar/foo"]) - .index, - 1 - ); - assert.equal( - findLongestAndNearestMatch("/foo", ["/", "/hoo", "/hoo/foo"]).index, - 0 - ); - assert.equal( - findLongestAndNearestMatch("/deno/land", [/d(.+?)o/, /d(.+?)d/]).index, - 1 - ); - assert.equal(findLongestAndNearestMatch("/foo", ["/", "/a/foo"]).index, 0); - assert.equal( - findLongestAndNearestMatch("/foo", [/\/foo/, /\/bar\/foo/]).index, - 0 - ); - assert.equal( - findLongestAndNearestMatch("/foo", [/\/a\/foo/, /\/foo/]).index, - 1 - ); -}); - -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/(?.+)"), 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(); - 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 httpServerResponder() { - const w = new Buffer(); - const res = createResponder(w); - assert.assert(!res.isResponded); - await res.respond({ - status: 200, - headers: new Headers({ - "content-type": "text/plain" - }), - body: encode("ok") - }); - assert.assert(res.isResponded); - const resp = await readResponse(w); - assert.equal(resp.status, 200); - assert.equal(resp.headers.get("content-type"), "text/plain"); - const sw = new StringWriter(); - await copy(sw, resp.body as Reader); - assert.equal(sw.toString(), "ok"); -}); - -test(async function httpServerResponderRespondJson() { - const w = new Buffer(); - const res = createResponder(w); - const json = { - id: 1, - deno: { - is: "land" - } - }; - assert.assert(!res.isResponded); - await res.respondJson( - json, - new Headers({ - deno: "land" - }) - ); - assert.assert(res.isResponded); - const resp = await readResponse(w); - assert.equal(resp.status, 200); - assert.equal(resp.headers.get("content-type"), "application/json"); - const sw = new StringWriter(); - await copy(sw, resp.body as Reader); - const resJson = JSON.parse(sw.toString()); - assert.equal(resJson, json); - assert.equal(resp.headers.get("deno"), "land"); -}); - -test(async function httpServerResponderRespondText() { - const w = new Buffer(); - const res = createResponder(w); - assert.assert(!res.isResponded); - await res.respondText( - "deno.land", - new Headers({ - deno: "land" - }) - ); - assert.assert(res.isResponded); - const resp = await readResponse(w); - assert.equal(resp.status, 200); - assert.equal(resp.headers.get("content-type"), "text/plain"); - const sw = new StringWriter(); - await copy(sw, resp.body as Reader); - assert.equal(sw.toString(), "deno.land"); - assert.equal(resp.headers.get("deno"), "land"); -}); - -test(async function httpServerResponderShouldThrow() { - const w = new Buffer(); +test(async function requestBodyWithContentLength() { { - const res = createResponder(w); - await res.respond({ - body: null - }); - 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 req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("content-length", "5"); + const buf = new Buffer(enc.encode("Hello")); + req.r = new BufReader(buf); + const body = dec.decode(await req.body()); + assertEqual(body, "Hello"); } + + // Larger than internal buf { - const res = createResponder(w); - await res.respondText(""); - 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" - ); + const longText = "1234\n".repeat(1000); + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("Content-Length", "5000"); + const buf = new Buffer(enc.encode(longText)); + req.r = new BufReader(buf); + const body = dec.decode(await req.body()); + assertEqual(body, longText); + } +}); + +test(async function requestBodyWithTransferEncoding() { + { + const shortText = "Hello"; + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("transfer-encoding", "chunked"); + let chunksData = ""; + let chunkOffset = 0; + const maxChunkSize = 70; + while (chunkOffset < shortText.length) { + const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); + chunksData += `${chunkSize.toString(16)}\r\n${shortText.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, 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() { + { + const shortText = "Hello"; + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("content-length", "" + shortText.length); + const buf = new Buffer(enc.encode(shortText)); + 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 + { + const longText = "1234\n".repeat(1000); + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("Content-Length", "5000"); + const buf = new Buffer(enc.encode(longText)); + 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(longText.substr(offset, s.length), s); + offset += s.length; + } + } +}); + +test(async function requestBodyStreamWithTransferEncoding() { + { + const shortText = "Hello"; + const req = new ServerRequest(); + req.headers = new Headers(); + req.headers.set("transfer-encoding", "chunked"); + let chunksData = ""; + let chunkOffset = 0; + const maxChunkSize = 70; + while (chunkOffset < shortText.length) { + const chunkSize = Math.min(maxChunkSize, shortText.length - chunkOffset); + chunksData += `${chunkSize.toString(16)}\r\n${shortText.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 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 + { + 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 it = await req.bodyStream(); + let offset = 0; + for await (const chunk of it) { + const s = dec.decode(chunk); + assertEqual(longText.substr(offset, s.length), s); + offset += s.length; + } } }); diff --git a/io/readers_test.ts b/io/readers_test.ts index add59877df..0bc8ca36a0 100644 --- a/io/readers_test.ts +++ b/io/readers_test.ts @@ -7,11 +7,9 @@ import { decode } from "../strings/strings.ts"; test(async function ioStringReader() { const r = new StringReader("abcdef"); - const buf = new Uint8Array(6); - const { nread, eof } = await r.read(buf); + const { nread, eof } = await r.read(new Uint8Array(6)); assert.equal(nread, 6); assert.equal(eof, true); - assert.equal(decode(buf), "abcdef"); }); test(async function ioStringReader() { diff --git a/test.ts b/test.ts index dd311c5271..e94bdcee67 100755 --- a/test.ts +++ b/test.ts @@ -1,8 +1,6 @@ #!/usr/bin/env deno -A // Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. - import "benching/test.ts"; -import "util/deferred_test.ts"; import "colors/test.ts"; import "datetime/test.ts"; import "examples/test.ts"; @@ -17,7 +15,6 @@ import "fs/walk_test.ts"; import "io/test.ts"; import "http/server_test.ts"; import "http/file_server_test.ts"; -import "http/readers_test.ts"; import "log/test.ts"; import "media_types/test.ts"; import "multipart/formfile_test.ts"; diff --git a/util/deferred.ts b/util/deferred.ts deleted file mode 100644 index f52087547a..0000000000 --- a/util/deferred.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license. - -export type Deferred = { - promise: Promise; - 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(): Deferred { - let handled = false; - let resolve; - let reject; - const promise = new Promise((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" - ); -} diff --git a/util/deferred_test.ts b/util/deferred_test.ts deleted file mode 100644 index a397b3012b..0000000000 --- a/util/deferred_test.ts +++ /dev/null @@ -1,16 +0,0 @@ -// 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 - ); -});