diff --git a/lib/_http_server.js b/lib/_http_server.js index 7e96a0b2bb3397..6fecbbbd21bd82 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -51,7 +51,9 @@ const { ConnectionsList } = internalBinding('http_parser'); const { kUniqueHeaders, parseUniqueHeadersOption, - OutgoingMessage + OutgoingMessage, + validateHeaderName, + validateHeaderValue, } = require('_http_outgoing'); const { kOutHeaders, @@ -84,7 +86,6 @@ const { const { validateInteger, validateBoolean, - validateLinkHeaderValue, validateObject } = require('internal/validators'); const Buffer = require('buffer').Buffer; @@ -305,20 +306,16 @@ ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) { validateObject(hints, 'hints'); - if (hints.link === null || hints.link === undefined) { - return; - } - - const link = validateLinkHeaderValue(hints.link); - - if (link.length === 0) { - return; - } - - head += 'Link: ' + link + '\r\n'; - for (const key of ObjectKeys(hints)) { - if (key !== 'link') { + const hint = hints[key]; + validateHeaderName(key); + validateHeaderValue(key, hint); + + if (ArrayIsArray(hint)) { + for (let i = 0; i < hint.length; i++) { + head += key + ': ' + hint[i] + '\r\n'; + } + } else { head += key + ': ' + hints[key] + '\r\n'; } } diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index dec5b734a2490f..f48cee4c2cb159 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -55,7 +55,6 @@ const { const { validateFunction, validateString, - validateLinkHeaderValue, validateObject, } = require('internal/validators'); const { @@ -850,29 +849,14 @@ class Http2ServerResponse extends Stream { writeEarlyHints(hints) { validateObject(hints, 'hints'); - const headers = { __proto__: null }; - - const linkHeaderValue = validateLinkHeaderValue(hints.link); - - for (const key of ObjectKeys(hints)) { - if (key !== 'link') { - headers[key] = hints[key]; - } - } - - if (linkHeaderValue.length === 0) { - return false; - } - const stream = this[kStream]; if (stream.headersSent || this[kState].closed) return false; stream.additionalHeaders({ - ...headers, + ...hints, [HTTP2_HEADER_STATUS]: HTTP_STATUS_EARLY_HINTS, - 'Link': linkHeaderValue, }); return true; diff --git a/lib/internal/validators.js b/lib/internal/validators.js index 50b3016ab78ec2..05c1f2a02a3c57 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -459,67 +459,12 @@ function validateUnion(value, name, union) { } } -const linkValueRegExp = /^(?:<[^>]*>;)\s*(?:rel=(")?[^;"]*\1;?)\s*(?:(?:as|anchor|title|crossorigin|disabled|fetchpriority|rel|referrerpolicy)=(")?[^;"]*\2)?$/; - -/** - * @param {any} value - * @param {string} name - */ -function validateLinkHeaderFormat(value, name) { - if ( - typeof value === 'undefined' || - !RegExpPrototypeExec(linkValueRegExp, value) - ) { - throw new ERR_INVALID_ARG_VALUE( - name, - value, - 'must be an array or string of format "; rel=preload; as=style"' - ); - } -} - const validateInternalField = hideStackFrames((object, fieldKey, className) => { if (typeof object !== 'object' || object === null || !ObjectPrototypeHasOwnProperty(object, fieldKey)) { throw new ERR_INVALID_ARG_TYPE('this', className, object); } }); -/** - * @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 "; rel=preload; as=style"' - ); -} - module.exports = { isInt32, isUint32, @@ -545,6 +490,5 @@ module.exports = { validateUndefined, validateUnion, validateAbortSignal, - validateLinkHeaderValue, validateInternalField, }; diff --git a/test/parallel/test-http-early-hints.js b/test/parallel/test-http-early-hints.js index 7183d9516f6dda..6cd017eb59f753 100644 --- a/test/parallel/test-http-early-hints.js +++ b/test/parallel/test-http-early-hints.js @@ -99,47 +99,6 @@ const testResBody = 'response content\n'; })); } -{ - // Happy flow - empty array - - const server = http.createServer(common.mustCall((req, res) => { - debug('Server sending early hints...'); - res.writeEarlyHints({ - link: [] - }); - - 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(); - })); -} - { // Happy flow - object argument with string @@ -256,7 +215,9 @@ const testResBody = 'response content\n'; }); debug('Client sending request...'); - req.on('information', common.mustNotCall()); + req.on('information', common.mustCall((info) => { + assert.strictEqual(info.statusCode, 103) + })); req.on('response', common.mustCall((res) => { let body = ''; diff --git a/test/parallel/test-http2-compat-write-early-hints-invalid-argument-value.js b/test/parallel/test-http2-compat-write-early-hints-invalid-argument-value.js deleted file mode 100644 index d640f13fae6f5e..00000000000000 --- a/test/parallel/test-http2-compat-write-early-hints-invalid-argument-value.js +++ /dev/null @@ -1,42 +0,0 @@ -'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'; - -{ - // Invalid link header value - - const server = http2.createServer(); - - server.on('request', common.mustCall((req, res) => { - debug('Server sending early hints...'); - res.writeEarlyHints({ link: BigInt(100) }); - - 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); - }); - })); -} diff --git a/test/parallel/test-http2-compat-write-early-hints.js b/test/parallel/test-http2-compat-write-early-hints.js index d1f26d7c20bbc0..29e63d1c186b31 100644 --- a/test/parallel/test-http2-compat-write-early-hints.js +++ b/test/parallel/test-http2-compat-write-early-hints.js @@ -128,7 +128,9 @@ const testResBody = 'response content'; debug('Client sending request...'); - req.on('headers', common.mustNotCall()); + req.on('headers', common.mustCall((headers) => { + assert.strictEqual(headers[':status'], 103); + })); req.on('response', common.mustCall((headers) => { assert.strictEqual(headers[':status'], 200); diff --git a/test/parallel/test-validators.js b/test/parallel/test-validators.js index a40139678eee65..63cf42e306605c 100644 --- a/test/parallel/test-validators.js +++ b/test/parallel/test-validators.js @@ -12,7 +12,6 @@ const { validateString, validateInt32, validateUint32, - validateLinkHeaderValue, } = require('internal/validators'); const { MAX_SAFE_INTEGER, MIN_SAFE_INTEGER } = Number; const outOfRangeError = { @@ -155,15 +154,3 @@ const invalidArgValueError = { code: 'ERR_INVALID_ARG_TYPE' })); } - -{ - // validateLinkHeaderValue type validation. - [ - ['; rel=preload; as=style', '; rel=preload; as=style'], - ['; rel=preload; title=hello', '; rel=preload; title=hello'], - ['; rel=preload; crossorigin=hello', '; rel=preload; crossorigin=hello'], - ['; rel=preload; disabled=true', '; rel=preload; disabled=true'], - ['; rel=preload; fetchpriority=high', '; rel=preload; fetchpriority=high'], - ['; rel=preload; referrerpolicy=origin', '; rel=preload; referrerpolicy=origin'], - ].forEach(([value, expected]) => assert.strictEqual(validateLinkHeaderValue(value), expected)); -}