mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 18:25:13 +08:00
http: add writeInformation to send arbitrary 1xx status codes
Signed-off-by: Tim Perry <pimterry@gmail.com> PR-URL: https://github.com/nodejs/node/pull/63155 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev>
This commit is contained in:
1 parent
c8206c8080
commit
03f27fef16
7 files changed
+306
-34
No files matched your search
@@ -2700,6 +2700,35 @@ been transmitted are equal or not.
|
||||
Attempting to set a header field name or value that contains invalid characters
|
||||
will result in a [`TypeError`][] being thrown.
|
||||
|
||||
### `response.writeInformation(statusCode[, headers][, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `statusCode` {number} An HTTP 1xx informational status code, between `100`
|
||||
and `199` inclusive, excluding `101` (Switching Protocols) which is only
|
||||
available through the [`'upgrade'`][] event.
|
||||
* `headers` {Object|Array} An optional set of headers to send with the
|
||||
informational response. Accepts the same shapes as
|
||||
[`response.writeHead()`][].
|
||||
* `callback` {Function} Optional, called once the message has been written
|
||||
to the socket.
|
||||
|
||||
Sends an arbitrary HTTP/1.1 1xx informational response to the client. This
|
||||
is a generic equivalent of [`response.writeContinue()`][],
|
||||
[`response.writeProcessing()`][] and [`response.writeEarlyHints()`][], and
|
||||
can be called multiple times before the final response. After the final
|
||||
response headers have been sent (via [`response.writeHead()`][] or an
|
||||
implicit header), calling this method throws `ERR_HTTP_HEADERS_SENT`.
|
||||
|
||||
Clients receive these responses via the [`'information'`][information event]
|
||||
event on `http.ClientRequest`.
|
||||
|
||||
```js
|
||||
response.writeInformation(110, { 'X-Progress': '50%' });
|
||||
```
|
||||
|
||||
### `response.writeProcessing()`
|
||||
|
||||
<!-- YAML
|
||||
@@ -4710,7 +4739,9 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
|
||||
[`response.write()`]: #responsewritechunk-encoding-callback
|
||||
[`response.write(data, encoding)`]: #responsewritechunk-encoding-callback
|
||||
[`response.writeContinue()`]: #responsewritecontinue
|
||||
[`response.writeEarlyHints()`]: #responsewriteearlyhintshints-callback
|
||||
[`response.writeHead()`]: #responsewriteheadstatuscode-statusmessage-headers
|
||||
[`response.writeProcessing()`]: #responsewriteprocessing
|
||||
[`server.close()`]: #serverclosecallback
|
||||
[`server.headersTimeout`]: #serverheaderstimeout
|
||||
[`server.keepAliveTimeoutBuffer`]: #serverkeepalivetimeoutbuffer
|
||||
@@ -4731,4 +4762,5 @@ const agent2 = new http.Agent({ proxyEnv: process.env });
|
||||
[`writable.destroyed`]: stream.md#writabledestroyed
|
||||
[`writable.uncork()`]: stream.md#writableuncork
|
||||
[`writable.write()`]: stream.md#writablewritechunk-encoding-callback
|
||||
[information event]: #event-information
|
||||
[initial delay]: net.md#socketsetkeepaliveenable-initialdelay
|
||||
@@ -4846,6 +4846,30 @@ response.writeEarlyHints({
|
||||
});
|
||||
```
|
||||
|
||||
#### `response.writeInformation(statusCode[, headers])`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `statusCode` {number} An HTTP 1xx informational status code, between `100`
|
||||
and `199` inclusive, excluding `101` (Switching Protocols) which is not
|
||||
allowed in HTTP/2.
|
||||
* `headers` {Object} An optional object of headers to send with the
|
||||
informational response.
|
||||
|
||||
Sends an arbitrary HTTP 1xx informational response, equivalent in HTTP/2 to a
|
||||
`HEADERS` frame whose `:status` pseudo-header is a 1xx code. May be called
|
||||
multiple times before the final response. After the final response headers
|
||||
have been sent, this method is a no-op and returns `false`.
|
||||
|
||||
This is the generic equivalent of [`response.writeContinue()`][] and
|
||||
[`response.writeEarlyHints()`][].
|
||||
|
||||
```js
|
||||
response.writeInformation(110, { 'X-Progress': '50%' });
|
||||
```
|
||||
|
||||
#### `response.writeHead(statusCode[, statusMessage][, headers])`
|
||||
|
||||
<!-- YAML
|
||||
@@ -5055,6 +5079,7 @@ you need to implement any fall-back behavior yourself.
|
||||
[`response.write()`]: #responsewritechunk-encoding-callback
|
||||
[`response.write(data, encoding)`]: http.md#responsewritechunk-encoding-callback
|
||||
[`response.writeContinue()`]: #responsewritecontinue
|
||||
[`response.writeEarlyHints()`]: #responsewriteearlyhintshints
|
||||
[`response.writeHead()`]: #responsewriteheadstatuscode-statusmessage-headers
|
||||
[`server.close()`]: #serverclosecallback
|
||||
[`server.maxHeadersCount`]: http.md#servermaxheaderscount
|
||||
|
||||
+55
-13
@@ -311,18 +311,66 @@ ServerResponse.prototype.detachSocket = function detachSocket(socket) {
|
||||
this.socket = null;
|
||||
};
|
||||
|
||||
ServerResponse.prototype.writeInformation = function writeInformation(
|
||||
statusCode, headers, cb) {
|
||||
if (this._header) {
|
||||
throw new ERR_HTTP_HEADERS_SENT('write');
|
||||
}
|
||||
|
||||
validateInteger(statusCode, 'statusCode', 100, 199);
|
||||
if (statusCode === 101) {
|
||||
throw new ERR_HTTP_INVALID_STATUS_CODE(statusCode);
|
||||
}
|
||||
|
||||
const statusMessage = STATUS_CODES[statusCode] || 'unknown';
|
||||
let head = `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`;
|
||||
|
||||
if (headers !== undefined && headers !== null) {
|
||||
if (ArrayIsArray(headers)) {
|
||||
if (headers.length && ArrayIsArray(headers[0])) {
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
const entry = headers[i];
|
||||
head += processInformationHeader(entry[0], entry[1]);
|
||||
}
|
||||
} else {
|
||||
if (headers.length % 2 !== 0) {
|
||||
throw new ERR_INVALID_ARG_VALUE('headers', headers);
|
||||
}
|
||||
for (let i = 0; i < headers.length; i += 2) {
|
||||
head += processInformationHeader(headers[i], headers[i + 1]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
validateObject(headers, 'headers');
|
||||
const keys = ObjectKeys(headers);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
head += processInformationHeader(key, headers[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
head += '\r\n';
|
||||
|
||||
return this._writeRaw(head, 'ascii', cb);
|
||||
};
|
||||
|
||||
function processInformationHeader(name, value) {
|
||||
validateHeaderName(name);
|
||||
validateHeaderValue(name, value);
|
||||
return `${name}: ${value}\r\n`;
|
||||
}
|
||||
|
||||
ServerResponse.prototype.writeContinue = function writeContinue(cb) {
|
||||
this._writeRaw('HTTP/1.1 100 Continue\r\n\r\n', 'ascii', cb);
|
||||
this.writeInformation(100, null, cb);
|
||||
this._sent100 = true;
|
||||
};
|
||||
|
||||
ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
|
||||
this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb);
|
||||
this.writeInformation(102, null, cb);
|
||||
};
|
||||
|
||||
ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) {
|
||||
let head = 'HTTP/1.1 103 Early Hints\r\n';
|
||||
|
||||
validateObject(hints, 'hints');
|
||||
|
||||
if (hints.link === null || hints.link === undefined) {
|
||||
@@ -339,22 +387,16 @@ ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) {
|
||||
throw new ERR_INVALID_CHAR('header content', 'Link');
|
||||
}
|
||||
|
||||
head += 'Link: ' + link + '\r\n';
|
||||
|
||||
const headers = { __proto__: null, Link: link };
|
||||
const keys = ObjectKeys(hints);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (key !== 'link') {
|
||||
validateHeaderName(key);
|
||||
const value = hints[key];
|
||||
validateHeaderValue(key, value);
|
||||
head += key + ': ' + value + '\r\n';
|
||||
headers[key] = hints[key];
|
||||
}
|
||||
}
|
||||
|
||||
head += '\r\n';
|
||||
|
||||
this._writeRaw(head, 'ascii', cb);
|
||||
this.writeInformation(103, headers, cb);
|
||||
};
|
||||
|
||||
ServerResponse.prototype._implicitHeader = function _implicitHeader() {
|
||||
|
||||
@@ -901,17 +901,39 @@ class Http2ServerResponse extends Stream {
|
||||
this[kStream].respond(headers, options);
|
||||
}
|
||||
|
||||
// TODO doesn't support callbacks
|
||||
writeContinue() {
|
||||
writeInformation(statusCode, headers) {
|
||||
if (typeof statusCode !== 'number' ||
|
||||
statusCode < 100 || statusCode > 199) {
|
||||
throw new ERR_HTTP2_STATUS_INVALID(statusCode);
|
||||
}
|
||||
if (statusCode === 101) {
|
||||
throw new ERR_HTTP2_STATUS_INVALID(statusCode);
|
||||
}
|
||||
|
||||
const stream = this[kStream];
|
||||
|
||||
if (stream.headersSent || this[kState].closed)
|
||||
return false;
|
||||
stream.additionalHeaders({
|
||||
[HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE,
|
||||
});
|
||||
|
||||
const outHeaders = { __proto__: null };
|
||||
if (headers !== undefined && headers !== null) {
|
||||
validateObject(headers, 'headers');
|
||||
const keys = ObjectKeys(headers);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
outHeaders[keys[i]] = headers[keys[i]];
|
||||
}
|
||||
}
|
||||
outHeaders[HTTP2_HEADER_STATUS] = statusCode;
|
||||
|
||||
stream.additionalHeaders(outHeaders);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO doesn't support callbacks
|
||||
writeContinue() {
|
||||
return this.writeInformation(HTTP_STATUS_CONTINUE);
|
||||
}
|
||||
|
||||
writeEarlyHints(hints) {
|
||||
validateObject(hints, 'hints');
|
||||
|
||||
@@ -933,18 +955,9 @@ class Http2ServerResponse extends Stream {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stream = this[kStream];
|
||||
headers.Link = linkHeaderValue;
|
||||
|
||||
if (stream.headersSent || this[kState].closed)
|
||||
return false;
|
||||
|
||||
stream.additionalHeaders({
|
||||
...headers,
|
||||
[HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS,
|
||||
'Link': linkHeaderValue,
|
||||
});
|
||||
|
||||
return true;
|
||||
return this.writeInformation(HTTP_STATUS_EARLY_HINTS, headers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,11 +9,7 @@ const countdown = new Countdown(2, () => server.close());
|
||||
|
||||
const server = http.createServer(common.mustCallAtLeast((req, res) => {
|
||||
console.error('Server sending informational message #1...');
|
||||
// These function calls may rewritten as necessary
|
||||
// to call res.writeHead instead
|
||||
res._writeRaw('HTTP/1.1 102 Processing\r\n');
|
||||
res._writeRaw('Foo: Bar\r\n');
|
||||
res._writeRaw('\r\n', common.mustCall());
|
||||
res.writeInformation(102, { Foo: 'Bar' }, common.mustCall());
|
||||
console.error('Server sending full response...');
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/plain',
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
'use strict';
|
||||
const common = require('../common');
|
||||
const assert = require('node:assert');
|
||||
const http = require('node:http');
|
||||
|
||||
// Happy flow: arbitrary 1xx status with custom headers, observed by the
|
||||
// client's 'information' event.
|
||||
{
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeInformation(110, { 'X-Progress': '50%', 'X-Stage': 'reading' });
|
||||
res.writeInformation(199, [['X-Custom', 'one'], ['X-Custom-2', 'two']]);
|
||||
res.end('done');
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const req = http.request({ port: server.address().port });
|
||||
|
||||
const seen = [];
|
||||
req.on('information', (res) => {
|
||||
seen.push({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
});
|
||||
});
|
||||
|
||||
req.on('response', common.mustCall((res) => {
|
||||
assert.strictEqual(res.statusCode, 200);
|
||||
|
||||
assert.strictEqual(seen.length, 2);
|
||||
assert.strictEqual(seen[0].statusCode, 110);
|
||||
assert.strictEqual(seen[0].headers['x-progress'], '50%');
|
||||
assert.strictEqual(seen[0].headers['x-stage'], 'reading');
|
||||
assert.strictEqual(seen[1].statusCode, 199);
|
||||
assert.strictEqual(seen[1].headers['x-custom'], 'one');
|
||||
assert.strictEqual(seen[1].headers['x-custom-2'], 'two');
|
||||
|
||||
res.resume();
|
||||
res.on('end', common.mustCall(() => server.close()));
|
||||
}));
|
||||
|
||||
req.end();
|
||||
}));
|
||||
}
|
||||
|
||||
// Headers argument is optional / nullable.
|
||||
{
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
res.writeInformation(150);
|
||||
res.writeInformation(151, null);
|
||||
res.end();
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const req = http.request({ port: server.address().port });
|
||||
let count = 0;
|
||||
req.on('information', () => count++);
|
||||
req.on('response', common.mustCall((res) => {
|
||||
assert.strictEqual(count, 2);
|
||||
res.resume();
|
||||
res.on('end', common.mustCall(() => server.close()));
|
||||
}));
|
||||
req.end();
|
||||
}));
|
||||
}
|
||||
|
||||
// Error cases.
|
||||
{
|
||||
const server = http.createServer(common.mustCall((req, res) => {
|
||||
assert.throws(() => res.writeInformation(101),
|
||||
{ code: 'ERR_HTTP_INVALID_STATUS_CODE' });
|
||||
assert.throws(() => res.writeInformation(99),
|
||||
{ code: 'ERR_OUT_OF_RANGE' });
|
||||
assert.throws(() => res.writeInformation(200),
|
||||
{ code: 'ERR_OUT_OF_RANGE' });
|
||||
assert.throws(() => res.writeInformation('100'),
|
||||
{ code: 'ERR_INVALID_ARG_TYPE' });
|
||||
assert.throws(() => res.writeInformation(150, { 'X-Bad\n': 'v' }),
|
||||
{ code: 'ERR_INVALID_HTTP_TOKEN' });
|
||||
|
||||
res.writeHead(200);
|
||||
assert.throws(() => res.writeInformation(150),
|
||||
{ code: 'ERR_HTTP_HEADERS_SENT' });
|
||||
res.end();
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const req = http.request({ port: server.address().port });
|
||||
req.on('response', common.mustCall((res) => {
|
||||
res.resume();
|
||||
res.on('end', common.mustCall(() => server.close()));
|
||||
}));
|
||||
req.end();
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) common.skip('missing crypto');
|
||||
|
||||
const assert = require('node:assert');
|
||||
const http2 = require('node:http2');
|
||||
|
||||
{
|
||||
const server = http2.createServer();
|
||||
|
||||
server.on('request', common.mustCall((req, res) => {
|
||||
res.writeInformation(110, { 'X-Progress': '50%' });
|
||||
res.writeInformation(199, { 'X-Custom': 'one' });
|
||||
res.end('done');
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
const req = client.request();
|
||||
|
||||
const seen = [];
|
||||
req.on('headers', (headers) => {
|
||||
seen.push({
|
||||
status: headers[':status'],
|
||||
headers,
|
||||
});
|
||||
});
|
||||
|
||||
req.on('response', common.mustCall((headers) => {
|
||||
assert.strictEqual(headers[':status'], 200);
|
||||
assert.strictEqual(seen.length, 2);
|
||||
assert.strictEqual(seen[0].status, 110);
|
||||
assert.strictEqual(seen[0].headers['x-progress'], '50%');
|
||||
assert.strictEqual(seen[1].status, 199);
|
||||
assert.strictEqual(seen[1].headers['x-custom'], 'one');
|
||||
}));
|
||||
|
||||
req.resume();
|
||||
req.on('end', common.mustCall(() => {
|
||||
client.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
// Error cases.
|
||||
{
|
||||
const server = http2.createServer();
|
||||
|
||||
server.on('request', common.mustCall((req, res) => {
|
||||
assert.throws(() => res.writeInformation(101),
|
||||
{ code: 'ERR_HTTP2_STATUS_INVALID' });
|
||||
assert.throws(() => res.writeInformation(200),
|
||||
{ code: 'ERR_HTTP2_STATUS_INVALID' });
|
||||
assert.throws(() => res.writeInformation('100'),
|
||||
{ code: 'ERR_HTTP2_STATUS_INVALID' });
|
||||
res.end();
|
||||
}));
|
||||
|
||||
server.listen(0, common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
const req = client.request();
|
||||
req.resume();
|
||||
req.on('end', common.mustCall(() => {
|
||||
client.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
Reference in new issue
Block a user