diff --git a/std/http/server.ts b/std/http/server.ts index 0bd5d06b5afae..b8a41379f0b6e 100644 --- a/std/http/server.ts +++ b/std/http/server.ts @@ -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(); @@ -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 { - const writer = bufWriter(w); + const writer = BufWriter.create(w); for await (const chunk of toAsyncIterator(r)) { if (chunk.byteLength <= 0) continue; @@ -53,13 +45,54 @@ async function writeChunkedBody(w: Writer, r: Reader): Promise { 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 { + 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 { 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"); } @@ -97,6 +130,10 @@ export async function writeResponse(w: Writer, r: Response): Promise { } else { await writeChunkedBody(writer, r.body); } + if (r.trailers) { + const t = await r.trailers(); + await writeTrailers(writer, headers, t); + } await writer.flush(); } @@ -572,4 +609,5 @@ export interface Response { status?: number; headers?: Headers; body?: Uint8Array | Reader | string; + trailers?: () => Promise | Headers; } diff --git a/std/http/server_test.ts b/std/http/server_test.ts index c14d63145a047..10b0fec20fec7 100644 --- a/std/http/server_test.ts +++ b/std/http/server_test.ts @@ -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, @@ -426,6 +433,35 @@ test(async function writeStringReaderResponse(): Promise { 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 { const input = `GET / HTTP/1.1 malformedHeader @@ -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);