mirror of
https://github.com/denoland/deno.git
synced 2024-10-30 09:08:00 -04: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();
|
||||
|
||||
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<void> {
|
||||
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<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");
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -572,4 +609,5 @@ export interface Response {
|
|||
status?: number;
|
||||
headers?: Headers;
|
||||
body?: Uint8Array | Reader | string;
|
||||
trailers?: () => Promise<Headers> | Headers;
|
||||
}
|
||||
|
|
|
@ -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<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
|
||||
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue