1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 23:34:47 -05:00

feat: Support HTTP trailer headers for response (#3938)

This commit is contained in:
Yusuke Sakurai 2020-02-11 01:38:48 +09:00 committed by GitHub
parent e8f639ce53
commit e6f204199b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 140 additions and 14 deletions

View file

@ -12,14 +12,6 @@ import { deferred, Deferred, MuxAsyncIterator } from "../util/async.ts";
const encoder = new TextEncoder(); 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 { export function setContentLength(r: Response): void {
if (!r.headers) { if (!r.headers) {
r.headers = new Headers(); r.headers = new Headers();
@ -30,16 +22,16 @@ export function setContentLength(r: Response): void {
// typeof r.body === "string" handled in writeResponse. // typeof r.body === "string" handled in writeResponse.
if (r.body instanceof Uint8Array) { if (r.body instanceof Uint8Array) {
const bodyLength = r.body.byteLength; const bodyLength = r.body.byteLength;
r.headers.append("Content-Length", bodyLength.toString()); r.headers.set("content-length", bodyLength.toString());
} else { } else {
r.headers.append("Transfer-Encoding", "chunked"); r.headers.set("transfer-encoding", "chunked");
} }
} }
} }
} }
async function writeChunkedBody(w: Writer, r: Reader): Promise<void> { 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)) { for await (const chunk of toAsyncIterator(r)) {
if (chunk.byteLength <= 0) continue; if (chunk.byteLength <= 0) continue;
@ -53,13 +45,54 @@ async function writeChunkedBody(w: Writer, r: Reader): Promise<void> {
const endChunk = encoder.encode("0\r\n\r\n"); const endChunk = encoder.encode("0\r\n\r\n");
await writer.write(endChunk); 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> { export async function writeResponse(w: Writer, r: Response): Promise<void> {
const protoMajor = 1; const protoMajor = 1;
const protoMinor = 1; const protoMinor = 1;
const statusCode = r.status || 200; const statusCode = r.status || 200;
const statusText = STATUS_TEXT.get(statusCode); const statusText = STATUS_TEXT.get(statusCode);
const writer = bufWriter(w); const writer = BufWriter.create(w);
if (!statusText) { if (!statusText) {
throw Error("bad status code"); throw Error("bad status code");
} }
@ -97,6 +130,10 @@ export async function writeResponse(w: Writer, r: Response): Promise<void> {
} else { } else {
await writeChunkedBody(writer, r.body); await writeChunkedBody(writer, r.body);
} }
if (r.trailers) {
const t = await r.trailers();
await writeTrailers(writer, headers, t);
}
await writer.flush(); await writer.flush();
} }
@ -572,4 +609,5 @@ export interface Response {
status?: number; status?: number;
headers?: Headers; headers?: Headers;
body?: Uint8Array | Reader | string; body?: Uint8Array | Reader | string;
trailers?: () => Promise<Headers> | Headers;
} }

View file

@ -8,14 +8,21 @@
const { Buffer } = Deno; const { Buffer } = Deno;
import { TextProtoReader } from "../textproto/mod.ts"; import { TextProtoReader } from "../textproto/mod.ts";
import { test, runIfMain } from "../testing/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 { import {
Response, Response,
ServerRequest, ServerRequest,
writeResponse, writeResponse,
serve, serve,
readRequest, readRequest,
parseHTTPVersion parseHTTPVersion,
writeTrailers
} from "./server.ts"; } from "./server.ts";
import { import {
BufReader, BufReader,
@ -426,6 +433,35 @@ test(async function writeStringReaderResponse(): Promise<void> {
assertEquals(r.more, false); 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> { test(async function readRequestError(): Promise<void> {
const input = `GET / HTTP/1.1 const input = `GET / HTTP/1.1
malformedHeader 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); runIfMain(import.meta);