@@ -79,6 +79,7 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
7979} ) ;
8080
8181const kCorked = Symbol ( 'corked' ) ;
82+ const kLenientValidation = Symbol ( 'kLenientValidation' ) ;
8283const kSocket = Symbol ( 'kSocket' ) ;
8384const kChunkedBuffer = Symbol ( 'kChunkedBuffer' ) ;
8485const kChunkedLength = Symbol ( 'kChunkedLength' ) ;
@@ -143,6 +144,7 @@ function OutgoingMessage(options) {
143144 this [ kChunkedBuffer ] = [ ] ;
144145 this [ kChunkedLength ] = 0 ;
145146 this . _closed = false ;
147+ this [ kLenientValidation ] = undefined ;
146148
147149 this [ kSocket ] = null ;
148150 this . _header = null ;
@@ -163,28 +165,40 @@ ObjectSetPrototypeOf(OutgoingMessage, Stream);
163165// For ClientRequest: checks this.httpValidation or this.insecureHTTPParser
164166// For ServerResponse: checks the server's httpValidation or insecureHTTPParser
165167// Falls back to global --insecure-http-parser flag.
168+ // The answer is invariant for the lifetime of the message (the options are
169+ // fixed at construction time), but this runs on every setHeader/appendHeader
170+ // call and once per stored header block, so the multi-step lookup chain is
171+ // resolved once per message and cached.
166172OutgoingMessage . prototype . _isLenientHeaderValidation = function ( ) {
173+ return ( this [ kLenientValidation ] ??= lenientHeaderValidation ( this ) ) ;
174+ } ;
175+
176+ function lenientHeaderValidation ( msg ) {
177+ const {
178+ httpValidation,
179+ insecureHTTPParser,
180+ } = msg ;
167181 // New httpValidation option takes priority (ClientRequest case)
168- if ( this . httpValidation !== undefined ) {
169- return this . httpValidation !== 'strict' ;
182+ if ( httpValidation !== undefined ) {
183+ return httpValidation !== 'strict' ;
170184 }
171185 // ServerResponse: check server's httpValidation option
172- const serverHttpValidation = this . req ?. socket ?. server ?. httpValidation ;
186+ const serverHttpValidation = msg . req ?. socket ?. server ?. httpValidation ;
173187 if ( serverHttpValidation !== undefined ) {
174188 return serverHttpValidation !== 'strict' ;
175189 }
176190 // Legacy insecureHTTPParser - ClientRequest has it directly
177- if ( typeof this . insecureHTTPParser === 'boolean' ) {
178- return this . insecureHTTPParser ;
191+ if ( typeof insecureHTTPParser === 'boolean' ) {
192+ return insecureHTTPParser ;
179193 }
180194 // ServerResponse can access via req.socket.server
181- const serverOption = this . req ?. socket ?. server ?. insecureHTTPParser ;
195+ const serverOption = msg . req ?. socket ?. server ?. insecureHTTPParser ;
182196 if ( typeof serverOption === 'boolean' ) {
183197 return serverOption ;
184198 }
185199 // Fall back to global option
186200 return isLenient ( ) ;
187- } ;
201+ }
188202
189203ObjectDefineProperty ( OutgoingMessage . prototype , 'errored' , {
190204 __proto__ : null ,
@@ -315,11 +329,12 @@ OutgoingMessage.prototype.uncork = function uncork() {
315329 callbacks . push ( buf [ n + 2 ] ) ;
316330 }
317331 }
318- this . _send ( crlf_buf , null , callbacks . length ? ( err ) => {
319- for ( const callback of callbacks ) {
320- callback ( err ) ;
321- }
322- } : null ) ;
332+ // The runner is a shared top-level function so flushing a corked
333+ // chunked buffer does not allocate a fresh closure environment per
334+ // flush.
335+ this . _send ( crlf_buf , null , callbacks ?. length ?
336+ runChunkCallbacks . bind ( undefined , callbacks ) :
337+ null ) ;
323338
324339 this [ kChunkedBuffer ] . length = 0 ;
325340 this [ kChunkedLength ] = 0 ;
@@ -381,14 +396,14 @@ OutgoingMessage.prototype._send = function _send(data, encoding, callback, byteL
381396 // This is a shameful hack to get the headers and first body chunk onto
382397 // the same packet. Future versions of Node are going to take care of
383398 // this at a lower level and in a more general way.
384- if ( ! this . _headerSent && this . _header !== null ) {
399+ let header ;
400+ if ( ! this . _headerSent && ( header = this . _header ) !== null ) {
385401 // `this._header` can be null if OutgoingMessage is used without a proper Socket
386402 // See: /test/parallel/test-http-outgoing-message-inheritance.js
387403 if ( typeof data === 'string' &&
388404 ( encoding === 'utf8' || encoding === 'latin1' || ! encoding ) ) {
389- data = this . _header + data ;
405+ data = header + data ;
390406 } else {
391- const header = this . _header ;
392407 this . outputData . unshift ( {
393408 data : header ,
394409 encoding : 'latin1' ,
@@ -454,17 +469,21 @@ function _storeHeader(firstLine, headers) {
454469 processHeader ( this , state , entry [ 0 ] , entry [ 1 ] , false , lenient ) ;
455470 }
456471 } else if ( ArrayIsArray ( headers ) ) {
457- if ( headers . length && ArrayIsArray ( headers [ 0 ] ) ) {
458- for ( let i = 0 ; i < headers . length ; i ++ ) {
472+ // The length is hoisted into a local because the engine cannot fold
473+ // the reload across the processHeader calls; the array is never
474+ // mutated while this loop runs.
475+ const headersLength = headers . length ;
476+ if ( headersLength && ArrayIsArray ( headers [ 0 ] ) ) {
477+ for ( let i = 0 ; i < headersLength ; i ++ ) {
459478 const entry = headers [ i ] ;
460479 processHeader ( this , state , entry [ 0 ] , entry [ 1 ] , true , lenient ) ;
461480 }
462481 } else {
463- if ( headers . length % 2 !== 0 ) {
482+ if ( headersLength % 2 !== 0 ) {
464483 throw new ERR_INVALID_ARG_VALUE ( 'headers' , headers ) ;
465484 }
466485
467- for ( let n = 0 ; n < headers . length ; n += 2 ) {
486+ for ( let n = 0 ; n < headersLength ; n += 2 ) {
468487 processHeader ( this , state , headers [ n + 0 ] , headers [ n + 1 ] , true , lenient ) ;
469488 }
470489 }
@@ -515,11 +534,13 @@ function _storeHeader(firstLine, headers) {
515534 header += 'Connection: close\r\n' ;
516535 } else if ( shouldSendKeepAlive ) {
517536 header += 'Connection: keep-alive\r\n' ;
518- if ( this . _keepAliveTimeout && this . _defaultKeepAlive ) {
519- const timeoutSeconds = MathFloor ( this . _keepAliveTimeout / 1000 ) ;
537+ const keepAliveTimeout = this . _keepAliveTimeout ;
538+ if ( keepAliveTimeout && this . _defaultKeepAlive ) {
539+ const timeoutSeconds = MathFloor ( keepAliveTimeout / 1000 ) ;
540+ const maxRequestsPerSocket = this . _maxRequestsPerSocket ;
520541 let max = '' ;
521- if ( ~ ~ this . _maxRequestsPerSocket > 0 ) {
522- max = `, max=${ this . _maxRequestsPerSocket } ` ;
542+ if ( ~ ~ maxRequestsPerSocket > 0 ) {
543+ max = `, max=${ maxRequestsPerSocket } ` ;
523544 }
524545 header += `Keep-Alive: timeout=${ timeoutSeconds } ${ max } \r\n` ;
525546 }
@@ -529,6 +550,7 @@ function _storeHeader(firstLine, headers) {
529550 }
530551 }
531552
553+ let contentLength ;
532554 if ( ! state . contLen && ! state . te ) {
533555 if ( ! this . _hasBody ) {
534556 // Make sure we don't end the 0\r\n\r\n at the end of the message.
@@ -537,8 +559,8 @@ function _storeHeader(firstLine, headers) {
537559 this . _last = true ;
538560 } else if ( ! state . trailer &&
539561 ! this . _removedContLen &&
540- typeof this . _contentLength === 'number' ) {
541- header += 'Content-Length: ' + this . _contentLength + '\r\n' ;
562+ typeof ( contentLength = this . _contentLength ) === 'number' ) {
563+ header += 'Content-Length: ' + contentLength + '\r\n' ;
542564 } else if ( ! this . _removedTE ) {
543565 header += 'Transfer-Encoding: chunked\r\n' ;
544566 this . chunkedEncoding = true ;
@@ -590,13 +612,14 @@ function processHeader(self, state, key, value, validate, lenient) {
590612 }
591613
592614 if ( ArrayIsArray ( value ) ) {
615+ const valueLength = value . length ;
593616 if (
594- ( value . length < 2 || ! isCookieField ( key ) ) &&
617+ ( valueLength < 2 || ! isCookieField ( key ) ) &&
595618 ( ! self [ kUniqueHeaders ] || ! self [ kUniqueHeaders ] . has ( key . toLowerCase ( ) ) )
596619 ) {
597620 // Retain for(;;) loop for performance reasons
598621 // Refs: https://github.com/nodejs/node/pull/30958
599- for ( let i = 0 ; i < value . length ; i ++ )
622+ for ( let i = 0 ; i < valueLength ; i ++ )
600623 storeHeader ( self , state , key , value [ i ] , validate , lenient ) ;
601624 return ;
602625 }
@@ -613,8 +636,23 @@ function storeHeader(self, state, key, value, validate, lenient) {
613636}
614637
615638function matchHeader ( self , state , field , value ) {
616- if ( field . length < 4 || field . length > 17 )
617- return ;
639+ const len = field . length ;
640+ // Cheap (length, first character) pre-filter so the common case, a
641+ // header that is not one of the eight known fields below, returns
642+ // without paying the per-header toLowerCase() allocation. The filter
643+ // only rejects names that cannot possibly match the switch: `| 0x20`
644+ // lower-cases ASCII letters and maps no other token character onto a
645+ // letter, and every known field length is enumerated.
646+ const c = field . charCodeAt ( 0 ) | 0x20 ;
647+ switch ( len ) {
648+ case 4 : if ( c !== 0x64 ) return ; break ; // Date
649+ case 6 : if ( c !== 0x65 ) return ; break ; // Expect
650+ case 7 : if ( c !== 0x74 ) return ; break ; // Trailer
651+ case 10 : if ( c !== 0x63 && c !== 0x6b ) return ; break ; // Connection, Keep-Alive
652+ case 14 : if ( c !== 0x63 ) return ; break ; // Content-Length
653+ case 17 : if ( c !== 0x74 ) return ; break ; // Transfer-Encoding
654+ default : return ; // No known field has this length
655+ }
618656 field = field . toLowerCase ( ) ;
619657 switch ( field ) {
620658 case 'connection' :
@@ -997,9 +1035,12 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
9971035 }
9981036 }
9991037
1000- if ( ! fromEnd && msg . socket && ! msg . socket . writableCorked ) {
1001- msg . socket . cork ( ) ;
1002- process . nextTick ( connectionCorkNT , msg . socket ) ;
1038+ // `socket` is an accessor on the prototype: load it once instead of
1039+ // paying the getter three times on every body write.
1040+ let socket ;
1041+ if ( ! fromEnd && ( socket = msg . socket ) && ! socket . writableCorked ) {
1042+ socket . cork ( ) ;
1043+ process . nextTick ( connectionCorkNT , socket ) ;
10031044 }
10041045
10051046 let ret ;
@@ -1028,6 +1069,12 @@ function connectionCorkNT(conn) {
10281069 conn . uncork ( ) ;
10291070}
10301071
1072+ function runChunkCallbacks ( callbacks , err ) {
1073+ for ( let i = 0 ; i < callbacks . length ; i ++ ) {
1074+ callbacks [ i ] ( err ) ;
1075+ }
1076+ }
1077+
10311078OutgoingMessage . prototype . addTrailers = function addTrailers ( headers ) {
10321079 if ( this . finished ) {
10331080 throw new ERR_HTTP_HEADERS_SENT ( 'set trailing' ) ;
@@ -1036,6 +1083,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
10361083 this . _trailer = '' ;
10371084 const keys = ObjectKeys ( headers ) ;
10381085 const isArray = ArrayIsArray ( headers ) ;
1086+ const lenient = this . _isLenientHeaderValidation ( ) ;
10391087 // Retain for(;;) loop for performance reasons
10401088 // Refs: https://github.com/nodejs/node/pull/30958
10411089 for ( let i = 0 , l = keys . length ; i < l ; i ++ ) {
@@ -1052,7 +1100,6 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) {
10521100
10531101 // Check if the field must be sent several times
10541102 const isArrayValue = ArrayIsArray ( value ) ;
1055- const lenient = this . _isLenientHeaderValidation ( ) ;
10561103 if (
10571104 isArrayValue && value . length > 1 &&
10581105 ( ! this [ kUniqueHeaders ] || ! this [ kUniqueHeaders ] . has ( field . toLowerCase ( ) ) )
0 commit comments