mirror of
https://github.com/nodejs/node.git
synced 2025-12-28 07:50:41 +00:00
http: add writeEarlyHints function to ServerResponse
Co-Authored-By: Matteo Collina <matteo.collina@gmail.com> Co-Authored-By: Livia Medeiros <livia@cirno.name> PR-URL: https://github.com/nodejs/node/pull/44180 Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: LiviaMedeiros <livia@cirno.name>
This commit is contained in:
parent
97e8bda5b2
commit
58ab0e2821
@ -2129,10 +2129,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])`
|
||||
|
||||
<!-- 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
|
||||
|
||||
@ -4005,6 +4005,32 @@ Sends a status `100 Continue` to the client, indicating that the request body
|
||||
should be sent. See the [`'checkContinue'`][] event on `Http2Server` and
|
||||
`Http2SecureServer`.
|
||||
|
||||
### `response.writeEarlyHints(links)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `links` {string|Array}
|
||||
|
||||
Sends a status `103 Early Hints` 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.
|
||||
|
||||
**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);
|
||||
```
|
||||
|
||||
#### `response.writeHead(statusCode[, statusMessage][, headers])`
|
||||
|
||||
<!-- YAML
|
||||
|
||||
@ -80,7 +80,8 @@ const {
|
||||
} = codes;
|
||||
const {
|
||||
validateInteger,
|
||||
validateBoolean
|
||||
validateBoolean,
|
||||
validateLinkHeaderValue
|
||||
} = require('internal/validators');
|
||||
const Buffer = require('buffer').Buffer;
|
||||
const { setInterval, clearInterval } = require('timers');
|
||||
@ -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') {
|
||||
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);
|
||||
};
|
||||
|
||||
@ -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
|
||||
@ -55,6 +56,7 @@ const {
|
||||
const {
|
||||
validateFunction,
|
||||
validateString,
|
||||
validateLinkHeaderValue,
|
||||
} = require('internal/validators');
|
||||
const {
|
||||
kSocket,
|
||||
@ -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,
|
||||
|
||||
@ -258,6 +258,21 @@ function validateUnion(value, name, union) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateLinkHeaderValue(value, name) {
|
||||
const linkValueRegExp = /^(?:<[^>]*>;)\s*(?:rel=(")?[^;"]*\1;?)\s*(?:(?:as|anchor|title)=(")?[^;"]*\2)?$/;
|
||||
|
||||
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,
|
||||
@ -280,4 +295,5 @@ module.exports = {
|
||||
validateUndefined,
|
||||
validateUnion,
|
||||
validateAbortSignal,
|
||||
validateLinkHeaderValue
|
||||
};
|
||||
|
||||
33
test/parallel/test-http-early-hints-invalid-argument-type.js
Normal file
33
test/parallel/test-http-early-hints-invalid-argument-type.js
Normal file
@ -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
test/parallel/test-http-early-hints-invalid-argument.js
Normal file
33
test/parallel/test-http-early-hints-invalid-argument.js
Normal file
@ -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
test/parallel/test-http-early-hints.js
Normal file
135
test/parallel/test-http-early-hints.js
Normal file
@ -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();
|
||||
}));
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) common.skip('missing crypto');
|
||||
|
||||
const assert = require('node:assert');
|
||||
const http2 = require('node:http2');
|
||||
const debug = require('node:util').debuglog('test');
|
||||
|
||||
const testResBody = 'response content';
|
||||
|
||||
const server = http2.createServer();
|
||||
|
||||
server.on('request', 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);
|
||||
|
||||
server.on('listening', common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
const req = client.request();
|
||||
|
||||
debug('Client sending request...');
|
||||
|
||||
req.on('headers', 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);
|
||||
});
|
||||
}));
|
||||
@ -0,0 +1,38 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) common.skip('missing crypto');
|
||||
|
||||
const assert = require('node:assert');
|
||||
const http2 = require('node:http2');
|
||||
const debug = require('node:util').debuglog('test');
|
||||
|
||||
const testResBody = 'response content';
|
||||
|
||||
const server = http2.createServer();
|
||||
|
||||
server.on('request', 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);
|
||||
|
||||
server.on('listening', common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
const req = client.request();
|
||||
|
||||
debug('Client sending request...');
|
||||
|
||||
req.on('headers', 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);
|
||||
});
|
||||
}));
|
||||
141
test/parallel/test-http2-compat-write-early-hints.js
Normal file
141
test/parallel/test-http2-compat-write-early-hints.js
Normal file
@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto) common.skip('missing crypto');
|
||||
|
||||
const assert = require('node:assert');
|
||||
const http2 = require('node:http2');
|
||||
const debug = require('node:util').debuglog('test');
|
||||
|
||||
const testResBody = 'response content';
|
||||
|
||||
{
|
||||
// Happy flow - string argument
|
||||
|
||||
const server = http2.createServer();
|
||||
|
||||
server.on('request', 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);
|
||||
|
||||
server.on('listening', common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
const req = client.request();
|
||||
|
||||
debug('Client sending request...');
|
||||
|
||||
req.on('headers', common.mustCall((headers) => {
|
||||
assert.notStrictEqual(headers, undefined);
|
||||
assert.strictEqual(headers[':status'], 103);
|
||||
assert.strictEqual(headers.link, '</styles.css>; rel=preload; as=style');
|
||||
}));
|
||||
|
||||
req.on('response', common.mustCall((headers) => {
|
||||
assert.strictEqual(headers[':status'], 200);
|
||||
}));
|
||||
|
||||
let data = '';
|
||||
req.on('data', common.mustCallAtLeast((d) => data += d));
|
||||
|
||||
req.on('end', common.mustCall(() => {
|
||||
debug('Got full response.');
|
||||
assert.strictEqual(data, testResBody);
|
||||
client.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
// Happy flow - array argument
|
||||
|
||||
const server = http2.createServer();
|
||||
|
||||
server.on('request', 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);
|
||||
|
||||
server.on('listening', common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
const req = client.request();
|
||||
|
||||
debug('Client sending request...');
|
||||
|
||||
req.on('headers', common.mustCall((headers) => {
|
||||
assert.notStrictEqual(headers, undefined);
|
||||
assert.strictEqual(headers[':status'], 103);
|
||||
assert.strictEqual(
|
||||
headers.link,
|
||||
'</styles.css>; rel=preload; as=style, </scripts.js>; rel=preload; as=script'
|
||||
);
|
||||
}));
|
||||
|
||||
req.on('response', common.mustCall((headers) => {
|
||||
assert.strictEqual(headers[':status'], 200);
|
||||
}));
|
||||
|
||||
let data = '';
|
||||
req.on('data', common.mustCallAtLeast((d) => data += d));
|
||||
|
||||
req.on('end', common.mustCall(() => {
|
||||
debug('Got full response.');
|
||||
assert.strictEqual(data, testResBody);
|
||||
client.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
// Happy flow - empty array
|
||||
|
||||
const server = http2.createServer();
|
||||
|
||||
server.on('request', common.mustCall((req, res) => {
|
||||
debug('Server sending early hints...');
|
||||
res.writeEarlyHints([]);
|
||||
|
||||
debug('Server sending full response...');
|
||||
res.end(testResBody);
|
||||
}));
|
||||
|
||||
server.listen(0);
|
||||
|
||||
server.on('listening', common.mustCall(() => {
|
||||
const client = http2.connect(`http://localhost:${server.address().port}`);
|
||||
const req = client.request();
|
||||
|
||||
debug('Client sending request...');
|
||||
|
||||
req.on('headers', common.mustNotCall());
|
||||
|
||||
req.on('response', common.mustCall((headers) => {
|
||||
assert.strictEqual(headers[':status'], 200);
|
||||
}));
|
||||
|
||||
let data = '';
|
||||
req.on('data', common.mustCallAtLeast((d) => data += d));
|
||||
|
||||
req.on('end', common.mustCall(() => {
|
||||
debug('Got full response.');
|
||||
assert.strictEqual(data, testResBody);
|
||||
client.close();
|
||||
server.close();
|
||||
}));
|
||||
}));
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user