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: add writeEarlyHints function to ServerResponse #44180

Merged
merged 6 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion doc/api/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -2127,10 +2127,41 @@ buffer. Returns `false` if all or part of the data was queued in user memory.
added: v0.3.0
-->

Sends a HTTP/1.1 100 Continue message to the client, indicating that
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])`

wingleung marked this conversation as resolved.
Show resolved Hide resolved
<!-- YAML
added: REPLACEME
-->

* `links` {string|Array}
* `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 response message has been written.

**Example**

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

const earlyHintsLinks = [
'</styles.css>; rel=preload; as=style',
'</scripts.js>; rel=preload; as=script',
];
response.writeEarlyHints(earlyHintsLinks);

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

### `response.writeHead(statusCode[, statusMessage][, headers])`

<!-- YAML
Expand Down
40 changes: 39 additions & 1 deletion lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ const {
} = codes;
const {
validateInteger,
validateBoolean
validateBoolean,
validateLinkHeaderValue
} = require('internal/validators');
const Buffer = require('buffer').Buffer;
const { setInterval, clearInterval } = require('timers');
Expand Down Expand Up @@ -295,6 +296,43 @@ 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) {
let head = 'HTTP/1.1 103 Early Hints\r\n';

if (typeof links === 'string') {
wingleung marked this conversation as resolved.
Show resolved Hide resolved
validateLinkHeaderValue(links, 'links');
head += 'Link: ' + links + '\r\n';
} else if (ArrayIsArray(links)) {
if (!links.length) {
return;
}

head += 'Link: ';

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

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

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 += '\r\n';

this._writeRaw(head, 'ascii', cb);
};

ServerResponse.prototype._implicitHeader = function _implicitHeader() {
this.writeHead(this.statusCode);
};
Expand Down
45 changes: 45 additions & 0 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const {
HTTP2_HEADER_STATUS,

HTTP_STATUS_CONTINUE,
HTTP_STATUS_EARLY_HINTS,
HTTP_STATUS_EXPECTATION_FAILED,
HTTP_STATUS_METHOD_NOT_ALLOWED,
HTTP_STATUS_OK
Expand All @@ -55,6 +56,7 @@ const {
const {
validateFunction,
validateString,
validateLinkHeaderValue,
} = require('internal/validators');
const {
kSocket,
Expand Down Expand Up @@ -844,6 +846,49 @@ class Http2ServerResponse extends Stream {
});
return true;
}

writeEarlyHints(links) {
let linkHeaderValue = '';

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

linkHeaderValue += '';

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

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

const stream = this[kStream];

if (stream.headersSent || this[kState].closed)
return false;

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

return true;
}
}

function onServerStream(ServerRequest, ServerResponse,
Expand Down
16 changes: 16 additions & 0 deletions lib/internal/validators.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,21 @@ function validateUnion(value, name, union) {
}
}

function validateLinkHeaderValue(value, name) {
const linkValueRegExp = /^((<([^>]*)>;)\s*(rel="?([^;]*)"?;?)\s*((as|anchor|title)="?(.*)"?)?)$/;
wingleung marked this conversation as resolved.
Show resolved Hide resolved

if (
typeof value === 'undefined' ||
!RegExpPrototypeExec(linkValueRegExp, value)
) {
throw new ERR_INVALID_ARG_VALUE(
name,
value,
'must be an array or string of format "</styles.css>; rel=preload; as=style"'
);
}
}

module.exports = {
isInt32,
isUint32,
Expand All @@ -280,4 +295,5 @@ module.exports = {
validateUndefined,
validateUnion,
validateAbortSignal,
validateLinkHeaderValue
};
33 changes: 33 additions & 0 deletions test/parallel/test-http-early-hints-invalid-argument-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';
const common = require('../common');
const assert = require('node:assert');
const http = require('node:http');
const debug = require('node:util').debuglog('test');

const testResBody = 'response content\n';

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

debug('Server sending full response...');
res.end(testResBody);
}));

server.listen(0, common.mustCall(() => {
const req = http.request({
port: server.address().port, path: '/'
});

req.end();
debug('Client sending request...');

req.on('information', common.mustNotCall());

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');
process.exit(0);
});
}));
33 changes: 33 additions & 0 deletions test/parallel/test-http-early-hints-invalid-argument.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';
const common = require('../common');
const assert = require('node:assert');
const http = require('node:http');
const debug = require('node:util').debuglog('test');

const testResBody = 'response content\n';

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

debug('Server sending full response...');
res.end(testResBody);
}));

server.listen(0, common.mustCall(() => {
const req = http.request({
port: server.address().port, path: '/'
});

req.end();
debug('Client sending request...');

req.on('information', common.mustNotCall());

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');
process.exit(0);
});
}));
135 changes: 135 additions & 0 deletions test/parallel/test-http-early-hints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'use strict';
const common = require('../common');
const assert = require('node:assert');
const http = require('node:http');
const debug = require('node:util').debuglog('test');

const testResBody = 'response content\n';

{
// Happy flow - string argument

const server = http.createServer(common.mustCall((req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints('</styles.css>; rel=preload; as=style');

debug('Server sending full response...');
res.end(testResBody);
}));

server.listen(0, common.mustCall(() => {
const req = http.request({
port: server.address().port, path: '/'
});

debug('Client sending request...');

req.on('information', common.mustCall((res) => {
assert.strictEqual(res.headers.link, '</styles.css>; rel=preload; as=style');
}));

req.on('response', common.mustCall((res) => {
let body = '';

assert.strictEqual(res.statusCode, 200, `Final status code was ${res.statusCode}, not 200.`);

res.on('data', (chunk) => {
body += chunk;
});

res.on('end', common.mustCall(() => {
debug('Got full response.');
assert.strictEqual(body, testResBody);
server.close();
}));
}));

req.end();
}));
}

{
// Happy flow - array argument

const server = http.createServer(common.mustCall((req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints([
'</styles.css>; rel=preload; as=style',
'</scripts.js>; rel=preload; as=script',
]);

debug('Server sending full response...');
res.end(testResBody);
}));

server.listen(0, common.mustCall(() => {
const req = http.request({
port: server.address().port, path: '/'
});
debug('Client sending request...');

req.on('information', common.mustCall((res) => {
assert.strictEqual(
res.headers.link,
'</styles.css>; rel=preload; as=style, </scripts.js>; rel=preload; as=script'
);
}));

req.on('response', common.mustCall((res) => {
let body = '';

assert.strictEqual(res.statusCode, 200, `Final status code was ${res.statusCode}, not 200.`);

res.on('data', (chunk) => {
body += chunk;
});

res.on('end', common.mustCall(() => {
debug('Got full response.');
assert.strictEqual(body, testResBody);
server.close();
}));
}));

req.end();
}));
}

{
// Happy flow - empty array

const server = http.createServer(common.mustCall((req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints([]);

debug('Server sending full response...');
res.end(testResBody);
}));

server.listen(0, common.mustCall(() => {
const req = http.request({
port: server.address().port, path: '/'
});
debug('Client sending request...');

req.on('information', common.mustNotCall());

req.on('response', common.mustCall((res) => {
let body = '';

assert.strictEqual(res.statusCode, 200, `Final status code was ${res.statusCode}, not 200.`);

res.on('data', (chunk) => {
body += chunk;
});

res.on('end', common.mustCall(() => {
debug('Got full response.');
assert.strictEqual(body, testResBody);
server.close();
}));
}));

req.end();
}));
}
Loading