Skip to content

Commit 4cd8e19

Browse files
pimterryjasnell
authored andcommittedApr 26, 2025
http2: add raw header array support to h2Session.request()
This also notably changes error handling for request(). Previously some invalid header values (but not all) would cause the session to be unnecessarily destroyed automatically, e.g. passing an unparseable header name to request(). This is no longer the case: header validation failures will throw an error, but will not destroy the session or emit 'error' events. PR-URL: #57917 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent a8a86b3 commit 4cd8e19

11 files changed

+354
-131
lines changed
 

‎doc/api/http2.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1073,7 +1073,7 @@ The `'origin'` event is only emitted when using a secure TLS connection.
10731073
added: v8.4.0
10741074
-->
10751075

1076-
* `headers` {HTTP/2 Headers Object}
1076+
* `headers` {HTTP/2 Headers Object} | {Array}
10771077

10781078
* `options` {Object}
10791079
* `endStream` {boolean} `true` if the `Http2Stream` _writable_ side should

‎lib/internal/http2/core.js

+50-60
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ const {
99
ObjectDefineProperty,
1010
ObjectEntries,
1111
ObjectHasOwn,
12-
ObjectKeys,
1312
Promise,
1413
Proxy,
1514
ReflectApply,
@@ -46,10 +45,7 @@ const { Duplex } = require('stream');
4645
const tls = require('tls');
4746
const { setImmediate, setTimeout, clearTimeout } = require('timers');
4847

49-
const {
50-
kIncomingMessage,
51-
_checkIsHttpToken: checkIsHttpToken,
52-
} = require('_http_common');
48+
const { kIncomingMessage } = require('_http_common');
5349
const { kServerResponse, Server: HttpServer, httpServerPreClose, setupConnectionsTracking } = require('_http_server');
5450
const JSStreamSocket = require('internal/js_stream_socket');
5551

@@ -68,9 +64,6 @@ const {
6864
codes: {
6965
ERR_HTTP2_ALTSVC_INVALID_ORIGIN,
7066
ERR_HTTP2_ALTSVC_LENGTH,
71-
ERR_HTTP2_CONNECT_AUTHORITY,
72-
ERR_HTTP2_CONNECT_PATH,
73-
ERR_HTTP2_CONNECT_SCHEME,
7467
ERR_HTTP2_GOAWAY_SESSION,
7568
ERR_HTTP2_HEADERS_AFTER_RESPOND,
7669
ERR_HTTP2_HEADERS_SENT,
@@ -108,7 +101,6 @@ const {
108101
ERR_INVALID_ARG_TYPE,
109102
ERR_INVALID_ARG_VALUE,
110103
ERR_INVALID_CHAR,
111-
ERR_INVALID_HTTP_TOKEN,
112104
ERR_OUT_OF_RANGE,
113105
ERR_SOCKET_CLOSED,
114106
},
@@ -137,23 +129,26 @@ const {
137129
const {
138130
assertIsObject,
139131
assertIsArray,
140-
assertValidPseudoHeader,
141132
assertValidPseudoHeaderResponse,
142133
assertValidPseudoHeaderTrailer,
143134
assertWithinRange,
135+
buildNgHeaderString,
144136
getAuthority,
145137
getDefaultSettings,
146138
getSessionState,
147139
getSettings,
148140
getStreamState,
149141
isPayloadMeaningless,
142+
kAuthority,
150143
kSensitiveHeaders,
151144
kSocket,
152145
kRequest,
146+
kProtocol,
153147
kProxySocket,
154-
mapToHeaders,
155148
MAX_ADDITIONAL_SETTINGS,
156149
NghttpError,
150+
prepareRequestHeadersArray,
151+
prepareRequestHeadersObject,
157152
remoteCustomSettingsToBuffer,
158153
sessionName,
159154
toHeaderObject,
@@ -229,7 +224,6 @@ const NETServer = net.Server;
229224
const TLSServer = tls.Server;
230225

231226
const kAlpnProtocol = Symbol('alpnProtocol');
232-
const kAuthority = Symbol('authority');
233227
const kEncrypted = Symbol('encrypted');
234228
const kID = Symbol('id');
235229
const kInit = Symbol('init');
@@ -241,7 +235,6 @@ const kOwner = owner_symbol;
241235
const kOrigin = Symbol('origin');
242236
const kPendingRequestCalls = Symbol('kPendingRequestCalls');
243237
const kProceed = Symbol('proceed');
244-
const kProtocol = Symbol('protocol');
245238
const kRemoteSettings = Symbol('remote-settings');
246239
const kRequestAsyncResource = Symbol('requestAsyncResource');
247240
const kSelectPadding = Symbol('select-padding');
@@ -286,7 +279,6 @@ const {
286279
HTTP2_HEADER_DATE,
287280
HTTP2_HEADER_METHOD,
288281
HTTP2_HEADER_PATH,
289-
HTTP2_HEADER_PROTOCOL,
290282
HTTP2_HEADER_SCHEME,
291283
HTTP2_HEADER_STATUS,
292284
HTTP2_HEADER_CONTENT_LENGTH,
@@ -301,7 +293,6 @@ const {
301293

302294
HTTP2_METHOD_GET,
303295
HTTP2_METHOD_HEAD,
304-
HTTP2_METHOD_CONNECT,
305296

306297
HTTP_STATUS_CONTINUE,
307298
HTTP_STATUS_RESET_CONTENT,
@@ -1767,7 +1758,7 @@ class ClientHttp2Session extends Http2Session {
17671758

17681759
// Submits a new HTTP2 request to the connected peer. Returns the
17691760
// associated Http2Stream instance.
1770-
request(headers, options) {
1761+
request(headersParam, options) {
17711762
debugSessionObj(this, 'initiating request');
17721763

17731764
if (this.destroyed)
@@ -1778,62 +1769,61 @@ class ClientHttp2Session extends Http2Session {
17781769

17791770
this[kUpdateTimer]();
17801771

1781-
if (headers !== null && headers !== undefined) {
1782-
const keys = ObjectKeys(headers);
1783-
for (let i = 0; i < keys.length; i++) {
1784-
const header = keys[i];
1785-
if (header[0] === ':') {
1786-
assertValidPseudoHeader(header);
1787-
} else if (header && !checkIsHttpToken(header))
1788-
this.destroy(new ERR_INVALID_HTTP_TOKEN('Header name', header));
1789-
}
1772+
let headersList;
1773+
let headersObject;
1774+
let scheme;
1775+
let authority;
1776+
let method;
1777+
1778+
if (ArrayIsArray(headersParam)) {
1779+
({
1780+
headersList,
1781+
scheme,
1782+
authority,
1783+
method,
1784+
} = prepareRequestHeadersArray(headersParam, this));
1785+
} else if (!!headersParam && typeof headersParam === 'object') {
1786+
({
1787+
headersObject,
1788+
headersList,
1789+
scheme,
1790+
authority,
1791+
method,
1792+
} = prepareRequestHeadersObject(headersParam, this));
1793+
} else if (headersParam === undefined) {
1794+
({
1795+
headersObject,
1796+
headersList,
1797+
scheme,
1798+
authority,
1799+
method,
1800+
} = prepareRequestHeadersObject({}, this));
1801+
} else {
1802+
throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(
1803+
'headers',
1804+
['Object', 'Array'],
1805+
headersParam,
1806+
);
17901807
}
17911808

1792-
assertIsObject(headers, 'headers');
17931809
assertIsObject(options, 'options');
1794-
1795-
headers = ObjectAssign({ __proto__: null }, headers);
17961810
options = { ...options };
17971811

1798-
if (headers[HTTP2_HEADER_METHOD] === undefined)
1799-
headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
1800-
1801-
const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;
1802-
1803-
if (!connect || headers[HTTP2_HEADER_PROTOCOL] !== undefined) {
1804-
if (getAuthority(headers) === undefined)
1805-
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
1806-
if (headers[HTTP2_HEADER_SCHEME] === undefined)
1807-
headers[HTTP2_HEADER_SCHEME] = this[kProtocol].slice(0, -1);
1808-
if (headers[HTTP2_HEADER_PATH] === undefined)
1809-
headers[HTTP2_HEADER_PATH] = '/';
1810-
} else {
1811-
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
1812-
throw new ERR_HTTP2_CONNECT_AUTHORITY();
1813-
if (headers[HTTP2_HEADER_SCHEME] !== undefined)
1814-
throw new ERR_HTTP2_CONNECT_SCHEME();
1815-
if (headers[HTTP2_HEADER_PATH] !== undefined)
1816-
throw new ERR_HTTP2_CONNECT_PATH();
1817-
}
1818-
18191812
setAndValidatePriorityOptions(options);
18201813

18211814
if (options.endStream === undefined) {
18221815
// For some methods, we know that a payload is meaningless, so end the
18231816
// stream by default if the user has not specifically indicated a
18241817
// preference.
1825-
options.endStream = isPayloadMeaningless(headers[HTTP2_HEADER_METHOD]);
1818+
options.endStream = isPayloadMeaningless(method);
18261819
} else {
18271820
validateBoolean(options.endStream, 'options.endStream');
18281821
}
18291822

1830-
const headersList = mapToHeaders(headers);
1831-
18321823
// eslint-disable-next-line no-use-before-define
18331824
const stream = new ClientHttp2Stream(this, undefined, undefined, {});
1834-
stream[kSentHeaders] = headers;
1835-
stream[kOrigin] = `${headers[HTTP2_HEADER_SCHEME]}://` +
1836-
`${getAuthority(headers)}`;
1825+
stream[kSentHeaders] = headersObject; // N.b. Only set for object headers, not raw headers
1826+
stream[kOrigin] = `${scheme}://${authority}`;
18371827
const reqAsync = new AsyncResource('PendingRequest');
18381828
stream[kRequestAsyncResource] = reqAsync;
18391829

@@ -2295,7 +2285,7 @@ class Http2Stream extends Duplex {
22952285

22962286
this[kUpdateTimer]();
22972287

2298-
const headersList = mapToHeaders(headers, assertValidPseudoHeaderTrailer);
2288+
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderTrailer);
22992289
this[kSentTrailers] = headers;
23002290

23012291
// Send the trailers in setImmediate so we don't do it on nghttp2 stack.
@@ -2528,7 +2518,7 @@ function processRespondWithFD(self, fd, headers, offset = 0, length = -1,
25282518

25292519
let headersList;
25302520
try {
2531-
headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
2521+
headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
25322522
} catch (err) {
25332523
self.destroy(err);
25342524
return;
@@ -2752,7 +2742,7 @@ class ServerHttp2Stream extends Http2Stream {
27522742
if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD)
27532743
headRequest = options.endStream = true;
27542744

2755-
const headersList = mapToHeaders(headers);
2745+
const headersList = buildNgHeaderString(headers);
27562746

27572747
const streamOptions = options.endStream ? STREAM_OPTION_EMPTY_PAYLOAD : 0;
27582748

@@ -2816,7 +2806,7 @@ class ServerHttp2Stream extends Http2Stream {
28162806
}
28172807

28182808
headers = processHeaders(headers, options);
2819-
const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
2809+
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
28202810
this[kSentHeaders] = headers;
28212811

28222812
state.flags |= STREAM_FLAGS_HEADERS_SENT;
@@ -2984,7 +2974,7 @@ class ServerHttp2Stream extends Http2Stream {
29842974

29852975
this[kUpdateTimer]();
29862976

2987-
const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
2977+
const headersList = buildNgHeaderString(headers, assertValidPseudoHeaderResponse);
29882978
if (!this[kInfoHeaders])
29892979
this[kInfoHeaders] = [headers];
29902980
else

0 commit comments

Comments
 (0)
Failed to load comments.