summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--http/README.md19
-rwxr-xr-xhttp/file_server.ts12
-rw-r--r--http/http_bench.ts11
-rw-r--r--http/readers.ts78
-rw-r--r--http/readers_test.ts12
-rw-r--r--http/server.ts438
-rw-r--r--http/server_test.ts443
-rw-r--r--io/readers_test.ts4
-rwxr-xr-xtest.ts3
-rw-r--r--util/deferred.ts42
-rw-r--r--util/deferred_test.ts16
11 files changed, 372 insertions, 706 deletions
diff --git a/http/README.md b/http/README.md
index 67c578f31..2c9a90853 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/(?<id>.+)"), 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 4aebd4957..1f3fdd586 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 8ca3bb33c..d80b2b103 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<void> {
- 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 f14955755..000000000
--- 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<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 };
- }
- }
-}
diff --git a/http/readers_test.ts b/http/readers_test.ts
deleted file mode 100644
index 4fd379feb..000000000
--- 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 a80becbd5..400171fc5 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<void>;
-
- respondJson(obj: any, headers?: Headers): Promise<void>;
-
- respondText(text: string, headers?: Headers): Promise<void>;
-
- 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);
- }
-}
-
-export interface HttpServer {
- handle(pattern: string | RegExp, handler: HttpHandler);
-
- listen(addr: string, cancel?: Deferred): Promise<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 { 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;
- }
+ for await (const request of server) {
+ await handler(request);
}
- 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<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 interface Response {
+ status?: number;
+ headers?: Headers;
+ body?: Uint8Array | Reader;
}
-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<Uint8Array> {
+ return readAllIterator(this.bodyStream());
+ }
+
+ async respond(r: Response): Promise<void> {
+ 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<void> {
+export async function writeResponse(w: Writer, r: Response): Promise<void> {
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<ServerRequest> {
- 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<ServerResponse> {
- 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<Uint8Array>
+): Promise<Uint8Array> {
+ const chunks = [];
+ let len = 0;
+ for await (const chunk of it) {
+ chunks.push(chunk);
+ len += chunk.length;
+ }
+ if (chunks.length === 0) {
+ // No need for copy
+ return chunks[0];
}
- const [proto, status, statusText] = line.split(" ", 3);
- const [headers, headersErr] = await tp.readMIMEHeader();
- if (headersErr) {
- throw headersErr;
+ const collected = new Uint8Array(len);
+ let offset = 0;
+ for (let chunk of chunks) {
+ collected.set(chunk, offset);
+ offset += chunk.length;
}
- 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 };
+ return collected;
}
diff --git a/http/server_test.ts b/http/server_test.ts
index 4f22e4a06..099547d0c 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;
-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);
+ await request.respond(testCase.response);
+ assertEqual(buf.toString(), testCase.raw);
+ }
});
-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(async function requestBodyWithContentLength() {
+ {
+ 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");
+ }
-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
- );
+ // 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 body = dec.decode(await req.body());
+ assertEqual(body, longText);
+ }
});
-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"));
+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;
}
- } finally {
- cancel.resolve();
+ 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);
}
-});
-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"
+ // 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;
}
- };
- 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");
+ 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 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 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;
+ }
+ }
-test(async function httpServerResponderShouldThrow() {
- const w = new Buffer();
+ // Larger than internal buf
{
- 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 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 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 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 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("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 add59877d..0bc8ca36a 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 dd311c527..e94bdcee6 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 f52087547..000000000
--- a/util/deferred.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-// 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"
- );
-}
diff --git a/util/deferred_test.ts b/util/deferred_test.ts
deleted file mode 100644
index a397b3012..000000000
--- 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
- );
-});