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:
parent
e8f639ce53
commit
e6f204199b
2 changed files with 140 additions and 14 deletions
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
Loading…
Reference in a new issue