GeezerVison is showing github.com — http: add writeEarlyHints function to ServerResponse · nodejs/node@4c869c8
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 +32-1Lines changed: 32 additions & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -2120,10 +2120,41 @@ buffer. Returns `false` if all or part of the data was queued in user memory. | 2120 | 2120 | added: v0.3.0 | 2121 | 2121 | --> | 2122 | 2122 |
| 2123 | | -Sends a HTTP/1.1 100 Continue message to the client, indicating that | | 2123 | +Sends an HTTP/1.1 100 Continue message to the client, indicating that | 2124 | 2124 | the request body should be sent. See the [`'checkContinue'`][] event on | 2125 | 2125 | `Server`. | 2126 | 2126 |
| | 2127 | +### `response.writeEarlyHints(links[, callback])` | | 2128 | + | | 2129 | +<!-- YAML | | 2130 | +added: REPLACEME | | 2131 | +--> | | 2132 | + | | 2133 | +* `links` {string|Array} | | 2134 | +* `callback` {Function} | | 2135 | + | | 2136 | +Sends an HTTP/1.1 103 Early Hints message to the client with a Link header, | | 2137 | +indicating that the user agent can preload/preconnect the linked resources. | | 2138 | +The `links` can be a string or an array of strings containing the values | | 2139 | +of the `Link` header. The optional `callback` argument will be called when | | 2140 | +the response message has been written. | | 2141 | + | | 2142 | +**Example** | | 2143 | + | | 2144 | +```js | | 2145 | +const earlyHintsLink = '</styles.css>; rel=preload; as=style'; | | 2146 | +response.writeEarlyHints(earlyHintsLink); | | 2147 | + | | 2148 | +const earlyHintsLinks = [ | | 2149 | + '</styles.css>; rel=preload; as=style', | | 2150 | + '</scripts.js>; rel=preload; as=script', | | 2151 | +]; | | 2152 | +response.writeEarlyHints(earlyHintsLinks); | | 2153 | + | | 2154 | +const earlyHintsCallback = () => console.log('early hints message sent'); | | 2155 | +response.writeEarlyHints(earlyHintsLinks, earlyHintsCallback); | | 2156 | +``` | | 2157 | + | 2127 | 2158 | ### `response.writeHead(statusCode[, statusMessage][, headers])` | 2128 | 2159 |
| 2129 | 2160 | <!-- YAML | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/http2.md +26Lines changed: 26 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -3991,6 +3991,32 @@ Sends a status `100 Continue` to the client, indicating that the request body | 3991 | 3991 | should be sent. See the [`'checkContinue'`][] event on `Http2Server` and | 3992 | 3992 | `Http2SecureServer`. | 3993 | 3993 |
| | 3994 | +### `response.writeEarlyHints(links)` | | 3995 | + | | 3996 | +<!-- YAML | | 3997 | +added: REPLACEME | | 3998 | +--> | | 3999 | + | | 4000 | +* `links` {string|Array} | | 4001 | + | | 4002 | +Sends a status `103 Early Hints` to the client with a Link header, | | 4003 | +indicating that the user agent can preload/preconnect the linked resources. | | 4004 | +The `links` can be a string or an array of strings containing the values | | 4005 | +of the `Link` header. | | 4006 | + | | 4007 | +**Example** | | 4008 | + | | 4009 | +```js | | 4010 | +const earlyHintsLink = '</styles.css>; rel=preload; as=style'; | | 4011 | +response.writeEarlyHints(earlyHintsLink); | | 4012 | + | | 4013 | +const earlyHintsLinks = [ | | 4014 | + '</styles.css>; rel=preload; as=style', | | 4015 | + '</scripts.js>; rel=preload; as=script', | | 4016 | +]; | | 4017 | +response.writeEarlyHints(earlyHintsLinks); | | 4018 | +``` | | 4019 | + | 3994 | 4020 | #### `response.writeHead(statusCode[, statusMessage][, headers])` | 3995 | 4021 |
| 3996 | 4022 | <!-- YAML | |
Collapse file Copy file name to clipboardExpand all lines: lib/_http_server.js +39-1Lines changed: 39 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -80,7 +80,8 @@ const { | 80 | 80 | } = codes; | 81 | 81 | const { | 82 | 82 | validateInteger, | 83 | | - validateBoolean | | 83 | + validateBoolean, | | 84 | + validateLinkHeaderValue | 84 | 85 | } = require('internal/validators'); | 85 | 86 | const Buffer = require('buffer').Buffer; | 86 | 87 | const { | @@ -300,6 +301,43 @@ ServerResponse.prototype.writeProcessing = function writeProcessing(cb) { | 300 | 301 | this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb); | 301 | 302 | }; | 302 | 303 |
| | 304 | +ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(links, cb) { | | 305 | + let head = 'HTTP/1.1 103 Early Hints\r\n'; | | 306 | + | | 307 | + if (typeof links === 'string') { | | 308 | + validateLinkHeaderValue(links, 'links'); | | 309 | + head += 'Link: ' + links + '\r\n'; | | 310 | + } else if (ArrayIsArray(links)) { | | 311 | + if (!links.length) { | | 312 | + return; | | 313 | + } | | 314 | + | | 315 | + head += 'Link: '; | | 316 | + | | 317 | + for (let i = 0; i < links.length; i++) { | | 318 | + const link = links[i]; | | 319 | + validateLinkHeaderValue(link, 'links'); | | 320 | + head += link; | | 321 | + | | 322 | + if (i !== links.length - 1) { | | 323 | + head += ', '; | | 324 | + } | | 325 | + } | | 326 | + | | 327 | + head += '\r\n'; | | 328 | + } else { | | 329 | + throw new ERR_INVALID_ARG_VALUE( | | 330 | + 'links', | | 331 | + links, | | 332 | + 'must be an array or string of format "</styles.css>; rel=preload; as=style"' | | 333 | + ); | | 334 | + } | | 335 | + | | 336 | + head += '\r\n'; | | 337 | + | | 338 | + this._writeRaw(head, 'ascii', cb); | | 339 | +}; | | 340 | + | 303 | 341 | ServerResponse.prototype._implicitHeader = function _implicitHeader() { | 304 | 342 | this.writeHead(this.statusCode); | 305 | 343 | }; | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/http2/compat.js +45Lines changed: 45 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -32,6 +32,7 @@ const { | 32 | 32 | HTTP2_HEADER_STATUS, | 33 | 33 |
| 34 | 34 | HTTP_STATUS_CONTINUE, | | 35 | + HTTP_STATUS_EARLY_HINTS, | 35 | 36 | HTTP_STATUS_EXPECTATION_FAILED, | 36 | 37 | HTTP_STATUS_METHOD_NOT_ALLOWED, | 37 | 38 | HTTP_STATUS_OK | @@ -55,6 +56,7 @@ const { | 55 | 56 | const { | 56 | 57 | validateFunction, | 57 | 58 | validateString, | | 59 | + validateLinkHeaderValue, | 58 | 60 | } = require('internal/validators'); | 59 | 61 | const { | 60 | 62 | kSocket, | @@ -844,6 +846,49 @@ class Http2ServerResponse extends Stream { | 844 | 846 | }); | 845 | 847 | return true; | 846 | 848 | } | | 849 | + | | 850 | + writeEarlyHints(links) { | | 851 | + let linkHeaderValue = ''; | | 852 | + | | 853 | + if (typeof links === 'string') { | | 854 | + validateLinkHeaderValue(links, 'links'); | | 855 | + linkHeaderValue += links; | | 856 | + } else if (ArrayIsArray(links)) { | | 857 | + if (!links.length) { | | 858 | + return; | | 859 | + } | | 860 | + | | 861 | + linkHeaderValue += ''; | | 862 | + | | 863 | + for (let i = 0; i < links.length; i++) { | | 864 | + const link = links[i]; | | 865 | + validateLinkHeaderValue(link, 'links'); | | 866 | + linkHeaderValue += link; | | 867 | + | | 868 | + if (i !== links.length - 1) { | | 869 | + linkHeaderValue += ', '; | | 870 | + } | | 871 | + } | | 872 | + } else { | | 873 | + throw new ERR_INVALID_ARG_VALUE( | | 874 | + 'links', | | 875 | + links, | | 876 | + 'must be an array or string of format "</styles.css>; rel=preload; as=style"' | | 877 | + ); | | 878 | + } | | 879 | + | | 880 | + const stream = this[kStream]; | | 881 | + | | 882 | + if (stream.headersSent || this[kState].closed) | | 883 | + return false; | | 884 | + | | 885 | + stream.additionalHeaders({ | | 886 | + [HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS, | | 887 | + 'Link': linkHeaderValue | | 888 | + }); | | 889 | + | | 890 | + return true; | | 891 | + } | 847 | 892 | } | 848 | 893 |
| 849 | 894 | function onServerStream(ServerRequest, ServerResponse, | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/validators.js +16Lines changed: 16 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -403,6 +403,21 @@ function validateUnion(value, name, union) { | 403 | 403 | } | 404 | 404 | } | 405 | 405 |
| | 406 | +function validateLinkHeaderValue(value, name) { | | 407 | + const linkValueRegExp = /^(?:<[^>]*>;)\s*(?:rel=(")?[^;"]*\1;?)\s*(?:(?:as|anchor|title)=(")?[^;"]*\2)?$/; | | 408 | + | | 409 | + if ( | | 410 | + typeof value === 'undefined' || | | 411 | + !RegExpPrototypeExec(linkValueRegExp, value) | | 412 | + ) { | | 413 | + throw new ERR_INVALID_ARG_VALUE( | | 414 | + name, | | 415 | + value, | | 416 | + 'must be an array or string of format "</styles.css>; rel=preload; as=style"' | | 417 | + ); | | 418 | + } | | 419 | +} | | 420 | + | 406 | 421 | module.exports = { | 407 | 422 | isInt32, | 408 | 423 | isUint32, | @@ -425,4 +440,5 @@ module.exports = { | 425 | 440 | validateUndefined, | 426 | 441 | validateUnion, | 427 | 442 | validateAbortSignal, | | 443 | + validateLinkHeaderValue | 428 | 444 | }; |
Collapse file Copy file name to clipboard +33Lines changed: 33 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 | +const debug = require('node:util').debuglog('test'); | | 6 | + | | 7 | +const testResBody = 'response content\n'; | | 8 | + | | 9 | +const server = http.createServer(common.mustCall((req, res) => { | | 10 | + debug('Server sending early hints...'); | | 11 | + res.writeEarlyHints({ links: 'bad argument object' }); | | 12 | + | | 13 | + debug('Server sending full response...'); | | 14 | + res.end(testResBody); | | 15 | +})); | | 16 | + | | 17 | +server.listen(0, common.mustCall(() => { | | 18 | + const req = http.request({ | | 19 | + port: server.address().port, path: '/' | | 20 | + }); | | 21 | + | | 22 | + req.end(); | | 23 | + debug('Client sending request...'); | | 24 | + | | 25 | + req.on('information', common.mustNotCall()); | | 26 | + | | 27 | + process.on('uncaughtException', (err) => { | | 28 | + debug(`Caught an exception: ${JSON.stringify(err)}`); | | 29 | + if (err.name === 'AssertionError') throw err; | | 30 | + assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE'); | | 31 | + process.exit(0); | | 32 | + }); | | 33 | +})); |
Collapse file Copy file name to clipboard +33Lines changed: 33 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 | +const debug = require('node:util').debuglog('test'); | | 6 | + | | 7 | +const testResBody = 'response content\n'; | | 8 | + | | 9 | +const server = http.createServer(common.mustCall((req, res) => { | | 10 | + debug('Server sending early hints...'); | | 11 | + res.writeEarlyHints('bad argument value'); | | 12 | + | | 13 | + debug('Server sending full response...'); | | 14 | + res.end(testResBody); | | 15 | +})); | | 16 | + | | 17 | +server.listen(0, common.mustCall(() => { | | 18 | + const req = http.request({ | | 19 | + port: server.address().port, path: '/' | | 20 | + }); | | 21 | + | | 22 | + req.end(); | | 23 | + debug('Client sending request...'); | | 24 | + | | 25 | + req.on('information', common.mustNotCall()); | | 26 | + | | 27 | + process.on('uncaughtException', (err) => { | | 28 | + debug(`Caught an exception: ${JSON.stringify(err)}`); | | 29 | + if (err.name === 'AssertionError') throw err; | | 30 | + assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE'); | | 31 | + process.exit(0); | | 32 | + }); | | 33 | +})); |
|