Skip to content

Commit

Permalink
http2: make early hints generic
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
anonrig authored and danielleadams committed Oct 11, 2022
1 parent 3627616 commit 9c7e664
Show file tree
Hide file tree
Showing 11 changed files with 318 additions and 163 deletions.
21 changes: 15 additions & 6 deletions doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -2124,32 +2124,41 @@ Sends an HTTP/1.1 100 Continue message to the client, indicating that
the request body should be sent. See the [`'checkContinue'`][] event on
`Server`.

### `response.writeEarlyHints(links[, callback])`
### `response.writeEarlyHints(hints[, callback])`

<!-- YAML
added: REPLACEME
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/44820
description: Allow passing hints as an object.
-->

* `links` {string|Array}
* `hints` {Object}
* `callback` {Function}

Sends an HTTP/1.1 103 Early Hints message to the client with a Link header,
indicating that the user agent can preload/preconnect the linked resources.
The `links` can be a string or an array of strings containing the values
of the `Link` header. The optional `callback` argument will be called when
The `hints` is an object containing the values of headers to be sent with
early hints message. The optional `callback` argument will be called when
the response message has been written.

**Example**

```js
const earlyHintsLink = '</styles.css>; rel=preload; as=style';
response.writeEarlyHints(earlyHintsLink);
response.writeEarlyHints({
'link': earlyHintsLink,
});

const earlyHintsLinks = [
'</styles.css>; rel=preload; as=style',
'</scripts.js>; rel=preload; as=script',
];
response.writeEarlyHints(earlyHintsLinks);
response.writeEarlyHints({
'link': earlyHintsLinks,
'x-trace-id': 'id for diagnostics'
});

const earlyHintsCallback = () => console.log('early hints message sent');
response.writeEarlyHints(earlyHintsLinks, earlyHintsCallback);
Expand Down
42 changes: 17 additions & 25 deletions lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,8 @@ const {
const {
validateInteger,
validateBoolean,
validateLinkHeaderValue
validateLinkHeaderValue,
validateObject
} = require('internal/validators');
const Buffer = require('buffer').Buffer;
const {
Expand Down Expand Up @@ -301,36 +302,27 @@ ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb);
};

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

if (typeof links === 'string') {
validateLinkHeaderValue(links, 'links');
head += 'Link: ' + links + '\r\n';
} else if (ArrayIsArray(links)) {
if (!links.length) {
return;
}
validateObject(hints, 'hints');

head += 'Link: ';
if (hints.link === null || hints.link === undefined) {
return;
}

for (let i = 0; i < links.length; i++) {
const link = links[i];
validateLinkHeaderValue(link, 'links');
head += link;
const link = validateLinkHeaderValue(hints.link);

if (i !== links.length - 1) {
head += ', ';
}
}
if (link.length === 0) {
return;
}

head += '\r\n';
} else {
throw new ERR_INVALID_ARG_VALUE(
'links',
links,
'must be an array or string of format "</styles.css>; rel=preload; as=style"'
);
head += 'Link: ' + link + '\r\n';

for (const key of ObjectKeys(hints)) {
if (key !== 'link') {
head += key + ': ' + hints[key] + '\r\n';
}
}

head += '\r\n';
Expand Down
39 changes: 14 additions & 25 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const {
validateFunction,
validateString,
validateLinkHeaderValue,
validateObject,
} = require('internal/validators');
const {
kSocket,
Expand Down Expand Up @@ -847,34 +848,21 @@ class Http2ServerResponse extends Stream {
return true;
}

writeEarlyHints(links) {
let linkHeaderValue = '';
writeEarlyHints(hints) {
validateObject(hints, 'hints');

if (typeof links === 'string') {
validateLinkHeaderValue(links, 'links');
linkHeaderValue += links;
} else if (ArrayIsArray(links)) {
if (!links.length) {
return;
}

linkHeaderValue += '';
const headers = ObjectCreate(null);

for (let i = 0; i < links.length; i++) {
const link = links[i];
validateLinkHeaderValue(link, 'links');
linkHeaderValue += link;
const linkHeaderValue = validateLinkHeaderValue(hints.link);

if (i !== links.length - 1) {
linkHeaderValue += ', ';
}
for (const key of ObjectKeys(hints)) {
if (key !== 'link') {
headers[key] = hints[key];
}
} else {
throw new ERR_INVALID_ARG_VALUE(
'links',
links,
'must be an array or string of format "</styles.css>; rel=preload; as=style"'
);
}

if (linkHeaderValue.length === 0) {
return false;
}

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

stream.additionalHeaders({
...headers,
[HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS,
'Link': linkHeaderValue
'Link': linkHeaderValue,
});

return true;
Expand Down
44 changes: 42 additions & 2 deletions lib/internal/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,13 @@ function validateUnion(value, name, union) {
}
}

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

/**
* @param {any} value
* @param {string} name
*/
function validateLinkHeaderFormat(value, name) {
if (
typeof value === 'undefined' ||
!RegExpPrototypeExec(linkValueRegExp, value)
Expand All @@ -418,6 +422,42 @@ function validateLinkHeaderValue(value, name) {
}
}

/**
* @param {any} hints
* @return {string}
*/
function validateLinkHeaderValue(hints) {
if (typeof hints === 'string') {
validateLinkHeaderFormat(hints, 'hints');
return hints;
} else if (ArrayIsArray(hints)) {
const hintsLength = hints.length;
let result = '';

if (hintsLength === 0) {
return result;
}

for (let i = 0; i < hintsLength; i++) {
const link = hints[i];
validateLinkHeaderFormat(link, 'hints');
result += link;

if (i !== hintsLength - 1) {
result += ', ';
}
}

return result;
}

throw new ERR_INVALID_ARG_VALUE(
'hints',
hints,
'must be an array or string of format "</styles.css>; rel=preload; as=style"'
);
}

module.exports = {
isInt32,
isUint32,
Expand Down
33 changes: 0 additions & 33 deletions test/parallel/test-http-early-hints-invalid-argument-type.js

This file was deleted.

4 changes: 2 additions & 2 deletions test/parallel/test-http-early-hints-invalid-argument.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const testResBody = 'response content\n';

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

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

0 comments on commit 9c7e664

Please sign in to comment.