Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http,http2: make early hints generic #44820

Merged
merged 7 commits into from Oct 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
21 changes: 15 additions & 6 deletions doc/api/http.md
Expand Up @@ -2137,32 +2137,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
anonrig marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Up @@ -81,7 +81,8 @@ const {
const {
validateInteger,
validateBoolean,
validateLinkHeaderValue
validateLinkHeaderValue,
validateObject
} = require('internal/validators');
const Buffer = require('buffer').Buffer;
const { setInterval, clearInterval } = require('timers');
Expand Down Expand Up @@ -296,36 +297,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
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
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)?$/;
anonrig marked this conversation as resolved.
Show resolved Hide resolved

/**
* @param {any} value
* @param {string} name
*/
function validateLinkHeaderFormat(value, name) {
if (
typeof value === 'undefined' ||
!RegExpPrototypeExec(linkValueRegExp, value)
Expand All @@ -424,6 +428,42 @@ const validateInternalField = hideStackFrames((object, fieldKey, className) => {
}
});

/**
* @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
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);
});
}));