mirror of
https://github.com/denoland/deno.git
synced 2024-11-23 15:16:54 -05:00
First pass at streaming http response (denoland/deno_std#16)
Original: 269665873a
This commit is contained in:
parent
579b92de59
commit
f6dae45cd2
6 changed files with 121 additions and 17 deletions
|
@ -17,7 +17,7 @@ function init() {
|
||||||
if (testBytes == null) {
|
if (testBytes == null) {
|
||||||
testBytes = new Uint8Array(N);
|
testBytes = new Uint8Array(N);
|
||||||
for (let i = 0; i < N; i++) {
|
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();
|
const decoder = new TextDecoder();
|
||||||
testString = decoder.decode(testBytes);
|
testString = decoder.decode(testBytes);
|
||||||
|
|
|
@ -109,7 +109,7 @@ test(async function bufioBufReader() {
|
||||||
for (let i = 0; i < texts.length - 1; i++) {
|
for (let i = 0; i < texts.length - 1; i++) {
|
||||||
texts[i] = str + "\n";
|
texts[i] = str + "\n";
|
||||||
all += texts[i];
|
all += texts[i];
|
||||||
str += String.fromCharCode((i % 26) + 97);
|
str += String.fromCharCode(i % 26 + 97);
|
||||||
}
|
}
|
||||||
texts[texts.length - 1] = all;
|
texts[texts.length - 1] = all;
|
||||||
|
|
||||||
|
@ -294,7 +294,7 @@ test(async function bufioWriter() {
|
||||||
const data = new Uint8Array(8192);
|
const data = new Uint8Array(8192);
|
||||||
|
|
||||||
for (let i = 0; i < data.byteLength; i++) {
|
for (let i = 0; i < data.byteLength; i++) {
|
||||||
data[i] = charCode(" ") + (i % (charCode("~") - charCode(" ")));
|
data[i] = charCode(" ") + i % (charCode("~") - charCode(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
const w = new Buffer();
|
const w = new Buffer();
|
||||||
|
|
|
@ -5,8 +5,13 @@
|
||||||
// TODO Add tests like these:
|
// TODO Add tests like these:
|
||||||
// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js
|
// https://github.com/indexzero/http-server/blob/master/test/http-server-test.js
|
||||||
|
|
||||||
import { listenAndServe, ServerRequest, setContentLength, Response } from "./http";
|
import {
|
||||||
import { cwd, readFile, DenoError, ErrorKind, args, stat, readDir } from "deno";
|
listenAndServe,
|
||||||
|
ServerRequest,
|
||||||
|
setContentLength,
|
||||||
|
Response
|
||||||
|
} from "./http";
|
||||||
|
import { cwd, DenoError, ErrorKind, args, stat, readDir, open } from "deno";
|
||||||
|
|
||||||
const dirViewerTemplate = `
|
const dirViewerTemplate = `
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
@ -146,9 +151,10 @@ async function serveDir(req: ServerRequest, dirPath: string, dirName: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function serveFile(req: ServerRequest, filename: 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();
|
const headers = new Headers();
|
||||||
headers.set("content-type", "octet-stream");
|
headers.set("content-length", fileInfo.len.toString());
|
||||||
|
|
||||||
const res = {
|
const res = {
|
||||||
status: 200,
|
status: 200,
|
||||||
|
@ -163,9 +169,9 @@ async function serveFallback(req: ServerRequest, e: Error) {
|
||||||
e instanceof DenoError &&
|
e instanceof DenoError &&
|
||||||
(e as DenoError<any>).kind === ErrorKind.NotFound
|
(e as DenoError<any>).kind === ErrorKind.NotFound
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
status: 404,
|
status: 404,
|
||||||
body: encoder.encode("Not found")
|
body: encoder.encode("Not found")
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
|
|
53
http.ts
53
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 { BufReader, BufState, BufWriter } from "./bufio.ts";
|
||||||
import { TextProtoReader } from "./textproto.ts";
|
import { TextProtoReader } from "./textproto.ts";
|
||||||
import { STATUS_TEXT } from "./http_status";
|
import { STATUS_TEXT } from "./http_status";
|
||||||
|
@ -96,16 +96,23 @@ export async function listenAndServe(
|
||||||
export interface Response {
|
export interface Response {
|
||||||
status?: number;
|
status?: number;
|
||||||
headers?: Headers;
|
headers?: Headers;
|
||||||
body?: Uint8Array;
|
body?: Uint8Array | Reader;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setContentLength(r: Response): void {
|
export function setContentLength(r: Response): void {
|
||||||
if (!r.headers) {
|
if (!r.headers) {
|
||||||
r.headers = new Headers();
|
r.headers = new Headers();
|
||||||
}
|
}
|
||||||
if (!r.headers.has("content-length")) {
|
|
||||||
const bodyLength = r.body ? r.body.byteLength : 0;
|
if (r.body) {
|
||||||
r.headers.append("Content-Length", bodyLength.toString());
|
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;
|
headers: Headers;
|
||||||
w: BufWriter;
|
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<void> {
|
async respond(r: Response): Promise<void> {
|
||||||
const protoMajor = 1;
|
const protoMajor = 1;
|
||||||
const protoMinor = 1;
|
const protoMinor = 1;
|
||||||
|
@ -139,9 +166,21 @@ export class ServerRequest {
|
||||||
const header = new TextEncoder().encode(out);
|
const header = new TextEncoder().encode(out);
|
||||||
let n = await this.w.write(header);
|
let n = await this.w.write(header);
|
||||||
assert(header.byteLength == n);
|
assert(header.byteLength == n);
|
||||||
|
|
||||||
if (r.body) {
|
if (r.body) {
|
||||||
n = await this.w.write(r.body);
|
if (r.body instanceof Uint8Array) {
|
||||||
assert(r.body.byteLength == n);
|
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();
|
await this.w.flush();
|
||||||
|
|
58
http_test.ts
Normal file
58
http_test.ts
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
});
|
1
test.ts
1
test.ts
|
@ -2,6 +2,7 @@ import { run } from "deno";
|
||||||
|
|
||||||
import "./buffer_test.ts";
|
import "./buffer_test.ts";
|
||||||
import "./bufio_test.ts";
|
import "./bufio_test.ts";
|
||||||
|
import "./http_test.ts";
|
||||||
import "./textproto_test.ts";
|
import "./textproto_test.ts";
|
||||||
import { runTests, completePromise } from "./file_server_test.ts";
|
import { runTests, completePromise } from "./file_server_test.ts";
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue