mirror of
https://github.com/denoland/deno.git
synced 2024-11-28 16:20:57 -05:00
fix(ext/node): ServerResponse header array handling (#24149)
Previously res.setHeader("foo", ["bar", "baz"]) added a single header with a value of `bar,baz`. Really this should add two separate headers. This is visible in `set-cookie` for example.
This commit is contained in:
parent
d74be0842a
commit
3d41b486da
2 changed files with 63 additions and 15 deletions
|
@ -1333,7 +1333,8 @@ function onError(self, error, cb) {
|
||||||
export class ServerResponse extends NodeWritable {
|
export class ServerResponse extends NodeWritable {
|
||||||
statusCode = 200;
|
statusCode = 200;
|
||||||
statusMessage?: string = undefined;
|
statusMessage?: string = undefined;
|
||||||
#headers = new Headers({});
|
#headers: Record<string, string | string[]> = { __proto__: null };
|
||||||
|
#hasNonStringHeaders: boolean = false;
|
||||||
#readable: ReadableStream;
|
#readable: ReadableStream;
|
||||||
override writable = true;
|
override writable = true;
|
||||||
// used by `npm:on-finished`
|
// used by `npm:on-finished`
|
||||||
|
@ -1411,32 +1412,35 @@ export class ServerResponse extends NodeWritable {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
setHeader(name: string, value: string) {
|
setHeader(name: string, value: string | string[]) {
|
||||||
this.#headers.set(name, value);
|
if (Array.isArray(value)) {
|
||||||
|
this.#hasNonStringHeaders = true;
|
||||||
|
}
|
||||||
|
this.#headers[name] = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
getHeader(name: string) {
|
getHeader(name: string) {
|
||||||
return this.#headers.get(name) ?? undefined;
|
return this.#headers[name];
|
||||||
}
|
}
|
||||||
removeHeader(name: string) {
|
removeHeader(name: string) {
|
||||||
return this.#headers.delete(name);
|
delete this.#headers[name];
|
||||||
}
|
}
|
||||||
getHeaderNames() {
|
getHeaderNames() {
|
||||||
return Array.from(this.#headers.keys());
|
return Object.keys(this.#headers);
|
||||||
}
|
}
|
||||||
getHeaders() {
|
getHeaders() {
|
||||||
return Object.fromEntries(this.#headers.entries());
|
return { __proto__: null, ...this.#headers };
|
||||||
}
|
}
|
||||||
hasHeader(name: string) {
|
hasHeader(name: string) {
|
||||||
return this.#headers.has(name);
|
return Object.hasOwn(this.#headers, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
writeHead(status: number, headers: Record<string, string> = {}) {
|
writeHead(status: number, headers: Record<string, string> = {}) {
|
||||||
this.statusCode = status;
|
this.statusCode = status;
|
||||||
for (const k in headers) {
|
for (const k in headers) {
|
||||||
if (Object.hasOwn(headers, k)) {
|
if (Object.hasOwn(headers, k)) {
|
||||||
this.#headers.set(k, headers[k]);
|
this.setHeader(k, headers[k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
|
@ -1461,9 +1465,26 @@ export class ServerResponse extends NodeWritable {
|
||||||
if (ServerResponse.#bodyShouldBeNull(this.statusCode)) {
|
if (ServerResponse.#bodyShouldBeNull(this.statusCode)) {
|
||||||
body = null;
|
body = null;
|
||||||
}
|
}
|
||||||
|
let headers: Record<string, string> | [string, string][] = this
|
||||||
|
.#headers as Record<string, string>;
|
||||||
|
if (this.#hasNonStringHeaders) {
|
||||||
|
headers = [];
|
||||||
|
// Guard is not needed as this is a null prototype object.
|
||||||
|
// deno-lint-ignore guard-for-in
|
||||||
|
for (const key in this.#headers) {
|
||||||
|
const entry = this.#headers[key];
|
||||||
|
if (Array.isArray(entry)) {
|
||||||
|
for (const value of entry) {
|
||||||
|
headers.push([key, value]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers.push([key, entry]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
this.#resolve(
|
this.#resolve(
|
||||||
new Response(body, {
|
new Response(body, {
|
||||||
headers: this.#headers,
|
headers,
|
||||||
status: this.statusCode,
|
status: this.statusCode,
|
||||||
statusText: this.statusMessage,
|
statusText: this.statusMessage,
|
||||||
}),
|
}),
|
||||||
|
@ -1473,11 +1494,11 @@ export class ServerResponse extends NodeWritable {
|
||||||
// deno-lint-ignore no-explicit-any
|
// deno-lint-ignore no-explicit-any
|
||||||
override end(chunk?: any, encoding?: any, cb?: any): this {
|
override end(chunk?: any, encoding?: any, cb?: any): this {
|
||||||
this.finished = true;
|
this.finished = true;
|
||||||
if (!chunk && this.#headers.has("transfer-encoding")) {
|
if (!chunk && "transfer-encoding" in this.#headers) {
|
||||||
// FIXME(bnoordhuis) Node sends a zero length chunked body instead, i.e.,
|
// FIXME(bnoordhuis) Node sends a zero length chunked body instead, i.e.,
|
||||||
// the trailing "0\r\n", but respondWith() just hangs when I try that.
|
// the trailing "0\r\n", but respondWith() just hangs when I try that.
|
||||||
this.#headers.set("content-length", "0");
|
this.#headers["content-length"] = "0";
|
||||||
this.#headers.delete("transfer-encoding");
|
delete this.#headers["transfer-encoding"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error The signature for cb is stricter than the one implemented here
|
// @ts-expect-error The signature for cb is stricter than the one implemented here
|
||||||
|
|
|
@ -182,6 +182,33 @@ Deno.test("[node/http] server can respond with 101, 204, 205, 304 status", async
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("[node/http] multiple set-cookie headers", async () => {
|
||||||
|
const { promise, resolve } = Promise.withResolvers<void>();
|
||||||
|
|
||||||
|
const server = http.createServer((_req, res) => {
|
||||||
|
res.setHeader("Set-Cookie", ["foo=bar", "bar=foo"]);
|
||||||
|
assertEquals(res.getHeader("Set-Cookie"), ["foo=bar", "bar=foo"]);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(async () => {
|
||||||
|
const res = await fetch(
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
|
`http://127.0.0.1:${(server.address() as any).port}/`,
|
||||||
|
);
|
||||||
|
assert(res.ok);
|
||||||
|
|
||||||
|
const setCookieHeaders = res.headers.getSetCookie();
|
||||||
|
assertEquals(setCookieHeaders, ["foo=bar", "bar=foo"]);
|
||||||
|
|
||||||
|
await res.body!.cancel();
|
||||||
|
|
||||||
|
server.close(() => resolve());
|
||||||
|
});
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("[node/http] IncomingRequest socket has remoteAddress + remotePort", async () => {
|
Deno.test("[node/http] IncomingRequest socket has remoteAddress + remotePort", async () => {
|
||||||
const { promise, resolve } = Promise.withResolvers<void>();
|
const { promise, resolve } = Promise.withResolvers<void>();
|
||||||
|
|
||||||
|
@ -1000,8 +1027,8 @@ Deno.test("[node/http] ServerResponse getHeaders", () => {
|
||||||
const res = new http.ServerResponse(req);
|
const res = new http.ServerResponse(req);
|
||||||
res.setHeader("foo", "bar");
|
res.setHeader("foo", "bar");
|
||||||
res.setHeader("bar", "baz");
|
res.setHeader("bar", "baz");
|
||||||
assertEquals(res.getHeaderNames(), ["bar", "foo"]);
|
assertEquals(res.getHeaderNames(), ["foo", "bar"]);
|
||||||
assertEquals(res.getHeaders(), { "bar": "baz", "foo": "bar" });
|
assertEquals(res.getHeaders(), { "foo": "bar", "bar": "baz" });
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("[node/http] ServerResponse default status code 200", () => {
|
Deno.test("[node/http] ServerResponse default status code 200", () => {
|
||||||
|
|
Loading…
Reference in a new issue