From f6dae45cd2bb0615c136188b4dba8a3272ac5d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Mon, 17 Dec 2018 17:49:10 +0100 Subject: [PATCH] First pass at streaming http response (denoland/deno_std#16) Original: https://github.com/denoland/deno_std/commit/269665873a9219423085418d605b8af8ac2565dc --- buffer_test.ts | 2 +- bufio_test.ts | 4 ++-- file_server.ts | 20 +++++++++++------ http.ts | 53 +++++++++++++++++++++++++++++++++++++++------ http_test.ts | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++ test.ts | 1 + 6 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 http_test.ts diff --git a/buffer_test.ts b/buffer_test.ts index 1958f8e97d..9a71e80a33 100644 --- a/buffer_test.ts +++ b/buffer_test.ts @@ -17,7 +17,7 @@ function init() { if (testBytes == null) { testBytes = new Uint8Array(N); for (let i = 0; i < N; i++) { - testBytes[i] = "a".charCodeAt(0) + (i % 26); + testBytes[i] = "a".charCodeAt(0) + i % 26; } const decoder = new TextDecoder(); testString = decoder.decode(testBytes); diff --git a/bufio_test.ts b/bufio_test.ts index fb5dc23e89..5f32500a7a 100644 --- a/bufio_test.ts +++ b/bufio_test.ts @@ -109,7 +109,7 @@ test(async function bufioBufReader() { for (let i = 0; i < texts.length - 1; i++) { texts[i] = str + "\n"; all += texts[i]; - str += String.fromCharCode((i % 26) + 97); + str += String.fromCharCode(i % 26 + 97); } texts[texts.length - 1] = all; @@ -294,7 +294,7 @@ test(async function bufioWriter() { const data = new Uint8Array(8192); for (let i = 0; i < data.byteLength; i++) { - data[i] = charCode(" ") + (i % (charCode("~") - charCode(" "))); + data[i] = charCode(" ") + i % (charCode("~") - charCode(" ")); } const w = new Buffer(); diff --git a/file_server.ts b/file_server.ts index 5cd87e2ec7..bd1c52b887 100755 --- a/file_server.ts +++ b/file_server.ts @@ -5,8 +5,13 @@ // TODO Add tests like these: // https://github.com/indexzero/http-server/blob/master/test/http-server-test.js -import { listenAndServe, ServerRequest, setContentLength, Response } from "./http"; -import { cwd, readFile, DenoError, ErrorKind, args, stat, readDir } from "deno"; +import { + listenAndServe, + ServerRequest, + setContentLength, + Response +} from "./http"; +import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno"; const dirViewerTemplate = ` @@ -146,9 +151,10 @@ async function serveDir(req: ServerRequest, dirPath: string, dirName: string) { } async function serveFile(req: ServerRequest, filename: string) { - let file = await readFile(filename); + const file = await open(filename); + const fileInfo = await stat(filename); const headers = new Headers(); - headers.set("content-type", "octet-stream"); + headers.set("content-length", fileInfo.len.toString()); const res = { status: 200, @@ -163,9 +169,9 @@ async function serveFallback(req: ServerRequest, e: Error) { e instanceof DenoError && (e as DenoError).kind === ErrorKind.NotFound ) { - return { - status: 404, - body: encoder.encode("Not found") + return { + status: 404, + body: encoder.encode("Not found") }; } else { return { diff --git a/http.ts b/http.ts index 6954a48ba2..bd45aea0d1 100644 --- a/http.ts +++ b/http.ts @@ -1,4 +1,4 @@ -import { listen, Conn } from "deno"; +import { listen, Conn, toAsyncIterator, Reader, copy } from "deno"; import { BufReader, BufState, BufWriter } from "./bufio.ts"; import { TextProtoReader } from "./textproto.ts"; import { STATUS_TEXT } from "./http_status"; @@ -96,16 +96,23 @@ export async function listenAndServe( export interface Response { status?: number; headers?: Headers; - body?: Uint8Array; + body?: Uint8Array | Reader; } export function setContentLength(r: Response): void { if (!r.headers) { r.headers = new Headers(); } - if (!r.headers.has("content-length")) { - const bodyLength = r.body ? r.body.byteLength : 0; - r.headers.append("Content-Length", bodyLength.toString()); + + if (r.body) { + if (!r.headers.has("content-length")) { + if (r.body instanceof Uint8Array) { + const bodyLength = r.body.byteLength; + r.headers.append("Content-Length", bodyLength.toString()); + } else { + r.headers.append("Transfer-Encoding", "chunked"); + } + } } } @@ -116,6 +123,26 @@ export class ServerRequest { headers: Headers; w: BufWriter; + private async _streamBody(body: Reader, bodyLength: number) { + const n = await copy(this.w, body); + assert(n == bodyLength); + } + + private async _streamChunkedBody(body: Reader) { + const encoder = new TextEncoder(); + + for await (const chunk of toAsyncIterator(body)) { + const start = encoder.encode(`${chunk.byteLength.toString(16)}\r\n`); + const end = encoder.encode("\r\n"); + await this.w.write(start); + await this.w.write(chunk); + await this.w.write(end); + } + + const endChunk = encoder.encode("0\r\n\r\n"); + await this.w.write(endChunk); + } + async respond(r: Response): Promise { const protoMajor = 1; const protoMinor = 1; @@ -139,9 +166,21 @@ export class ServerRequest { const header = new TextEncoder().encode(out); let n = await this.w.write(header); assert(header.byteLength == n); + if (r.body) { - n = await this.w.write(r.body); - assert(r.body.byteLength == n); + if (r.body instanceof Uint8Array) { + n = await this.w.write(r.body); + assert(r.body.byteLength == n); + } else { + if (r.headers.has("content-length")) { + await this._streamBody( + r.body, + parseInt(r.headers.get("content-length")) + ); + } else { + await this._streamChunkedBody(r.body); + } + } } await this.w.flush(); diff --git a/http_test.ts b/http_test.ts new file mode 100644 index 0000000000..879afbf532 --- /dev/null +++ b/http_test.ts @@ -0,0 +1,58 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Ported from +// https://github.com/golang/go/blob/master/src/net/http/responsewrite_test.go + +import { + test, + assert, + assertEqual +} from "https://deno.land/x/testing/testing.ts"; + +import { + listenAndServe, + ServerRequest, + setContentLength, + Response +} from "./http"; +import { Buffer } from "./buffer"; +import { BufWriter } from "./bufio"; + +interface ResponseTest { + response: Response; + raw: string; +} + +const responseTests: ResponseTest[] = [ + // Default response + { + response: {}, + raw: "HTTP/1.1 200 OK\r\n" + "\r\n" + }, + // HTTP/1.1, chunked coding; empty trailer; close + { + response: { + status: 200, + body: new Buffer(new TextEncoder().encode("abcdef")) + }, + + raw: + "HTTP/1.1 200 OK\r\n" + + "transfer-encoding: chunked\r\n\r\n" + + "6\r\nabcdef\r\n0\r\n\r\n" + } +]; + +test(async function responseWrite() { + for (const testCase of responseTests) { + const buf = new Buffer(); + const bufw = new BufWriter(buf); + const request = new ServerRequest(); + request.w = bufw; + + await request.respond(testCase.response); + assertEqual(buf.toString(), testCase.raw); + } +}); diff --git a/test.ts b/test.ts index 44a6920158..8cb0a2ca2e 100644 --- a/test.ts +++ b/test.ts @@ -2,6 +2,7 @@ import { run } from "deno"; import "./buffer_test.ts"; import "./bufio_test.ts"; +import "./http_test.ts"; import "./textproto_test.ts"; import { runTests, completePromise } from "./file_server_test.ts";