Skip to content

Commit 4c869c8

Browse 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

11 files changed

+576
-2
lines changed

doc/api/http.md

+32-1
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

doc/api/http2.md

+26
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

lib/_http_server.js

+39-1
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
};

lib/internal/http2/compat.js

+45
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,

lib/internal/validators.js

+16
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
};
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+
}));
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)