Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -2702,6 +2702,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
Expand Down Expand Up @@ -4712,7 +4741,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
Expand All @@ -4733,4 +4764,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
25 changes: 25 additions & 0 deletions doc/api/http2.md
Original file line number Diff line number Diff line change
Expand Up @@ -4848,6 +4848,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
Expand Down Expand Up @@ -5057,6 +5081,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
Expand Down
68 changes: 55 additions & 13 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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() {
Expand Down
45 changes: 29 additions & 16 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -929,18 +951,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);
}
}

Expand Down
6 changes: 1 addition & 5 deletions test/parallel/test-http-information-headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
94 changes: 94 additions & 0 deletions test/parallel/test-http-write-information.js
Original file line number Diff line number Diff line change
@@ -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();
}));
}
Loading
Loading