Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4c869c8

Browse filesBrowse files
wingleungmcollinaLiviaMedeiros
authored andcommitted
http: add writeEarlyHints function to ServerResponse
Co-Authored-By: Matteo Collina <matteo.collina@gmail.com> Co-Authored-By: Livia Medeiros <livia@cirno.name> PR-URL: #44180 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: LiviaMedeiros <livia@cirno.name>
1 parent 3abb607 commit 4c869c8
Copy full SHA for 4c869c8
Expand file treeCollapse file tree

11 files changed

+576
-2
lines changed
Open diff view settings
Collapse file

‎doc/api/http.md‎

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 numberDiff line numberDiff line change
@@ -2120,10 +2120,41 @@ buffer. Returns `false` if all or part of the data was queued in user memory.
21202120
added: v0.3.0
21212121
-->
21222122

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
21242124
the request body should be sent. See the [`'checkContinue'`][] event on
21252125
`Server`.
21262126

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+
21272158
### `response.writeHead(statusCode[, statusMessage][, headers])`
21282159

21292160
<!-- YAML
Collapse file

‎doc/api/http2.md‎

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 numberDiff line numberDiff line change
@@ -3991,6 +3991,32 @@ Sends a status `100 Continue` to the client, indicating that the request body
39913991
should be sent. See the [`'checkContinue'`][] event on `Http2Server` and
39923992
`Http2SecureServer`.
39933993

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+
39944020
#### `response.writeHead(statusCode[, statusMessage][, headers])`
39954021

39964022
<!-- YAML
Collapse file

‎lib/_http_server.js‎

Copy file name to clipboardExpand all lines: lib/_http_server.js
+39-1Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ const {
8080
} = codes;
8181
const {
8282
validateInteger,
83-
validateBoolean
83+
validateBoolean,
84+
validateLinkHeaderValue
8485
} = require('internal/validators');
8586
const Buffer = require('buffer').Buffer;
8687
const {
@@ -300,6 +301,43 @@ ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
300301
this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb);
301302
};
302303

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+
303341
ServerResponse.prototype._implicitHeader = function _implicitHeader() {
304342
this.writeHead(this.statusCode);
305343
};
Collapse file

‎lib/internal/http2/compat.js‎

Copy file name to clipboardExpand all lines: lib/internal/http2/compat.js
+45Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ const {
3232
HTTP2_HEADER_STATUS,
3333

3434
HTTP_STATUS_CONTINUE,
35+
HTTP_STATUS_EARLY_HINTS,
3536
HTTP_STATUS_EXPECTATION_FAILED,
3637
HTTP_STATUS_METHOD_NOT_ALLOWED,
3738
HTTP_STATUS_OK
@@ -55,6 +56,7 @@ const {
5556
const {
5657
validateFunction,
5758
validateString,
59+
validateLinkHeaderValue,
5860
} = require('internal/validators');
5961
const {
6062
kSocket,
@@ -844,6 +846,49 @@ class Http2ServerResponse extends Stream {
844846
});
845847
return true;
846848
}
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+
}
847892
}
848893

849894
function onServerStream(ServerRequest, ServerResponse,
Collapse file

‎lib/internal/validators.js‎

Copy file name to clipboardExpand all lines: lib/internal/validators.js
+16Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,21 @@ function validateUnion(value, name, union) {
403403
}
404404
}
405405

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+
406421
module.exports = {
407422
isInt32,
408423
isUint32,
@@ -425,4 +440,5 @@ module.exports = {
425440
validateUndefined,
426441
validateUnion,
427442
validateAbortSignal,
443+
validateLinkHeaderValue
428444
};
Collapse file
+33Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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
+33Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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+
}));

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.