Skip to content

Commit 9e1824d

Browse files
authored
http: correctly calculate strict content length
Fixes some logical errors related to strict content length. Also, previously Buffer.byteLength (which is slow) was run regardless of whether or not the len was required. PR-URL: #46601 Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Paolo Insogna <[email protected]> Reviewed-By: Yagiz Nizipli <[email protected]>
1 parent d0531eb commit 9e1824d

File tree

2 files changed

+39
-45
lines changed

2 files changed

+39
-45
lines changed

lib/_http_outgoing.js

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ const {
2525
Array,
2626
ArrayIsArray,
2727
ArrayPrototypeJoin,
28-
MathAbs,
2928
MathFloor,
3029
NumberPrototypeToString,
3130
ObjectDefineProperty,
@@ -86,7 +85,6 @@ const HIGH_WATER_MARK = getDefaultHighWaterMark();
8685
const kCorked = Symbol('corked');
8786
const kUniqueHeaders = Symbol('kUniqueHeaders');
8887
const kBytesWritten = Symbol('kBytesWritten');
89-
const kEndCalled = Symbol('kEndCalled');
9088
const kErrored = Symbol('errored');
9189

9290
const nop = () => {};
@@ -133,7 +131,6 @@ function OutgoingMessage() {
133131

134132
this.strictContentLength = false;
135133
this[kBytesWritten] = 0;
136-
this[kEndCalled] = false;
137134
this._contentLength = null;
138135
this._hasBody = true;
139136
this._trailer = '';
@@ -355,7 +352,7 @@ OutgoingMessage.prototype.destroy = function destroy(error) {
355352

356353

357354
// This abstract either writing directly to the socket or buffering it.
358-
OutgoingMessage.prototype._send = function _send(data, encoding, callback) {
355+
OutgoingMessage.prototype._send = function _send(data, encoding, callback, byteLength) {
359356
// This is a shameful hack to get the headers and first body chunk onto
360357
// the same packet. Future versions of Node are going to take care of
361358
// this at a lower level and in a more general way.
@@ -377,20 +374,11 @@ OutgoingMessage.prototype._send = function _send(data, encoding, callback) {
377374
}
378375
this._headerSent = true;
379376
}
380-
return this._writeRaw(data, encoding, callback);
377+
return this._writeRaw(data, encoding, callback, byteLength);
381378
};
382379

383-
function _getMessageBodySize(chunk, headers, encoding) {
384-
if (Buffer.isBuffer(chunk)) return chunk.length;
385-
const chunkLength = chunk ? Buffer.byteLength(chunk, encoding) : 0;
386-
const headerLength = headers ? headers.length : 0;
387-
if (headerLength === chunkLength) return 0;
388-
if (headerLength < chunkLength) return MathAbs(chunkLength - headerLength);
389-
return chunkLength;
390-
}
391-
392380
OutgoingMessage.prototype._writeRaw = _writeRaw;
393-
function _writeRaw(data, encoding, callback) {
381+
function _writeRaw(data, encoding, callback, size) {
394382
const conn = this.socket;
395383
if (conn && conn.destroyed) {
396384
// The socket was destroyed. If we're still trying to write to it,
@@ -403,25 +391,6 @@ function _writeRaw(data, encoding, callback) {
403391
encoding = null;
404392
}
405393

406-
// TODO(sidwebworks): flip the `strictContentLength` default to `true` in a future PR
407-
if (this.strictContentLength && conn && conn.writable && !this._removedContLen && this._hasBody) {
408-
const skip = conn._httpMessage.statusCode === 304 || (this.hasHeader('transfer-encoding') || this.chunkedEncoding);
409-
410-
if (typeof this._contentLength === 'number' && !skip) {
411-
const size = _getMessageBodySize(data, conn._httpMessage._header, encoding);
412-
413-
if ((size + this[kBytesWritten]) > this._contentLength) {
414-
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(size + this[kBytesWritten], this._contentLength);
415-
}
416-
417-
if (this[kEndCalled] && (size + this[kBytesWritten]) !== this._contentLength) {
418-
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(size + this[kBytesWritten], this._contentLength);
419-
}
420-
421-
this[kBytesWritten] += size;
422-
}
423-
}
424-
425394
if (conn && conn._httpMessage === this && conn.writable) {
426395
// There might be pending data in the this.output buffer.
427396
if (this.outputData.length) {
@@ -886,18 +855,24 @@ function emitErrorNt(msg, err, callback) {
886855
}
887856
}
888857

858+
function strictContentLength(msg) {
859+
return (
860+
msg.strictContentLength &&
861+
msg._contentLength != null &&
862+
msg._hasBody &&
863+
!msg._removedContLen &&
864+
!msg.chunkedEncoding &&
865+
!msg.hasHeader('transfer-encoding')
866+
);
867+
}
868+
889869
function write_(msg, chunk, encoding, callback, fromEnd) {
890870
if (typeof callback !== 'function')
891871
callback = nop;
892872

893-
let len;
894873
if (chunk === null) {
895874
throw new ERR_STREAM_NULL_VALUES();
896-
} else if (typeof chunk === 'string') {
897-
len = Buffer.byteLength(chunk, encoding);
898-
} else if (isUint8Array(chunk)) {
899-
len = chunk.length;
900-
} else {
875+
} else if (typeof chunk !== 'string' && !isUint8Array(chunk)) {
901876
throw new ERR_INVALID_ARG_TYPE(
902877
'chunk', ['string', 'Buffer', 'Uint8Array'], chunk);
903878
}
@@ -918,8 +893,24 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
918893
return false;
919894
}
920895

896+
let len;
897+
898+
if (msg.strictContentLength) {
899+
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
900+
901+
if (
902+
strictContentLength(msg) &&
903+
(fromEnd ? msg[kBytesWritten] + len !== msg._contentLength : msg[kBytesWritten] + len > msg._contentLength)
904+
) {
905+
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(len + msg[kBytesWritten], msg._contentLength);
906+
}
907+
908+
msg[kBytesWritten] += len;
909+
}
910+
921911
if (!msg._header) {
922912
if (fromEnd) {
913+
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
923914
msg._contentLength = len;
924915
}
925916
msg._implicitHeader();
@@ -939,12 +930,13 @@ function write_(msg, chunk, encoding, callback, fromEnd) {
939930

940931
let ret;
941932
if (msg.chunkedEncoding && chunk.length !== 0) {
933+
len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength;
942934
msg._send(NumberPrototypeToString(len, 16), 'latin1', null);
943935
msg._send(crlf_buf, null, null);
944-
msg._send(chunk, encoding, null);
936+
msg._send(chunk, encoding, null, len);
945937
ret = msg._send(crlf_buf, null, callback);
946938
} else {
947-
ret = msg._send(chunk, encoding, callback);
939+
ret = msg._send(chunk, encoding, callback, len);
948940
}
949941

950942
debug('write ret = ' + ret);
@@ -1016,8 +1008,6 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
10161008
encoding = null;
10171009
}
10181010

1019-
this[kEndCalled] = true;
1020-
10211011
if (chunk) {
10221012
if (this.finished) {
10231013
onError(this,
@@ -1052,6 +1042,10 @@ OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
10521042
if (typeof callback === 'function')
10531043
this.once('finish', callback);
10541044

1045+
if (strictContentLength(this) && this[kBytesWritten] !== this._contentLength) {
1046+
throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(this[kBytesWritten], this._contentLength);
1047+
}
1048+
10551049
const finish = onFinish.bind(undefined, this);
10561050

10571051
if (this._hasBody && this.chunkedEncoding) {

test/parallel/test-http-content-length-mismatch.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ function shouldThrowOnFewerBytes() {
5757
res.write('a');
5858
res.statusCode = 200;
5959
assert.throws(() => {
60-
res.end();
60+
res.end('aaa');
6161
}, {
6262
code: 'ERR_HTTP_CONTENT_LENGTH_MISMATCH'
6363
});

0 commit comments

Comments
 (0)