Skip to content

Commit dafee05

Browse files
pimterrytargos
authored andcommitted
http2: add support for raw header arrays in h2Stream.respond()
PR-URL: #59455 Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Gerhard Stöbich <deb2001-github@yahoo.de>
1 parent 8dc6f5b commit dafee05

5 files changed

Lines changed: 204 additions & 35 deletions

File tree

‎doc/api/http2.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,11 @@ changes:
10801080
pr-url: https://github.com/nodejs/node/pull/58313
10811081
description: Following the deprecation of priority signaling as of RFC 9113,
10821082
`weight` option is deprecated.
1083+
- version:
1084+
- v24.0.0
1085+
- v22.17.0
1086+
pr-url: https://github.com/nodejs/node/pull/57917
1087+
description: Allow passing headers in raw array format.
10831088
-->
10841089

10851090
*`headers` {HTTP/2 Headers Object|Array}
@@ -1850,14 +1855,18 @@ and will throw an error.
18501855
<!-- YAML
18511856
added: v8.4.0
18521857
changes:
1858+
- version:
1859+
- REPLACEME
1860+
pr-url: https://github.com/nodejs/node/pull/59455
1861+
description: Allow passing headers in raw array format.
18531862
- version:
18541863
- v14.5.0
18551864
- v12.19.0
18561865
pr-url: https://github.com/nodejs/node/pull/33160
18571866
description: Allow explicitly setting date headers.
18581867
-->
18591868

1860-
*`headers` {HTTP/2 Headers Object}
1869+
*`headers` {HTTP/2 Headers Object|Array}
18611870
*`options` {Object}
18621871
*`endStream` {boolean} Set to `true` to indicate that the response will not
18631872
include payload data.

‎lib/internal/http2/core.js‎

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2541,8 +2541,31 @@ function callStreamClose(stream) {
25412541
stream.close();
25422542
}
25432543

