Skip to content

Commit 9c7e664

Browse files
anonrigdanielleadams
authored andcommitted
http2: make early hints generic
PR-URL: #44820 Fixes: #44816 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Minwoo Jung <nodecorelab@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
1 parent 3627616 commit 9c7e664

11 files changed

+318
-163
lines changed

doc/api/http.md

+15-6
Original file line numberDiff line numberDiff line change
@@ -2124,32 +2124,41 @@ 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])`
2127+
### `response.writeEarlyHints(hints[, callback])`
21282128

21292129
<!-- YAML
21302130
added: REPLACEME
2131+
changes:
2132+
- version: REPLACEME
2133+
pr-url: https://github.com/nodejs/node/pull/44820
2134+
description: Allow passing hints as an object.
21312135
-->
21322136

2133-
* `links` {string|Array}
2137+
* `hints` {Object}
21342138
* `callback` {Function}
21352139

21362140
Sends an HTTP/1.1 103 Early Hints message to the client with a Link header,
21372141
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
2142+
The `hints` is an object containing the values of headers to be sent with
2143+
early hints message. The optional `callback` argument will be called when
21402144
the response message has been written.
21412145

21422146
**Example**
21432147

21442148
```js
21452149
const earlyHintsLink = '</styles.css>; rel=preload; as=style';
2146-
response.writeEarlyHints(earlyHintsLink);
2150+
response.writeEarlyHints({
2151+
'link': earlyHintsLink,
2152+
});
21472153

21482154
const earlyHintsLinks = [
21492155
'</styles.css>; rel=preload; as=style',
21502156
'</scripts.js>; rel=preload; as=script',
21512157
];
2152-
response.writeEarlyHints(earlyHintsLinks);
2158+
response.writeEarlyHints({
2159+
'link': earlyHintsLinks,
2160+
'x-trace-id': 'id for diagnostics'
2161+
});
21532162

21542163
const earlyHintsCallback = () => console.log('early hints message sent');
21552164
response.writeEarlyHints(earlyHintsLinks, earlyHintsCallback);

lib/_http_server.js

+17-25
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,8 @@ const {
8181
const {
8282
validateInteger,
8383
validateBoolean,
84-
validateLinkHeaderValue
84+
validateLinkHeaderValue,
85+
validateObject
8586
} = require('internal/validators');
8687
const Buffer = require('buffer').Buffer;
8788
const {
@@ -301,36 +302,27 @@ ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
301302
this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb);
302303
};
303304

304-
ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(links, cb) {
305+
ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) {
305306
let head = 'HTTP/1.1 103 Early Hints\r\n';
306307

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-
}
308+
validateObject(hints, 'hints');
314309

315-
head += 'Link: ';
310+
if (hints.link === null || hints.link === undefined) {
311+
return;
312+
}
316313

317-
for (let i = 0; i < links.length; i++) {
318-
const link = links[i];
319-
validateLinkHeaderValue(link, 'links');
320-
head += link;
314+
const link = validateLinkHeaderValue(hints.link);
321315

322-
if (i !== links.length - 1) {
323-
head += ', ';
324-
}
325-
}
316+
if (link.length === 0) {
317+
return;
318+
}
326319

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-
);
320+
head += 'Link: ' + link + '\r\n';
321+
322+
for (const key of ObjectKeys(hints)) {
323+
if (key !== 'link') {
324+
head += key + ': ' + hints[key] + '\r\n';
325+
}
334326
}
335327

336328
head += '\r\n';

lib/internal/http2/compat.js

