Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
http2: make early hints generic
  • Loading branch information
anonrig committed Oct 4, 2022
commit ca64b81be309c44c794066b8bf10fccb26827fd6
47 changes: 46 additions & 1 deletion lib/_http_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,11 +320,56 @@ ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(links, cb) {
}

head += '\r\n';
} else if (typeof links === 'object') {
const keys = ObjectKeys(links);

if (!keys.length) {
return;
}

const link = links.link;

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

head += 'Link: ';

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

if (i !== linkLength - 1) {
head += ', ';
}
}

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

for (const key of keys) {
if (key === 'link') {
continue;
}

head += key + ', ' + keys[key] + '\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"'
'must be an array, object or string of format "</styles.css>; rel=preload; as=style"'
);
}

Expand Down
46 changes: 44 additions & 2 deletions lib/internal/http2/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,7 @@ class Http2ServerResponse extends Stream {
}

writeEarlyHints(links) {
const headers = ObjectCreate(null);
let linkHeaderValue = '';

if (typeof links === 'string') {
Expand All @@ -869,11 +870,51 @@ class Http2ServerResponse extends Stream {
linkHeaderValue += ', ';
}
}
} else if (typeof links === 'object') {
const keys = ObjectKeys(links);

if (!keys.length) {
return;
}

const link = links.link;

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

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

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

for (const key of keys) {
if (key === 'link') {
continue;
}

headers[key] = keys[key];
}
} else {
throw new ERR_INVALID_ARG_VALUE(
'links',
links,
'must be an array or string of format "</styles.css>; rel=preload; as=style"'
'must be an array, object or string of format "</styles.css>; rel=preload; as=style"'
);
}

Expand All @@ -884,7 +925,8 @@ class Http2ServerResponse extends Stream {

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

return true;
Expand Down
33 changes: 0 additions & 33 deletions test/parallel/test-http-early-hints-invalid-argument-type.js

This file was deleted.

86 changes: 86 additions & 0 deletions test/parallel/test-http-early-hints.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,89 @@ const testResBody = 'response content\n';
req.end();
}));
}

{
// Happy flow - object argument

const server = http.createServer(common.mustCall((req, res) => {
debug('Server sending early hints...');
res.writeEarlyHints({
'link': '</styles.css>; rel=preload; as=style',
'x-trace-id': 'id for diagnostics'
});

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 object

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();
}));
}