Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions benchmark/buffers/buffer-stringlength.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

const common = require('../common.js');
const { Buffer } = require('node:buffer');
const assert = require('node:assert');

const bench = common.createBenchmark(main, {
n: [1e6],
encoding: ['utf8', 'latin1', 'base64'],
len: [32, 4096, 1048576],
input: ['ascii', 'multibyte', 'invalid'],
});

function main({ n, encoding, len, input }) {
let buf;
if (input === 'ascii') {
buf = Buffer.alloc(len, 'a');
} else {
buf = Buffer.alloc(len - (len % 3), '€');
if (input === 'invalid') buf = Buffer.concat([buf, Buffer.from([0xE2, 0x82])]);
}
const expected = buf.toString(encoding).length;
bench.start();
for (let i = 0; i < n; ++i) {
assert.strictEqual(Buffer.stringLength(buf, encoding), expected);
}
bench.end(n);
}
54 changes: 54 additions & 0 deletions doc/api/buffer.md
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,59 @@ console.log(`${str}: ${str.length} characters, ` +
When `string` is a {Buffer|DataView|TypedArray|ArrayBuffer|SharedArrayBuffer},
the byte length as reported by `.byteLength` is returned.

### Static method: `Buffer.stringLength(input[, encoding])`

<!-- YAML
added: REPLACEME
-->

* `input` {Buffer | ArrayBuffer | TypedArray} The bytes that would be decoded.
* `encoding` {string} The character encoding `input` would be decoded with.
**Default:** `'utf8'`.
* Returns: {integer}

Returns the length, in UTF-16 code units, of the string that
`buf.toString(encoding)` would produce for the same bytes, without decoding
them. This is the counterpart of [`Buffer.byteLength()`][], which returns the
number of bytes a string would encode to.

For `'utf8'`, invalid byte sequences are counted as they would be decoded:
each maximal invalid subsequence becomes one `U+FFFD` replacement character.
For every other encoding the result is computed from `input.byteLength` alone.

A detached `ArrayBuffer`, or a `TypedArray` backed by one, is treated as empty.

The result is not capped: compare it with
[`buffer.constants.MAX_STRING_LENGTH`][] before decoding to know whether the
decode can succeed at all. A string of `n` code units occupies between `n` and
`2 * n` bytes of memory.

```mjs
import { Buffer, constants } from 'node:buffer';

const buf = Buffer.from('€ 100', 'utf8');

console.log(Buffer.stringLength(buf));
// Prints: 5
console.log(Buffer.stringLength(buf, 'hex'));
// Prints: 14
console.log(Buffer.stringLength(buf) <= constants.MAX_STRING_LENGTH);
// Prints: true
```

```cjs
const { Buffer, constants } = require('node:buffer');

const buf = Buffer.from('€ 100', 'utf8');

console.log(Buffer.stringLength(buf));
// Prints: 5
console.log(Buffer.stringLength(buf, 'hex'));
// Prints: 14
console.log(Buffer.stringLength(buf) <= constants.MAX_STRING_LENGTH);
// Prints: true
```

### Static method: `Buffer.compare(buf1, buf2)`

<!-- YAML
Expand Down Expand Up @@ -5726,6 +5779,7 @@ or after startup, if the alignment has to hold at run time.
[`Buffer.alloc()`]: #static-method-bufferallocsize-fill-encoding
[`Buffer.allocUnsafe()`]: #static-method-bufferallocunsafesize-alignment
[`Buffer.allocUnsafeSlow()`]: #static-method-bufferallocunsafeslowsize-alignment
[`Buffer.byteLength()`]: #static-method-bufferbytelengthstring-encoding
[`Buffer.concat()`]: #static-method-bufferconcatlist-totallength
[`Buffer.copyBytesFrom()`]: #static-method-buffercopybytesfromview-offset-length
[`Buffer.from(array)`]: #static-method-bufferfromarray
Expand Down
51 changes: 45 additions & 6 deletions lib/_http_incoming.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,15 @@ function IncomingMessage(socket) {
return deprecateInstantiation(IncomingMessage, 'DEP0195', socket);
}

let streamOptions;
Readable.call(this);

if (socket) {
streamOptions = {
highWaterMark: socket.readableHighWaterMark,
};
const hwm = socket.readableHighWaterMark;
if (this._readableState.highWaterMark !== hwm) {
this._readableState.highWaterMark = hwm;
}
}

Readable.call(this, streamOptions);

this._readableState.readingMore = true;

this.socket = socket;
Expand Down Expand Up @@ -530,6 +529,43 @@ IncomingMessage.prototype._dump = function _dump() {
}
};

