Skip to content

Commit 794324d

Browse files
wingleungmcollinaLiviaMedeiros
authored andcommitted
http: add writeEarlyHints function to ServerResponse
Co-Authored-By: Matteo Collina <[email protected]> Co-Authored-By: Livia Medeiros <[email protected]> PR-URL: nodejs#44180 Reviewed-By: Robert Nagy <[email protected]> Reviewed-By: Paolo Insogna <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: LiviaMedeiros <[email protected]>
1 parent 199d9a6 commit 794324d

11 files changed

+576
-2
lines changed

doc/api/http.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2129,10 +2129,41 @@ buffer. Returns `false` if all or part of the data was queued in user memory.
21292129
added: v0.3.0
21302130
-->
21312131

2132-
Sends a HTTP/1.1 100 Continue message to the client, indicating that
2132+
Sends an HTTP/1.1 100 Continue message to the client, indicating that
21332133
the request body should be sent. See the [`'checkContinue'`][] event on
21342134
`Server`.
21352135

2136+
### `response.writeEarlyHints(links[, callback])`
2137+
2138+
<!-- YAML
2139+
added: REPLACEME
2140+
-->
2141+
2142+
* `links` {string|Array}
2143+
* `callback` {Function}
2144+
2145+
Sends an HTTP/1.1 103 Early Hints message to the client with a Link header,
2146+
indicating that the user agent can preload/preconnect the linked resources.
2147+
The `links` can be a string or an array of strings containing the values
2148+
of the `Link` header. The optional `callback` argument will be called when
2149+
the response message has been written.
2150+
2151+
**Example**
2152+
2153+
```js
2154+
const earlyHintsLink = '</styles.css>; rel=preload; as=style';
2155+
response.writeEarlyHints(earlyHintsLink);
2156+
2157+
const earlyHintsLinks = [
2158+
'</styles.css>; rel=preload; as=style',
2159+
'</scripts.js>; rel=preload; as=script',
2160+
];
2161+
response.writeEarlyHints(earlyHintsLinks);
2162+
2163+
const earlyHintsCallback = () => console.log('early hints message sent');
2164+
response.writeEarlyHints(earlyHintsLinks, earlyHintsCallback);
2165+
```
2166+
21362167
### `response.writeHead(statusCode[, statusMessage][, headers])`
21372168

21382169
<!-- YAML

doc/api/http2.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4005,6 +4005,32 @@ Sends a status `100 Continue` to the client, indicating that the request body
40054005
should be sent. See the [`'checkContinue'`][] event on `Http2Server` and
40064006
`Http2SecureServer`.
40074007

4008+
### `response.writeEarlyHints(links)`
4009+
4010+
<!-- YAML
4011+
added: REPLACEME
4012+
-->
4013+
4014+
* `links` {string|Array}
4015+
4016+
Sends a status `103 Early Hints` to the client with a Link header,
4017+
indicating that the user agent can preload/preconnect the linked resources.
4018+
The `links` can be a string or an array of strings containing the values
4019+
of the `Link` header.
4020+
4021+
**Example**
4022+
4023+
```js
4024+
const earlyHintsLink = '</styles.css>; rel=preload; as=style';
4025+
response.writeEarlyHints(earlyHintsLink);
4026+
4027+
const earlyHintsLinks = [
4028+
'</styles.css>; rel=preload; as=style',
4029+
'</scripts.js>; rel=preload; as=script',
4030+
];
4031+
response.writeEarlyHints(earlyHintsLinks);
4032+
```
4033+
40084034
#### `response.writeHead(statusCode[, statusMessage][, headers])`
40094035

40104036
<!-- YAML

lib/_http_server.js

Lines 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 { setInterval, clearInterval } = require('timers');
@@ -295,6 +296,43 @@ ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
295296
this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb);
296297
};
297298

299+
ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(links, cb) {
300+
let head = 'HTTP/1.1 103 Early Hints\r\n';
301+
302+
if (typeof links === 'string') {
303+
validateLinkHeaderValue(links, 'links');
304+
head += 'Link: ' + links + '\r\n';
305+
} else if (ArrayIsArray(links)) {
306+
if (!links.length) {
307+
return;
308+
}
309+
310+
head += 'Link: ';
311+
312+
for (let i = 0; i < links.length; i++) {
313+
const link = links[i];
314+
validateLinkHeaderValue(link, 'links');
315+
head += link;
316+
317+
if (i !== links.length - 1) {
318+
head += ', ';
319+
}
320+
}
321+
322+
head += '\r\n';
323+
} else {
324+
throw new ERR_INVALID_ARG_VALUE(
325+
'links',
326+
links,
327+
'must be an array or string of format "</styles.css>; rel=preload; as=style"'
328+
);
329+
}
330+
331+
head += '\r\n';
332+
333+
this._writeRaw(head, 'ascii', cb);
334+
};
335+
298336
ServerResponse.prototype._implicitHeader = function _implicitHeader() {
299337
this.writeHead(this.statusCode);
300338
};

lib/internal/http2/compat.js

Lines 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,

lib/internal/validators.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,21 @@ function validateUnion(value, name, union) {
258258
}
259259
}
260260

261+
function validateLinkHeaderValue(value, name) {
262+
const linkValueRegExp = /^(?:<[^>]*>;)\s*(?:rel=(")?[^;"]*\1;?)\s*(?:(?:as|anchor|title)=(")?[^;"]*\2)?$/;
263+
264+
if (
265+
typeof value === 'undefined' ||
266+
!RegExpPrototypeExec(linkValueRegExp, value)
267+
) {
268+
throw new ERR_INVALID_ARG_VALUE(
269+
name,
270+
value,
271+
'must be an array or string of format "</styles.css>; rel=preload; as=style"'
272+
);
273+
}
274+
}
275+
261276
module.exports = {
262277
isInt32,
263278
isUint32,
@@ -280,4 +295,5 @@ module.exports = {
280295
validateUndefined,
281296
validateUnion,
282297
validateAbortSignal,
298+
validateLinkHeaderValue
283299
};
Lines 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+
}));
Lines 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)