@@ -94,21 +94,22 @@ const kFlushError = Symbol('kFlushError');
9494const kHighWaterMark = Symbol ( 'kHighWaterMark' ) ;
9595const kRejectNonStandardBodyWrites = Symbol ( 'kRejectNonStandardBodyWrites' ) ;
9696const kLenientHeaderValidation = Symbol ( 'kLenientHeaderValidation' ) ;
97+ const kChunkedEndSent = Symbol ( 'kChunkedEndSent' ) ;
9798const kMaxHeaderBodyCoalesce = 16 * 1024 ;
9899
99100const nop = ( ) => { } ;
100101
101102const RE_CONN_CLOSE = / (?: ^ | \W ) c l o s e (?: $ | \W ) / i;
102103
103- // isCookieField performs a case-insensitive comparison of a provided string
104- // against the word "cookie." As of V8 6.6 this is faster than handrolling or
105- // using a case-insensitive RegExp.
106104function isCookieField ( s ) {
107- return s . length === 6 && s . toLowerCase ( ) === 'cookie' ;
105+ return s . length === 6 &&
106+ ( s === 'Cookie' || s === 'cookie' || s . toLowerCase ( ) === 'cookie' ) ;
108107}
109108
110109function isContentDispositionField ( s ) {
111- return s . length === 19 && s . toLowerCase ( ) === 'content-disposition' ;
110+ return s . length === 19 &&
111+ ( s === 'Content-Disposition' || s === 'content-disposition' ||
112+ s . toLowerCase ( ) === 'content-disposition' ) ;
112113}
113114
114115function OutgoingMessage ( options ) {
@@ -637,24 +638,80 @@ function storeHeader(self, state, key, value, validate, lenient) {
637638 matchHeader ( self , state , key , value ) ;
638639}
639640
641+ function matchConnection ( self , state , value ) {
642+ state . connection = true ;
643+ self . _removedConnection = false ;
644+ if ( RE_CONN_CLOSE . test ( value ) )
645+ self . _last = true ;
646+ else
647+ self . shouldKeepAlive = true ;
648+ }
649+
650+ function matchTransferEncoding ( self , state , value ) {
651+ state . te = true ;
652+ self . _removedTE = false ;
653+ if ( RE_TE_CHUNKED . test ( value ) )
654+ self . chunkedEncoding = true ;
655+ }
656+
640657function matchHeader ( self , state , field , value ) {
641- if ( field . length < 4 || field . length > 17 )
658+ const len = field . length ;
659+ if ( len < 4 || len > 17 )
642660 return ;
643- field = field . toLowerCase ( ) ;
644- switch ( field ) {
661+
662+ // Avoid toLowerCase() for the usual Title-Case / lowercase spellings.
663+ switch ( len ) {
664+ case 4 :
665+ if ( field === 'Date' || field === 'date' ) {
666+ state . date = true ;
667+ return ;
668+ }
669+ break ;
670+ case 6 :
671+ if ( field === 'Expect' || field === 'expect' ) {
672+ state . expect = true ;
673+ return ;
674+ }
675+ break ;
676+ case 7 :
677+ if ( field === 'Trailer' || field === 'trailer' ) {
678+ state . trailer = true ;
679+ return ;
680+ }
681+ break ;
682+ case 10 :
683+ if ( field === 'Connection' || field === 'connection' ) {
684+ matchConnection ( self , state , value ) ;
685+ return ;
686+ }
687+ if ( field === 'Keep-Alive' || field === 'keep-alive' ) {
688+ self . _defaultKeepAlive = false ;
689+ return ;
690+ }
691+ break ;
692+ case 14 :
693+ if ( field === 'Content-Length' || field === 'content-length' ) {
694+ state . contLen = true ;
695+ self . _contentLength = + value ;
696+ self . _removedContLen = false ;
697+ return ;
698+ }
699+ break ;
700+ case 17 :
701+ if ( field === 'Transfer-Encoding' || field === 'transfer-encoding' ) {
702+ matchTransferEncoding ( self , state , value ) ;
703+ return ;
704+ }
705+ break ;
706+ }
707+
708+ const lower = field . toLowerCase ( ) ;
709+ switch ( lower ) {
645710 case 'connection' :
646- state . connection = true ;
647- self . _removedConnection = false ;
648- if ( RE_CONN_CLOSE . test ( value ) )
649- self . _last = true ;
650- else
651- self . shouldKeepAlive = true ;
711+ matchConnection ( self , state , value ) ;
652712 break ;
653713 case 'transfer-encoding' :
654- state . te = true ;
655- self . _removedTE = false ;
656- if ( RE_TE_CHUNKED . test ( value ) )
657- self . chunkedEncoding = true ;
714+ matchTransferEncoding ( self , state , value ) ;
658715 break ;
659716 case 'content-length' :
660717 state . contLen = true ;
@@ -664,7 +721,7 @@ function matchHeader(self, state, field, value) {
664721 case 'date' :
665722 case 'expect' :
666723 case 'trailer' :
667- state [ field ] = true ;
724+ state [ lower ] = true ;
668725 break ;
669726 case 'keep-alive' :
670727 self . _defaultKeepAlive = false ;
@@ -917,6 +974,37 @@ ObjectDefineProperty(OutgoingMessage.prototype, 'writableNeedDrain', {
917974} ) ;
918975
919976const crlf_buf = Buffer . from ( '\r\n' ) ;
977+
978+ // One write() for headers + last chunk + terminator. Used by end(chunk)
979+ // when Transfer-Encoding is chunked and headers have not been flushed.
980+ function trySendCombinedChunkedEnd ( msg , chunk , encoding , len , callback ) {
981+ if ( msg . _headerSent || msg . _header === null || msg [ kChunkedLength ] !== 0 )
982+ return undefined ;
983+
984+ const suffix = '\r\n0\r\n' + msg . _trailer + '\r\n' ;
985+ const hex = len . toString ( 16 ) ;
986+
987+ if ( typeof chunk === 'string' &&
988+ ( encoding === 'utf8' || encoding === 'latin1' || ! encoding ) ) {
989+ msg [ kChunkedEndSent ] = true ;
990+ return msg . _send ( hex + '\r\n' + chunk + suffix , encoding , callback ) ;
991+ }
992+
993+ if ( isUint8Array ( chunk ) && chunk . byteLength <= kMaxHeaderBodyCoalesce ) {
994+ const prefix = msg . _header + hex + '\r\n' ;
995+ const total = prefix . length + chunk . byteLength + suffix . length ;
996+ const combined = Buffer . allocUnsafe ( total ) ;
997+ combined . write ( prefix , 0 , prefix . length , 'latin1' ) ;
998+ combined . set ( chunk , prefix . length ) ;
999+ combined . write ( suffix , prefix . length + chunk . byteLength , suffix . length , 'latin1' ) ;
1000+ msg . _headerSent = true ;
1001+ msg [ kChunkedEndSent ] = true ;
1002+ return msg . _writeRaw ( combined , undefined , callback , total ) ;
1003+ }
1004+
1005+ return undefined ;
1006+ }
1007+
9201008OutgoingMessage . prototype . write = function write ( chunk , encoding , callback ) {
9211009 if ( typeof encoding === 'function' ) {
9221010 callback = encoding ;
@@ -962,8 +1050,14 @@ function strictContentLength(msg) {
9621050}
9631051
9641052function write_ ( msg , chunk , encoding , callback , fromEnd ) {
965- if ( typeof callback !== 'function' )
1053+ let endCallback ;
1054+ if ( fromEnd && typeof callback === 'function' ) {
1055+ // end() owns the finish callback unless the chunked fast path sends it.
1056+ endCallback = callback ;
1057+ callback = nop ;
1058+ } else if ( typeof callback !== 'function' ) {
9661059 callback = nop ;
1060+ }
9671061
9681062 if ( chunk === null ) {
9691063 throw new ERR_STREAM_NULL_VALUES ( ) ;
@@ -1030,6 +1124,12 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
10301124 let ret ;
10311125 if ( msg . chunkedEncoding && chunk . length !== 0 ) {
10321126 len ??= typeof chunk === 'string' ? Buffer . byteLength ( chunk , encoding ) : chunk . byteLength ;
1127+ if ( fromEnd && ! msg . strictContentLength ) {
1128+ const combined = trySendCombinedChunkedEnd (
1129+ msg , chunk , encoding , len , endCallback ?? nop ) ;
1130+ if ( combined !== undefined )
1131+ return combined ;
1132+ }
10331133 if ( msg [ kCorked ] && msg . _headerSent ) {
10341134 msg [ kChunkedBuffer ] . push ( chunk , encoding , callback ) ;
10351135 msg [ kChunkedLength ] += len ;
@@ -1170,7 +1270,7 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11701270 this [ kSocket ] . cork ( ) ;
11711271 }
11721272
1173- write_ ( this , chunk , encoding , null , true ) ;
1273+ write_ ( this , chunk , encoding , onFinish . bind ( undefined , this ) , true ) ;
11741274 } else if ( this . finished ) {
11751275 if ( typeof callback === 'function' ) {
11761276 queueEndCallback ( this , callback ) ;
@@ -1192,14 +1292,18 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
11921292 throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH ( this [ kBytesWritten ] , this . _contentLength ) ;
11931293 }
11941294
1195- const finish = onFinish . bind ( undefined , this ) ;
1196-
1197- if ( this . _hasBody && this . chunkedEncoding ) {
1198- this . _send ( '0\r\n' + this . _trailer + '\r\n' , 'latin1' , finish ) ;
1199- } else if ( ! this . _headerSent || this . writableLength || chunk ) {
1200- this . _send ( '' , 'latin1' , finish ) ;
1295+ if ( this [ kChunkedEndSent ] ) {
1296+ // Headers, last chunk, and terminator were already flushed together.
12011297 } else {
1202- process . nextTick ( finish ) ;
1298+ const finish = onFinish . bind ( undefined , this ) ;
1299+
1300+ if ( this . _hasBody && this . chunkedEncoding ) {
1301+ this . _send ( '0\r\n' + this . _trailer + '\r\n' , 'latin1' , finish ) ;
1302+ } else if ( ! this . _headerSent || this . writableLength || chunk ) {
1303+ this . _send ( '' , 'latin1' , finish ) ;
1304+ } else {
1305+ process . nextTick ( finish ) ;
1306+ }
12031307 }
12041308
12051309 if ( this [ kSocket ] ) {
0 commit comments