// Case-insensitive ASCII compare against an already-lowercased name.
// Avoids allocating a lowercased copy of every header name.
function asciiEqualIgnoreCase(a, lower) {
const len = lower.length;
if (a.length !== len)
return false;
if (a === lower)
return true;
for (let i = 0; i < len; i++) {
let c = a.charCodeAt(i);
if (c >= 65 && c <= 90)
c += 32;
if (c !== lower.charCodeAt(i))
return false;
}
return true;
}

function getRawHeader(msg, lowerName, joinDuplicates) {
const rawHeaders = msg.rawHeaders;
const count = msg[kHeadersCount];
let result;
for (let i = 0; i < count; i += 2) {
if (!asciiEqualIgnoreCase(rawHeaders[i], lowerName))
continue;
const value = rawHeaders[i + 1];
if (result === undefined) {
result = value;
if (!joinDuplicates)
return result;
} else {
result += ', ' + value;
}
}
return result;
}

function onError(self, error, cb) {
// This is to keep backward compatible behavior.
// An error is emitted only if there are listeners attached to the event.
Expand All @@ -543,6 +579,9 @@ function onError(self, error, cb) {
module.exports = {
IncomingMessage,
kDetachAbortSignal,
kHeadersCount,
asciiEqualIgnoreCase,
getRawHeader,
readStart,
readStop,
};
80 changes: 64 additions & 16 deletions lib/_http_outgoing.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const { getDefaultHighWaterMark } = require('internal/streams/state');
const assert = require('internal/assert');
const EE = require('events');
const Stream = require('stream');
const { kOutHeaders, utcDate, kNeedDrain } = require('internal/http');
const { kOutHeaders, utcDateHeader, kNeedDrain } = require('internal/http');
const { Buffer } = require('buffer');
const {
_checkIsHttpToken: checkIsHttpToken,
Expand Down Expand Up @@ -89,6 +89,11 @@ const kChunkedLength = Symbol('kChunkedLength');
const kUniqueHeaders = Symbol('kUniqueHeaders');
const kBytesWritten = Symbol('kBytesWritten');
const kErrored = Symbol('errored');
const kLenientCache = Symbol('kLenientCache');

let keepAliveTimeoutCache = -1;
let keepAliveMaxCache = -1;
let keepAliveHeaderCache = '';
const kWritableFinished = Symbol('kWritableFinished');
const kEndCallbacks = Symbol('kEndCallbacks');
const kFlushError = Symbol('kFlushError');
Expand Down Expand Up @@ -169,6 +174,7 @@ function OutgoingMessage(options) {
this[kFlushError] = null;
this[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark();
this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false;
this[kLenientCache] = null;
}
ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype);
ObjectSetPrototypeOf(OutgoingMessage, Stream);
Expand All @@ -178,27 +184,34 @@ ObjectSetPrototypeOf(OutgoingMessage, Stream);
// For ServerResponse: checks the server's httpValidation or insecureHTTPParser
// Falls back to global --insecure-http-parser flag.
OutgoingMessage.prototype._isLenientHeaderValidation = function() {
// The underlying options cannot change during the lifetime of a message:
// compute the lookup chain only once per message.
this[kLenientCache] ??= isLenientHeaderValidation(this);
return this[kLenientCache];
};

function isLenientHeaderValidation(msg) {
// New httpValidation option takes priority (ClientRequest case)
if (this.httpValidation !== undefined) {
return this.httpValidation !== 'strict';
if (msg.httpValidation !== undefined) {
return msg.httpValidation !== 'strict';
}
// ServerResponse: check server's httpValidation option
const serverHttpValidation = this.req?.socket?.server?.httpValidation;
const serverHttpValidation = msg.req?.socket?.server?.httpValidation;
if (serverHttpValidation !== undefined) {
return serverHttpValidation !== 'strict';
}
// Legacy insecureHTTPParser - ClientRequest has it directly
if (typeof this.insecureHTTPParser === 'boolean') {
return this.insecureHTTPParser;
if (typeof msg.insecureHTTPParser === 'boolean') {
return msg.insecureHTTPParser;
}
// ServerResponse can access via req.socket.server
const serverOption = this.req?.socket?.server?.insecureHTTPParser;
const serverOption = msg.req?.socket?.server?.insecureHTTPParser;
if (typeof serverOption === 'boolean') {
return serverOption;
}
// Fall back to global option
return isLenient();
};
}

ObjectDefineProperty(OutgoingMessage.prototype, 'errored', {
__proto__: null,
Expand Down Expand Up @@ -508,7 +521,7 @@ function _storeHeader(firstLine, headers) {

// Date header
if (this.sendDate && !state.date) {
header += 'Date: ' + utcDate() + '\r\n';
header += utcDateHeader();
}

// Force the connection to close when the response is a 204 No Content or
Expand Down Expand Up @@ -541,14 +554,21 @@ function _storeHeader(firstLine, headers) {
if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) {
header += 'Connection: close\r\n';
} else if (shouldSendKeepAlive) {
header += 'Connection: keep-alive\r\n';
if (this._keepAliveTimeout && this._defaultKeepAlive) {
const timeoutSeconds = MathFloor(this._keepAliveTimeout / 1000);
let max = '';
if (~~this._maxRequestsPerSocket > 0) {
max = `, max=${this._maxRequestsPerSocket}`;
// The keep-alive header lines are identical for every response of a
// given server: cache the last rendered value.
const timeout = this._keepAliveTimeout;
const max = ~~this._maxRequestsPerSocket;
if (timeout !== keepAliveTimeoutCache || max !== keepAliveMaxCache) {
keepAliveTimeoutCache = timeout;
keepAliveMaxCache = max;
keepAliveHeaderCache = 'Connection: keep-alive\r\n' +
`Keep-Alive: timeout=${MathFloor(timeout / 1000)}` +
(max > 0 ? `, max=${max}` : '') + '\r\n';
}
header += `Keep-Alive: timeout=${timeoutSeconds}${max}\r\n`;
header += keepAliveHeaderCache;
} else {
header += 'Connection: keep-alive\r\n';
}
} else {
this._last = true;
Expand Down Expand Up @@ -639,10 +659,38 @@ function storeHeader(self, state, key, value, validate, lenient) {
matchHeader(self, state, key, value);
}

function lowerOutgoingHeaderName(field) {
switch (field) {
case 'Connection':
case 'connection':
return 'connection';
case 'Content-Length':
case 'content-length':
return 'content-length';
case 'Transfer-Encoding':
case 'transfer-encoding':
return 'transfer-encoding';
case 'Date':
case 'date':
return 'date';
case 'Expect':
case 'expect':
return 'expect';
case 'Trailer':
case 'trailer':
return 'trailer';
case 'Keep-Alive':
case 'keep-alive':
return 'keep-alive';
default:
return field.toLowerCase();
}
}

function matchHeader(self, state, field, value) {
if (field.length < 4 || field.length > 17)
return;
field = field.toLowerCase();
field = lowerOutgoingHeaderName(field);
switch (field) {
case 'connection':
state.connection = true;
Expand Down
Loading