Skip to content

Commit

Permalink
feat: Support HTTP trailer headers for response (denoland#3938)
Browse files Browse the repository at this point in the history
  • Loading branch information
keroxp committed Feb 10, 2020
1 parent e8f639c commit e6f2041
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 14 deletions.
62 changes: 50 additions & 12 deletions std/http/server.ts
Expand Up @@ -12,14 +12,6 @@ import { deferred, Deferred, MuxAsyncIterator } from "../util/async.ts";

const encoder = new TextEncoder();

function bufWriter(w: Writer): BufWriter {
if (w instanceof BufWriter) {
return w;
} else {
return new BufWriter(w);
}
}

export function setContentLength(r: Response): void {
if (!r.headers) {
r.headers = new Headers();
Expand All @@ -30,16 +22,16 @@ export function setContentLength(r: Response): void {
// typeof r.body === "string" handled in writeResponse.
if (r.body instanceof Uint8Array) {
const bodyLength = r.body.byteLength;
r.headers.append("Content-Length", bodyLength.toString());
r.headers.set("content-length", bodyLength.toString());
} else {
r.headers.append("Transfer-Encoding", "chunked");
r.headers.set("transfer-encoding", "chunked");
}
}
}
}

async function writeChunkedBody(w: Writer, r: Reader): Promise<void> {
const writer = bufWriter(w);
const writer = BufWriter.create(w);

for await (const chunk of toAsyncIterator(r)) {
if (chunk.byteLength <= 0) continue;
Expand All @@ -53,13 +45,54 @@ async function writeChunkedBody(w: Writer, r: Reader): Promise<void> {
const endChunk = encoder.encode("0\r\n\r\n");
await writer.write(endChunk);
}
const kProhibitedTrailerHeaders = [
"transfer-encoding",
"content-length",
"trailer"
];

/** write trailer headers to writer. it mostly should be called after writeResponse */
export async function writeTrailers(
w: Writer,
headers: Headers,
trailers: Headers
): Promise<void> {
const trailer = headers.get("trailer");
if (trailer === null) {
throw new Error('response headers must have "trailer" header field');
}
const transferEncoding = headers.get("transfer-encoding");
if (transferEncoding === null || !transferEncoding.match(/^chunked/)) {
throw new Error(
`trailer headers is only allowed for "transfer-encoding: chunked": got "${transferEncoding}"`
);
}
const writer = BufWriter.create(w);
const trailerHeaderFields = trailer
.split(",")
.map(s => s.trim().toLowerCase());
for (const f of trailerHeaderFields) {
assert(
!kProhibitedTrailerHeaders.includes(f),
`"${f}" is prohibited for trailer header`
);
}
for (const [key, value] of trailers) {
assert(
trailerHeaderFields.includes(key),
`Not trailer header field: ${key}`
);
await writer.write(encoder.encode(`${key}: ${value}\r\n`));
}
await writer.flush();
}

export async function writeResponse(w: Writer, r: Response): Promise<void> {
const protoMajor = 1;
const protoMinor = 1;
const statusCode = r.status || 200;
const statusText = STATUS_TEXT.get(statusCode);
const writer = bufWriter(w);
const writer = BufWriter.create(w);
if (!statusText) {
throw Error("bad status code");
}
Expand Down Expand Up @@ -97,6 +130,10 @@ export async function writeResponse(w: Writer, r: Response): Promise<void> {
} else {
await writeChunkedBody(writer, r.body);
}
if (r.trailers) {
const t = await r.trailers();
await writeTrailers(writer, headers, t);
}
await writer.flush();
}

Expand Down Expand Up @@ -572,4 +609,5 @@ export interface Response {
status?: number;
headers?: Headers;
body?: Uint8Array | Reader | string;
trailers?: () => Promise<Headers> | Headers;
}
92 changes: 90 additions & 2 deletions std/http/server_test.ts
Expand Up @@ -8,14 +8,21 @@
const { Buffer } = Deno;
import { TextProtoReader } from "../textproto/mod.ts";
import { test, runIfMain } from "../testing/mod.ts";
import { assert, assertEquals, assertNotEquals } from "../testing/asserts.ts";
import {
assert,
assertEquals,
assertNotEquals,
assertThrowsAsync,
AssertionError
} from "../testing/asserts.ts";
import {
Response,
ServerRequest,
writeResponse,
serve,
readRequest,
parseHTTPVersion
parseHTTPVersion,
writeTrailers
} from "./server.ts";
import {
BufReader,
Expand Down Expand Up @@ -426,6 +433,35 @@ test(async function writeStringReaderResponse(): Promise<void> {
assertEquals(r.more, false);
});

test("writeResponse with trailer", async () => {
const w = new Buffer();
const body = new StringReader("Hello");
await writeResponse(w, {
status: 200,
headers: new Headers({
"transfer-encoding": "chunked",
trailer: "deno,node"
}),
body,
trailers: () => new Headers({ deno: "land", node: "js" })
});
const ret = w.toString();
const exp = [
"HTTP/1.1 200 OK",
"transfer-encoding: chunked",
"trailer: deno,node",
"",
"5",
"Hello",
"0",
"",
"deno: land",
"node: js",
""
].join("\r\n");
assertEquals(ret, exp);
});

test(async function readRequestError(): Promise<void> {
const input = `GET / HTTP/1.1
malformedHeader
Expand Down Expand Up @@ -733,4 +769,56 @@ if (Deno.build.os !== "win") {
});
}

test("writeTrailer", async () => {
const w = new Buffer();
await writeTrailers(
w,
new Headers({ "transfer-encoding": "chunked", trailer: "deno,node" }),
new Headers({ deno: "land", node: "js" })
);
assertEquals(w.toString(), "deno: land\r\nnode: js\r\n");
});

test("writeTrailer should throw", async () => {
const w = new Buffer();
await assertThrowsAsync(
() => {
return writeTrailers(w, new Headers(), new Headers());
},
Error,
'must have "trailer"'
);
await assertThrowsAsync(
() => {
return writeTrailers(w, new Headers({ trailer: "deno" }), new Headers());
},
Error,
"only allowed"
);
for (const f of ["content-length", "trailer", "transfer-encoding"]) {
await assertThrowsAsync(
() => {
return writeTrailers(
w,
new Headers({ "transfer-encoding": "chunked", trailer: f }),
new Headers({ [f]: "1" })
);
},
AssertionError,
"prohibited"
);
}
await assertThrowsAsync(
() => {
return writeTrailers(
w,
new Headers({ "transfer-encoding": "chunked", trailer: "deno" }),
new Headers({ node: "js" })
);
},
AssertionError,
"Not trailer"
);
});

runIfMain(import.meta);

0 comments on commit e6f2041

Please sign in to comment.