diff --git a/src/js/node/http.ts b/src/js/node/http.ts index 99bc862fde..0024ad4d5f 100644 --- a/src/js/node/http.ts +++ b/src/js/node/http.ts @@ -16,9 +16,9 @@ const enum NodeHTTPIncomingRequestType { NodeHTTPResponse, } const enum NodeHTTPHeaderState { - none, - assigned, - sent, + none = 0, + assigned = 1 << 0, + sent = 1 << 1, } const enum NodeHTTPBodyReadState { none, @@ -198,7 +198,7 @@ function validateMsecs(numberlike: any, field: string) { if (numberlike > TIMEOUT_MAX) { process.emitWarning( `${numberlike} does not fit into a 32-bit signed integer.` + `\nTimer duration was truncated to ${TIMEOUT_MAX}.`, - "TimeoutOverflowWarning" + "TimeoutOverflowWarning", ); return TIMEOUT_MAX; } @@ -1668,7 +1668,7 @@ const OutgoingMessagePrototype = { }, removeHeader(name) { - if (this[headerStateSymbol] === NodeHTTPHeaderState.sent) { + if (this[headerStateSymbol] >= NodeHTTPHeaderState.assigned) { throw $ERR_HTTP_HEADERS_SENT("Cannot remove header after headers have been sent."); } const headers = this[headersSymbol]; @@ -1683,6 +1683,41 @@ const OutgoingMessagePrototype = { return this; }, + setHeaders(headers) { + if (this[headerStateSymbol] >= NodeHTTPHeaderState.assigned) { + throw $ERR_HTTP_HEADERS_SENT("set"); + } + + if (!headers || Array.isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") { + throw $ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers); + } + + // Headers object joins multiple cookies with a comma when using + // the getter to retrieve the value, + // unless iterating over the headers directly. + // We also cannot safely split by comma. + // To avoid setHeader overwriting the previous value we push + // set-cookie values in array and set them all at once. + const cookies = []; + + for (const [key, value] of headers) { + if (key === "set-cookie") { + if (Array.isArray(value)) { + cookies.push(...value); + } else { + cookies.push(value); + } + continue; + } + this.setHeader(key, value); + } + if (cookies.length) { + this.setHeader("set-cookie", cookies); + } + + return this; + }, + hasHeader(name) { const headers = this[headersSymbol]; if (!headers) return false; @@ -1930,9 +1965,7 @@ const ServerResponsePrototype = { _removedContLen: false, _hasBody: true, get headersSent() { - return ( - this[headerStateSymbol] === NodeHTTPHeaderState.sent || this[headerStateSymbol] === NodeHTTPHeaderState.assigned - ); + return this[headerStateSymbol] >= NodeHTTPHeaderState.assigned; }, set headersSent(value) { this[headerStateSymbol] = value ? NodeHTTPHeaderState.sent : NodeHTTPHeaderState.none; @@ -2208,6 +2241,41 @@ const ServerResponsePrototype = { return this; }, + setHeaders(headers) { + if (this[headerStateSymbol] >= NodeHTTPHeaderState.assigned) { + throw $ERR_HTTP_HEADERS_SENT("set"); + } + + if (!headers || Array.isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") { + throw $ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers); + } + + // Headers object joins multiple cookies with a comma when using + // the getter to retrieve the value, + // unless iterating over the headers directly. + // We also cannot safely split by comma. + // To avoid setHeader overwriting the previous value we push + // set-cookie values in array and set them all at once. + const cookies = []; + + for (const [key, value] of headers) { + if (key === "set-cookie") { + if (Array.isArray(value)) { + cookies.push(...value); + } else { + cookies.push(value); + } + continue; + } + this.setHeader(key, value); + } + if (cookies.length) { + this.setHeader("set-cookie", cookies); + } + + return this; + }, + assignSocket(socket) { if (socket._httpMessage) { throw ERR_HTTP_SOCKET_ASSIGNED(); diff --git a/test/js/node/test/parallel/test-http-response-setheaders.js b/test/js/node/test/parallel/test-http-response-setheaders.js new file mode 100644 index 0000000000..2f52c54a49 --- /dev/null +++ b/test/js/node/test/parallel/test-http-response-setheaders.js @@ -0,0 +1,174 @@ +'use strict'; +const common = require('../common'); +const http = require('http'); +const assert = require('assert'); + +{ + const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => { + res.writeHead(200); // Headers already sent + const headers = new globalThis.Headers({ foo: '1' }); + assert.throws(() => { + res.setHeaders(headers); + }, { + code: 'ERR_HTTP_HEADERS_SENT' + }); + res.end(); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.headers.foo, undefined); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} + +{ + const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => { + assert.throws(() => { + res.setHeaders(['foo', '1']); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => { + res.setHeaders({ foo: '1' }); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => { + res.setHeaders(null); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => { + res.setHeaders(undefined); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => { + res.setHeaders('test'); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => { + res.setHeaders(1); + }, { + code: 'ERR_INVALID_ARG_TYPE' + }); + res.end(); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.headers.foo, undefined); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} + +{ + const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => { + const headers = new globalThis.Headers({ foo: '1', bar: '2' }); + res.setHeaders(headers); + res.writeHead(200); + res.end(); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(res.headers.foo, '1'); + assert.strictEqual(res.headers.bar, '2'); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} + +{ + const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => { + const headers = new globalThis.Headers({ foo: '1', bar: '2' }); + res.setHeaders(headers); + res.writeHead(200, ['foo', '3']); + res.end(); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(res.headers.foo, '3'); // Override by writeHead + assert.strictEqual(res.headers.bar, '2'); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} + +{ + const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => { + const headers = new Map([['foo', '1'], ['bar', '2']]); + res.setHeaders(headers); + res.writeHead(200); + res.end(); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert.strictEqual(res.statusCode, 200); + assert.strictEqual(res.headers.foo, '1'); + assert.strictEqual(res.headers.bar, '2'); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} + +{ + const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => { + const headers = new Headers(); + headers.append('Set-Cookie', 'a=b'); + headers.append('Set-Cookie', 'c=d'); + res.setHeaders(headers); + res.end(); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert(Array.isArray(res.headers['set-cookie'])); + assert.strictEqual(res.headers['set-cookie'].length, 2); + assert.strictEqual(res.headers['set-cookie'][0], 'a=b'); + assert.strictEqual(res.headers['set-cookie'][1], 'c=d'); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +} + +{ + const server = http.createServer({ requireHostHeader: false }, common.mustCall((req, res) => { + const headers = new Map(); + headers.set('Set-Cookie', ['a=b', 'c=d']); + res.setHeaders(headers); + res.end(); + })); + + server.listen(0, common.mustCall(() => { + http.get({ port: server.address().port }, (res) => { + assert(Array.isArray(res.headers['set-cookie'])); + assert.strictEqual(res.headers['set-cookie'].length, 2); + assert.strictEqual(res.headers['set-cookie'][0], 'a=b'); + assert.strictEqual(res.headers['set-cookie'][1], 'c=d'); + res.resume().on('end', common.mustCall(() => { + server.close(); + })); + }); + })); +}