GeezerVison is showing github.com — http: add writeInformation to send arbitrary 1xx status codes · nodejs/node@0cbb389
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/http.md +32Lines changed: 32 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2696,6 +2696,35 @@ been transmitted are equal or not. | 2696 | 2696 | Attempting to set a header field name or value that contains invalid characters | 2697 | 2697 | will result in a [`TypeError`][] being thrown. | 2698 | 2698 |
| | 2699 | +### `response.writeInformation(statusCode[, headers][, callback])` | | 2700 | + | | 2701 | +<!-- YAML | | 2702 | +added: REPLACEME | | 2703 | +--> | | 2704 | + | | 2705 | +* `statusCode` {number} An HTTP 1xx informational status code, between `100` | | 2706 | + and `199` inclusive, excluding `101` (Switching Protocols) which is only | | 2707 | + available through the [`'upgrade'`][] event. | | 2708 | +* `headers` {Object|Array} An optional set of headers to send with the | | 2709 | + informational response. Accepts the same shapes as | | 2710 | + [`response.writeHead()`][]. | | 2711 | +* `callback` {Function} Optional, called once the message has been written | | 2712 | + to the socket. | | 2713 | + | | 2714 | +Sends an arbitrary HTTP/1.1 1xx informational response to the client. This | | 2715 | +is a generic equivalent of [`response.writeContinue()`][], | | 2716 | +[`response.writeProcessing()`][] and [`response.writeEarlyHints()`][], and | | 2717 | +can be called multiple times before the final response. After the final | | 2718 | +response headers have been sent (via [`response.writeHead()`][] or an | | 2719 | +implicit header), calling this method throws `ERR_HTTP_HEADERS_SENT`. | | 2720 | + | | 2721 | +Clients receive these responses via the [`'information'`][information event] | | 2722 | +event on `http.ClientRequest`. | | 2723 | + | | 2724 | +```js | | 2725 | +response.writeInformation(110, { 'X-Progress': '50%' }); | | 2726 | +``` | | 2727 | + | 2699 | 2728 | ### `response.writeProcessing()` | 2700 | 2729 |
| 2701 | 2730 | <!-- YAML | @@ -4694,7 +4723,9 @@ const agent2 = new http.Agent({ proxyEnv: process.env }); | 4694 | 4723 | [`response.write()`]: #responsewritechunk-encoding-callback | 4695 | 4724 | [`response.write(data, encoding)`]: #responsewritechunk-encoding-callback | 4696 | 4725 | [`response.writeContinue()`]: #responsewritecontinue | | 4726 | +[`response.writeEarlyHints()`]: #responsewriteearlyhintshints-callback | 4697 | 4727 | [`response.writeHead()`]: #responsewriteheadstatuscode-statusmessage-headers | | 4728 | +[`response.writeProcessing()`]: #responsewriteprocessing | 4698 | 4729 | [`server.close()`]: #serverclosecallback | 4699 | 4730 | [`server.headersTimeout`]: #serverheaderstimeout | 4700 | 4731 | [`server.keepAliveTimeoutBuffer`]: #serverkeepalivetimeoutbuffer | @@ -4715,4 +4746,5 @@ const agent2 = new http.Agent({ proxyEnv: process.env }); | 4715 | 4746 | [`writable.destroyed`]: stream.md#writabledestroyed | 4716 | 4747 | [`writable.uncork()`]: stream.md#writableuncork | 4717 | 4748 | [`writable.write()`]: stream.md#writablewritechunk-encoding-callback | | 4749 | +[information event]: #event-information | 4718 | 4750 | [initial delay]: net.md#socketsetkeepaliveenable-initialdelay |
Collapse file Copy file name to clipboardExpand all lines: doc/api/http2.md +25Lines changed: 25 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -4832,6 +4832,30 @@ response.writeEarlyHints({ | 4832 | 4832 | }); | 4833 | 4833 | ``` | 4834 | 4834 |
| | 4835 | +#### `response.writeInformation(statusCode[, headers])` | | 4836 | + | | 4837 | +<!-- YAML | | 4838 | +added: REPLACEME | | 4839 | +--> | | 4840 | + | | 4841 | +* `statusCode` {number} An HTTP 1xx informational status code, between `100` | | 4842 | + and `199` inclusive, excluding `101` (Switching Protocols) which is not | | 4843 | + allowed in HTTP/2. | | 4844 | +* `headers` {Object} An optional object of headers to send with the | | 4845 | + informational response. | | 4846 | + | | 4847 | +Sends an arbitrary HTTP 1xx informational response, equivalent in HTTP/2 to a | | 4848 | +`HEADERS` frame whose `:status` pseudo-header is a 1xx code. May be called | | 4849 | +multiple times before the final response. After the final response headers | | 4850 | +have been sent, this method is a no-op and returns `false`. | | 4851 | + | | 4852 | +This is the generic equivalent of [`response.writeContinue()`][] and | | 4853 | +[`response.writeEarlyHints()`][]. | | 4854 | + | | 4855 | +```js | | 4856 | +response.writeInformation(110, { 'X-Progress': '50%' }); | | 4857 | +``` | | 4858 | + | 4835 | 4859 | #### `response.writeHead(statusCode[, statusMessage][, headers])` | 4836 | 4860 |
| 4837 | 4861 | <!-- YAML | @@ -5041,6 +5065,7 @@ you need to implement any fall-back behavior yourself. | 5041 | 5065 | [`response.write()`]: #responsewritechunk-encoding-callback | 5042 | 5066 | [`response.write(data, encoding)`]: http.md#responsewritechunk-encoding-callback | 5043 | 5067 | [`response.writeContinue()`]: #responsewritecontinue | | 5068 | +[`response.writeEarlyHints()`]: #responsewriteearlyhintshints | 5044 | 5069 | [`response.writeHead()`]: #responsewriteheadstatuscode-statusmessage-headers | 5045 | 5070 | [`server.close()`]: #serverclosecallback | 5046 | 5071 | [`server.maxHeadersCount`]: http.md#servermaxheaderscount | |
Collapse file Copy file name to clipboardExpand all lines: lib/_http_server.js +55-13Lines changed: 55 additions & 13 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -311,18 +311,66 @@ ServerResponse.prototype.detachSocket = function detachSocket(socket) { | 311 | 311 | this.socket = null; | 312 | 312 | }; | 313 | 313 |
| | 314 | +ServerResponse.prototype.writeInformation = function writeInformation( | | 315 | + statusCode, headers, cb) { | | 316 | + if (this._header) { | | 317 | + throw new ERR_HTTP_HEADERS_SENT('write'); | | 318 | + } | | 319 | + | | 320 | + validateInteger(statusCode, 'statusCode', 100, 199); | | 321 | + if (statusCode === 101) { | | 322 | + throw new ERR_HTTP_INVALID_STATUS_CODE(statusCode); | | 323 | + } | | 324 | + | | 325 | + const statusMessage = STATUS_CODES[statusCode] || 'unknown'; | | 326 | + let head = `HTTP/1.1 ${statusCode} ${statusMessage}\r\n`; | | 327 | + | | 328 | + if (headers !== undefined && headers !== null) { | | 329 | + if (ArrayIsArray(headers)) { | | 330 | + if (headers.length && ArrayIsArray(headers[0])) { | | 331 | + for (let i = 0; i < headers.length; i++) { | | 332 | + const entry = headers[i]; | | 333 | + head += processInformationHeader(entry[0], entry[1]); | | 334 | + } | | 335 | + } else { | | 336 | + if (headers.length % 2 !== 0) { | | 337 | + throw new ERR_INVALID_ARG_VALUE('headers', headers); | | 338 | + } | | 339 | + for (let i = 0; i < headers.length; i += 2) { | | 340 | + head += processInformationHeader(headers[i], headers[i + 1]); | | 341 | + } | | 342 | + } | | 343 | + } else { | | 344 | + validateObject(headers, 'headers'); | | 345 | + const keys = ObjectKeys(headers); | | 346 | + for (let i = 0; i < keys.length; i++) { | | 347 | + const key = keys[i]; | | 348 | + head += processInformationHeader(key, headers[key]); | | 349 | + } | | 350 | + } | | 351 | + } | | 352 | + | | 353 | + head += '\r\n'; | | 354 | + | | 355 | + return this._writeRaw(head, 'ascii', cb); | | 356 | +}; | | 357 | + | | 358 | +function processInformationHeader(name, value) { | | 359 | + validateHeaderName(name); | | 360 | + validateHeaderValue(name, value); | | 361 | + return `${name}: ${value}\r\n`; | | 362 | +} | | 363 | + | 314 | 364 | ServerResponse.prototype.writeContinue = function writeContinue(cb) { | 315 | | - this._writeRaw('HTTP/1.1 100 Continue\r\n\r\n', 'ascii', cb); | | 365 | + this.writeInformation(100, null, cb); | 316 | 366 | this._sent100 = true; | 317 | 367 | }; | 318 | 368 |
| 319 | 369 | ServerResponse.prototype.writeProcessing = function writeProcessing(cb) { | 320 | | - this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb); | | 370 | + this.writeInformation(102, null, cb); | 321 | 371 | }; | 322 | 372 |
| 323 | 373 | ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) { | 324 | | - let head = 'HTTP/1.1 103 Early Hints\r\n'; | 325 | | - | 326 | 374 | validateObject(hints, 'hints'); | 327 | 375 |
| 328 | 376 | if (hints.link === null || hints.link === undefined) { | @@ -339,22 +387,16 @@ ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) { | 339 | 387 | throw new ERR_INVALID_CHAR('header content', 'Link'); | 340 | 388 | } | 341 | 389 |
| 342 | | - head += 'Link: ' + link + '\r\n'; | 343 | | - | | 390 | + const headers = { __proto__: null, Link: link }; | 344 | 391 | const keys = ObjectKeys(hints); | 345 | 392 | for (let i = 0; i < keys.length; i++) { | 346 | 393 | const key = keys[i]; | 347 | 394 | if (key !== 'link') { | 348 | | - validateHeaderName(key); | 349 | | - const value = hints[key]; | 350 | | - validateHeaderValue(key, value); | 351 | | - head += key + ': ' + value + '\r\n'; | | 395 | + headers[key] = hints[key]; | 352 | 396 | } | 353 | 397 | } | 354 | 398 |
| 355 | | - head += '\r\n'; | 356 | | - | 357 | | - this._writeRaw(head, 'ascii', cb); | | 399 | + this.writeInformation(103, headers, cb); | 358 | 400 | }; | 359 | 401 |
| 360 | 402 | ServerResponse.prototype._implicitHeader = function _implicitHeader() { | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/http2/compat.js +29-16Lines changed: 29 additions & 16 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -901,17 +901,39 @@ class Http2ServerResponse extends Stream { | 901 | 901 | this[kStream].respond(headers, options); | 902 | 902 | } | 903 | 903 |
| 904 | | - // TODO doesn't support callbacks | 905 | | - writeContinue() { | | 904 | + writeInformation(statusCode, headers) { | | 905 | + if (typeof statusCode !== 'number' || | | 906 | + statusCode < 100 || statusCode > 199) { | | 907 | + throw new ERR_HTTP2_STATUS_INVALID(statusCode); | | 908 | + } | | 909 | + if (statusCode === 101) { | | 910 | + throw new ERR_HTTP2_STATUS_INVALID(statusCode); | | 911 | + } | | 912 | + | 906 | 913 | const stream = this[kStream]; | | 914 | + | 907 | 915 | if (stream.headersSent || this[kState].closed) | 908 | 916 | return false; | 909 | | - stream.additionalHeaders({ | 910 | | - [HTTP2_HEADER_STATUS]: HTTP_STATUS_CONTINUE, | 911 | | - }); | | 917 | + | | 918 | + const outHeaders = { __proto__: null }; | | 919 | + if (headers !== undefined && headers !== null) { | | 920 | + validateObject(headers, 'headers'); | | 921 | + const keys = ObjectKeys(headers); | | 922 | + for (let i = 0; i < keys.length; i++) { | | 923 | + outHeaders[keys[i]] = headers[keys[i]]; | | 924 | + } | | 925 | + } | | 926 | + outHeaders[HTTP2_HEADER_STATUS] = statusCode; | | 927 | + | | 928 | + stream.additionalHeaders(outHeaders); | 912 | 929 | return true; | 913 | 930 | } | 914 | 931 |
| | 932 | + // TODO doesn't support callbacks | | 933 | + writeContinue() { | | 934 | + return this.writeInformation(HTTP_STATUS_CONTINUE); | | 935 | + } | | 936 | + | 915 | 937 | writeEarlyHints(hints) { | 916 | 938 | validateObject(hints, 'hints'); | 917 | 939 |
| @@ -933,18 +955,9 @@ class Http2ServerResponse extends Stream { | 933 | 955 | return false; | 934 | 956 | } | 935 | 957 |
| 936 | | - const stream = this[kStream]; | | 958 | + headers.Link = linkHeaderValue; | 937 | 959 |
| 938 | | - if (stream.headersSent || this[kState].closed) | 939 | | - return false; | 940 | | - | 941 | | - stream.additionalHeaders({ | 942 | | - ...headers, | 943 | | - [HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS, | 944 | | - 'Link': linkHeaderValue, | 945 | | - }); | 946 | | - | 947 | | - return true; | | 960 | + return this.writeInformation(HTTP_STATUS_EARLY_HINTS, headers); | 948 | 961 | } | 949 | 962 | } | 950 | 963 |
| |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-http-information-headers.js +1-5Lines changed: 1 addition & 5 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -9,11 +9,7 @@ const countdown = new Countdown(2, () => server.close()); | 9 | 9 |
| 10 | 10 | const server = http.createServer(common.mustCallAtLeast((req, res) => { | 11 | 11 | console.error('Server sending informational message #1...'); | 12 | | - // These function calls may rewritten as necessary | 13 | | - // to call res.writeHead instead | 14 | | - res._writeRaw('HTTP/1.1 102 Processing\r\n'); | 15 | | - res._writeRaw('Foo: Bar\r\n'); | 16 | | - res._writeRaw('\r\n', common.mustCall()); | | 12 | + res.writeInformation(102, { Foo: 'Bar' }, common.mustCall()); | 17 | 13 | console.error('Server sending full response...'); | 18 | 14 | res.writeHead(200, { | 19 | 15 | 'Content-Type': 'text/plain', | |
Collapse file Copy file name to clipboard +94Lines changed: 94 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +'use strict'; | | 2 | +const common = require('../common'); | | 3 | +const assert = require('node:assert'); | | 4 | +const http = require('node:http'); | | 5 | + | | 6 | +// Happy flow: arbitrary 1xx status with custom headers, observed by the | | 7 | +// client's 'information' event. | | 8 | +{ | | 9 | + const server = http.createServer(common.mustCall((req, res) => { | | 10 | + res.writeInformation(110, { 'X-Progress': '50%', 'X-Stage': 'reading' }); | | 11 | + res.writeInformation(199, [['X-Custom', 'one'], ['X-Custom-2', 'two']]); | | 12 | + res.end('done'); | | 13 | + })); | | 14 | + | | 15 | + server.listen(0, common.mustCall(() => { | | 16 | + const req = http.request({ port: server.address().port }); | | 17 | + | | 18 | + const seen = []; | | 19 | + req.on('information', (res) => { | | 20 | + seen.push({ | | 21 | + statusCode: res.statusCode, | | 22 | + headers: res.headers, | | 23 | + }); | | 24 | + }); | | 25 | + | | 26 | + req.on('response', common.mustCall((res) => { | | 27 | + assert.strictEqual(res.statusCode, 200); | | 28 | + | | 29 | + assert.strictEqual(seen.length, 2); | | 30 | + assert.strictEqual(seen[0].statusCode, 110); | | 31 | + assert.strictEqual(seen[0].headers['x-progress'], '50%'); | | 32 | + assert.strictEqual(seen[0].headers['x-stage'], 'reading'); | | 33 | + assert.strictEqual(seen[1].statusCode, 199); | | 34 | + assert.strictEqual(seen[1].headers['x-custom'], 'one'); | | 35 | + assert.strictEqual(seen[1].headers['x-custom-2'], 'two'); | | 36 | + | | 37 | + res.resume(); | | 38 | + res.on('end', common.mustCall(() => server.close())); | | 39 | + })); | | 40 | + | | 41 | + req.end(); | | 42 | + })); | | 43 | +} | | 44 | + | | 45 | +// Headers argument is optional / nullable. | | 46 | +{ | | 47 | + const server = http.createServer(common.mustCall((req, res) => { | | 48 | + res.writeInformation(150); | | 49 | + res.writeInformation(151, null); | | 50 | + res.end(); | | 51 | + })); | | 52 | + | | 53 | + server.listen(0, common.mustCall(() => { | | 54 | + const req = http.request({ port: server.address().port }); | | 55 | + let count = 0; | | 56 | + req.on('information', () => count++); | | 57 | + req.on('response', common.mustCall((res) => { | | 58 | + assert.strictEqual(count, 2); | | 59 | + res.resume(); | | 60 | + res.on('end', common.mustCall(() => server.close())); | | 61 | + })); | | 62 | + req.end(); | | 63 | + })); | | 64 | +} | | 65 | + | | 66 | +// Error cases. | | 67 | +{ | | 68 | + const server = http.createServer(common.mustCall((req, res) => { | | 69 | + assert.throws(() => res.writeInformation(101), | | 70 | + { code: 'ERR_HTTP_INVALID_STATUS_CODE' }); | | 71 | + assert.throws(() => res.writeInformation(99), | | 72 | + { code: 'ERR_OUT_OF_RANGE' }); | | 73 | + assert.throws(() => res.writeInformation(200), | | 74 | + { code: 'ERR_OUT_OF_RANGE' }); | | 75 | + assert.throws(() => res.writeInformation('100'), | | 76 | + { code: 'ERR_INVALID_ARG_TYPE' }); | | 77 | + assert.throws(() => res.writeInformation(150, { 'X-Bad\n': 'v' }), | | 78 | + { code: 'ERR_INVALID_HTTP_TOKEN' }); | | 79 | + | | 80 | + res.writeHead(200); | | 81 | + assert.throws(() => res.writeInformation(150), | | 82 | + { code: 'ERR_HTTP_HEADERS_SENT' }); | | 83 | + res.end(); | | 84 | + })); | | 85 | + | | 86 | + server.listen(0, common.mustCall(() => { | | 87 | + const req = http.request({ port: server.address().port }); | | 88 | + req.on('response', common.mustCall((res) => { | | 89 | + res.resume(); | | 90 | + res.on('end', common.mustCall(() => server.close())); | | 91 | + })); | | 92 | + req.end(); | | 93 | + })); | | 94 | +} |
|