2544-
functionprocessHeaders(oldHeaders,options){
2545-
assertIsObject(oldHeaders,'headers');
2544+
functionprepareResponseHeaders(stream,headersParam,options){
2545+
letheaders;
2546+
letstatusCode;
2547+
2548+
if(ArrayIsArray(headersParam)){
2549+
({
2550+
headers,
2551+
statusCode,
2552+
}=prepareResponseHeadersArray(headersParam,options));
2553+
stream[kRawHeaders]=headers;
2554+
}else{
2555+
({
2556+
headers,
2557+
statusCode,
2558+
}=prepareResponseHeadersObject(headersParam,options));
2559+
stream[kSentHeaders]=headers;
2560+
}
2561+
2562+
constheadersList=buildNgHeaderString(headers,assertValidPseudoHeaderResponse);
2563+
2564+
return{ headers, headersList, statusCode };
2565+
}
2566+
2567+
functionprepareResponseHeadersObject(oldHeaders,options){
2568+
assertIsObject(oldHeaders,'headers',['Object','Array']);
25462569
constheaders={__proto__: null};
25472570

25482571
if(oldHeaders!==null&&oldHeaders!==undefined){
@@ -2563,23 +2586,58 @@ function processHeaders(oldHeaders, options) {
25632586
headers[HTTP2_HEADER_DATE]??=utcDate();
25642587
}
25652588

2589+
validatePreparedResponseHeaders(headers,statusCode);
2590+
2591+
return{
2592+
headers,
2593+
statusCode: headers[HTTP2_HEADER_STATUS],
2594+
};
2595+
}
2596+
2597+
functionprepareResponseHeadersArray(headers,options){
2598+
letstatusCode;
2599+
letisDateSet=false;
2600+
2601+
for(leti=0;i<headers.length;i+=2){
2602+
constheader=headers[i].toLowerCase();
2603+
constvalue=headers[i+1];
2604+
2605+
if(header===HTTP2_HEADER_STATUS){
2606+
statusCode=value|0;
2607+
}elseif(header===HTTP2_HEADER_DATE){
2608+
isDateSet=true;
2609+
}
2610+
}
2611+
2612+
if(!statusCode){
2613+
statusCode=HTTP_STATUS_OK;
2614+
headers.unshift(HTTP2_HEADER_STATUS,statusCode);
2615+
}
2616+
2617+
if(!isDateSet&&(options.sendDate==null||options.sendDate)){
2618+
headers.push(HTTP2_HEADER_DATE,utcDate());
2619+
}
2620+
2621+
validatePreparedResponseHeaders(headers,statusCode);
2622+
2623+
return{ headers, statusCode };
2624+
}
2625+
2626+
functionvalidatePreparedResponseHeaders(headers,statusCode){
25662627
// This is intentionally stricter than the HTTP/1 implementation, which
25672628
// allows values between 100 and 999 (inclusive) in order to allow for
25682629
// backwards compatibility with non-spec compliant code. With HTTP/2,
25692630
// we have the opportunity to start fresh with stricter spec compliance.
25702631
// This will have an impact on the compatibility layer for anyone using
25712632
// non-standard, non-compliant status codes.
25722633
if(statusCode<200||statusCode>599)
2573-
thrownewERR_HTTP2_STATUS_INVALID(headers[HTTP2_HEADER_STATUS]);
2634+
thrownewERR_HTTP2_STATUS_INVALID(statusCode);
25742635

25752636
constneverIndex=headers[kSensitiveHeaders];
25762637
if(neverIndex!==undefined&&!ArrayIsArray(neverIndex))
25772638
thrownewERR_INVALID_ARG_VALUE('headers[http2.neverIndex]',neverIndex);
2578-
2579-
returnheaders;
25802639
}
25812640

2582-
25832641
functiononFileUnpipe(){
25842642
conststream=this.sink[kOwner];
25852643
if(stream.ownsFd)
@@ -2882,7 +2940,7 @@ class ServerHttp2Stream extends Http2Stream {
28822940
}
28832941

28842942
// Initiate a response on this Http2Stream
2885-
respond(headers,options){
2943+
respond(headersParam,options){
28862944
if(this.destroyed||this.closed)
28872945
thrownewERR_HTTP2_INVALID_STREAM();
28882946
if(this.headersSent)
@@ -2907,15 +2965,16 @@ class ServerHttp2Stream extends Http2Stream {
29072965
state.flags|=STREAM_FLAGS_HAS_TRAILERS;
29082966
}
29092967

2910-
headers=processHeaders(headers,options);
2911-
constheadersList=buildNgHeaderString(headers,assertValidPseudoHeaderResponse);
2912-
this[kSentHeaders]=headers;
2968+
const{
2969+
headers,
2970+
headersList,
2971+
statusCode,
2972+
}=prepareResponseHeaders(this,headersParam,options);
29132973

29142974
state.flags|=STREAM_FLAGS_HEADERS_SENT;
29152975

29162976
// Close the writable side if the endStream option is set or status
29172977
// is one of known codes with no payload, or it's a head request
2918-
conststatusCode=headers[HTTP2_HEADER_STATUS]|0;
29192978
if(!!options.endStream||
29202979
statusCode===HTTP_STATUS_NO_CONTENT||
29212980
statusCode===HTTP_STATUS_RESET_CONTENT||
@@ -2945,7 +3004,7 @@ class ServerHttp2Stream extends Http2Stream {
29453004
// regular file, here the fd is passed directly. If the underlying
29463005
// mechanism is not able to read from the fd, then the stream will be
29473006
// reset with an error code.
2948-
respondWithFD(fd,headers,options){
3007+
respondWithFD(fd,headersParam,options){
29493008
if(this.destroyed||this.closed)
29503009
thrownewERR_HTTP2_INVALID_STREAM();
29513010
if(this.headersSent)
@@ -2982,8 +3041,11 @@ class ServerHttp2Stream extends Http2Stream {
29823041
this[kUpdateTimer]();
29833042
this.ownsFd=false;
29843043

2985-
headers=processHeaders(headers,options);
2986-
conststatusCode=headers[HTTP2_HEADER_STATUS]|=0;
3044+
const{
3045+
headers,
3046+
statusCode,
3047+
}=prepareResponseHeadersObject(headersParam,options);
3048+
29873049
// Payload/DATA frames are not permitted in these cases
29883050
if(statusCode===HTTP_STATUS_NO_CONTENT||
29893051
statusCode===HTTP_STATUS_RESET_CONTENT||
@@ -3011,7 +3073,7 @@ class ServerHttp2Stream extends Http2Stream {
30113073
// giving the user an opportunity to verify the details and set additional
30123074
// headers. If statCheck returns false, the operation is aborted and no
30133075
// file details are sent.
3014-
respondWithFile(path,headers,options){
3076+
respondWithFile(path,headersParam,options){
30153077
if(this.destroyed||this.closed)
30163078
thrownewERR_HTTP2_INVALID_STREAM();
30173079
if(this.headersSent)
@@ -3042,8 +3104,11 @@ class ServerHttp2Stream extends Http2Stream {
30423104
this[kUpdateTimer]();
30433105
this.ownsFd=true;
30443106

3045-
headers=processHeaders(headers,options);
3046-
conststatusCode=headers[HTTP2_HEADER_STATUS]|=0;
3107+
const{
3108+
headers,
3109+
statusCode,
3110+
}=prepareResponseHeadersObject(headersParam,options);
3111+
30473112
// Payload/DATA frames are not permitted in these cases
30483113
if(statusCode===HTTP_STATUS_NO_CONTENT||
30493114
statusCode===HTTP_STATUS_RESET_CONTENT||

‎lib/internal/http2/util.js‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,6 @@ function prepareRequestHeadersArray(headers, session) {
690690
constheadersList=buildNgHeaderString(
691691
rawHeaders,
692692
assertValidPseudoHeader,
693-
headers[kSensitiveHeaders],
694693
);
695694

696695
return{
@@ -755,14 +754,14 @@ const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE);
755754
* @returns {[string, number]}
756755
*/
757756
functionbuildNgHeaderString(arrayOrMap,
758-
assertValuePseudoHeader=assertValidPseudoHeader,
759-
sensitiveHeaders=arrayOrMap[kSensitiveHeaders]){
757+
assertValuePseudoHeader=assertValidPseudoHeader){
760758
letheaders='';
761759
letpseudoHeaders='';
762760
letcount=0;
763761

764762
constsingles=newSafeSet();
765-
constneverIndex=(sensitiveHeaders||emptyArray).map((v)=>v.toLowerCase());
763+
constsensitiveHeaders=arrayOrMap[kSensitiveHeaders]||emptyArray;
764+
constneverIndex=sensitiveHeaders.map((v)=>v.toLowerCase());
766765

767766
functionprocessHeader(key,value){
768767
key=key.toLowerCase();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
'use strict';
2+
3+
constcommon=require('../common');
4+
if(!common.hasCrypto)
5+
common.skip('missing crypto');
6+
constassert=require('assert');
7+
consthttp2=require('http2');
8+
9+
{
10+
constserver=http2.createServer();
11+
server.on('stream',common.mustCall((stream,_headers,_flags,rawHeaders)=>{
12+
assert.deepStrictEqual(rawHeaders,[
13+
':method','GET',
14+
':authority',`localhost:${server.address().port}`,
15+
':scheme','http',
16+
':path','/',
17+
'a','b',
18+
'x-foo','bar',// Lowercased as required for HTTP/2
19+
'a','c',// Duplicate header order preserved
20+
]);
21+
stream.respond([
22+
'x','1',
23+
'x-FOO','bar',
24+
'x','2',
25+
]);
26+
27+
assert.partialDeepStrictEqual(stream.sentHeaders,{
28+
'__proto__': null,
29+
':status': 200,
30+
'x': ['1','2'],
31+
'x-FOO': 'bar',
32+
});
33+
34+
assert.strictEqual(typeofstream.sentHeaders.date,'string');
35+
36+
stream.end();
37+
}));
38+
39+
40+
server.listen(0,common.mustCall(()=>{
41+
constport=server.address().port;
42+
constclient=http2.connect(`http://localhost:${port}`);
43+
44+
constreq=client.request([
45+
'a','b',
46+
'x-FOO','bar',
47+
'a','c',
48+
]).end();
49+
50+
assert.deepStrictEqual(req.sentHeaders,{
51+
'__proto__': null,
52+
':path': '/',
53+
':scheme': 'http',
54+
':authority': `localhost:${server.address().port}`,
55+
':method': 'GET',
56+
'a': ['b','c'],
57+
'x-FOO': 'bar',
58+
});
59+
60+
req.on('response',common.mustCall((_headers,_flags,rawHeaders)=>{
61+
assert.strictEqual(rawHeaders.length,10);
62+
assert.deepStrictEqual(rawHeaders.slice(0,8),[
63+
':status','200',
64+
'x','1',
65+
'x-foo','bar',// Lowercased as required for HTTP/2
66+
'x','2',// Duplicate header order preserved
67+
]);
68+
69+
assert.strictEqual(rawHeaders[8],'date');
70+
assert.strictEqual(typeofrawHeaders[9],'string');
71+
72+
client.close();
73+
server.close();
74+
}));
75+
}));
76+
}

‎test/parallel/test-http2-raw-headers.js‎

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,33 @@ const http2 = require('http2');
88

99
{
1010
constserver=http2.createServer();
11-
server.on('stream',common.mustCall((stream,headers,flags,rawHeaders)=>{
11+
server.on('stream',common.mustCall((stream,_headers,_flags,rawHeaders)=>{
1212
assert.deepStrictEqual(rawHeaders,[
1313
':path','/foobar',
1414
':scheme','http',
15-
':authority',`localhost:${server.address().port}`,
16-
':method','GET',
15+
':authority',`test.invalid:${server.address().port}`,
16+
':method','POST',
1717
'a','b',
18-
'x-foo','bar',
19-
'a','c',
18+
'x-foo','bar',// Lowercased as required for HTTP/2
19+
'a','c',// Duplicate header order preserved
20+
]);
21+
22+
stream.respond([
23+
':status','404',
24+
'x','1',
25+
'x-FOO','bar',
26+
'x','2',
27+
'DATE','0000',
2028
]);
21-
stream.respond({
22-
':status': 200
29+
30+
assert.deepStrictEqual(stream.sentHeaders,{
31+
'__proto__': null,
32+
':status': '404',
33+
'x': ['1','2'],
34+
'x-FOO': 'bar',
35+
'DATE': '0000',
2336
});
37+
2438
stream.end();
2539
}));
2640

@@ -32,8 +46,8 @@ const http2 = require('http2');
3246
constreq=client.request([
3347
':path','/foobar',
3448
':scheme','http',
35-
':authority',`localhost:${server.address().port}`,
36-
':method','GET',
49+
':authority',`test.invalid:${server.address().port}`,
50+
':method','POST',
3751
'a','b',
3852
'x-FOO','bar',
3953
'a','c',
@@ -43,14 +57,20 @@ const http2 = require('http2');
4357
'__proto__': null,
4458
':path': '/foobar',
4559
':scheme': 'http',
46-
':authority': `localhost:${server.address().port}`,
47-
':method': 'GET',
60+
':authority': `test.invalid:${server.address().port}`,
61+
':method': 'POST',
4862
'a': ['b','c'],
4963
'x-FOO': 'bar',
5064
});
5165

52-
req.on('response',common.mustCall((headers)=>{
53-
assert.strictEqual(headers[':status'],200);
66+
req.on('response',common.mustCall((_headers,_flags,rawHeaders)=>{
67+
assert.deepStrictEqual(rawHeaders,[
68+
':status','404',
69+
'x','1',
70+
'x-foo','bar',// Lowercased as required for HTTP/2
71+
'x','2',// Duplicate header order preserved
72+
'date','0000',// Server doesn't automatically set its own value
73+
]);
5474
client.close();
5575
server.close();
5676
}));

0 commit comments

Comments
 (0)