+14-25
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const {
5757
validateFunction,
5858
validateString,
5959
validateLinkHeaderValue,
60+
validateObject,
6061
} = require('internal/validators');
6162
const {
6263
kSocket,
@@ -847,34 +848,21 @@ class Http2ServerResponse extends Stream {
847848
return true;
848849
}
849850

850-
writeEarlyHints(links) {
851-
let linkHeaderValue = '';
851+
writeEarlyHints(hints) {
852+
validateObject(hints, 'hints');
852853

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 += '';
854+
const headers = ObjectCreate(null);
862855

863-
for (let i = 0; i < links.length; i++) {
864-
const link = links[i];
865-
validateLinkHeaderValue(link, 'links');
866-
linkHeaderValue += link;
856+
const linkHeaderValue = validateLinkHeaderValue(hints.link);
867857

868-
if (i !== links.length - 1) {
869-
linkHeaderValue += ', ';
870-
}
858+
for (const key of ObjectKeys(hints)) {
859+
if (key !== 'link') {
860+
headers[key] = hints[key];
871861
}
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-
);
862+
}
863+
864+
if (linkHeaderValue.length === 0) {
865+
return false;
878866
}
879867

880868
const stream = this[kStream];
@@ -883,8 +871,9 @@ class Http2ServerResponse extends Stream {
883871
return false;
884872

885873
stream.additionalHeaders({
874+
...headers,
886875
[HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS,
887-
'Link': linkHeaderValue
876+
'Link': linkHeaderValue,
888877
});
889878

890879
return true;

lib/internal/validators.js

+42-2
Original file line numberDiff line numberDiff line change
@@ -403,9 +403,13 @@ function validateUnion(value, name, union) {
403403
}
404404
}
405405

406-
function validateLinkHeaderValue(value, name) {
407-
const linkValueRegExp = /^(?:<[^>]*>;)\s*(?:rel=(")?[^;"]*\1;?)\s*(?:(?:as|anchor|title)=(")?[^;"]*\2)?$/;
406+
const linkValueRegExp = /^(?:<[^>]*>;)\s*(?:rel=(")?[^;"]*\1;?)\s*(?:(?:as|anchor|title)=(")?[^;"]*\2)?$/;
408407

408+
/**
409+
* @param {any} value
410+
* @param {string} name
411+
*/
412+
function validateLinkHeaderFormat(value, name) {
409413
if (
410414
typeof value === 'undefined' ||
411415
!RegExpPrototypeExec(linkValueRegExp, value)
@@ -418,6 +422,42 @@ function validateLinkHeaderValue(value, name) {
418422
}
419423
}
420424

425+
/**
426+
* @param {any} hints
427+
* @return {string}
428+
*/
429+
function validateLinkHeaderValue(hints) {
430+
if (typeof hints === 'string') {
431+
validateLinkHeaderFormat(hints, 'hints');
432+
return hints;
433+
} else if (ArrayIsArray(hints)) {
434+
const hintsLength = hints.length;
435+
let result = '';
436+
437+
if (hintsLength === 0) {
438+
return result;
439+
}
440+
441+
for (let i = 0; i < hintsLength; i++) {
442+
const link = hints[i];
443+
validateLinkHeaderFormat(link, 'hints');
444+
result += link;
445+
446+
if (i !== hintsLength - 1) {
447+
result += ', ';
448+
}
449+
}
450+
451+
return result;
452+
}
453+
454+
throw new ERR_INVALID_ARG_VALUE(
455+
'hints',
456+
hints,
457+
'must be an array or string of format "</styles.css>; rel=preload; as=style"'
458+
);
459+
}
460+
421461
module.exports = {
422462
isInt32,
423463
isUint32,

test/parallel/test-http-early-hints-invalid-argument-type.js

-33
This file was deleted.

test/parallel/test-http-early-hints-invalid-argument.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const testResBody = 'response content\n';
88

99
const server = http.createServer(common.mustCall((req, res) => {
1010
debug('Server sending early hints...');
11-
res.writeEarlyHints('bad argument value');
11+
res.writeEarlyHints('bad argument type');
1212

1313
debug('Server sending full response...');
1414
res.end(testResBody);
@@ -27,7 +27,7 @@ server.listen(0, common.mustCall(() => {
2727
process.on('uncaughtException', (err) => {
2828
debug(`Caught an exception: ${JSON.stringify(err)}`);
2929
if (err.name === 'AssertionError') throw err;
30-
assert.strictEqual(err.code, 'ERR_INVALID_ARG_VALUE');
30+
assert.strictEqual(err.code, 'ERR_INVALID_ARG_TYPE');
3131
process.exit(0);
3232
});
3333
}));

0 commit comments

Comments
 (0)