diff --git a/benchmark/error/hidestackframes-noerr.js b/benchmark/error/hidestackframes-noerr.js new file mode 100644 index 00000000000000..9e662f6a620f13 --- /dev/null +++ b/benchmark/error/hidestackframes-noerr.js @@ -0,0 +1,62 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + type: [ + 'hide-stackframes', + 'direct-call', + ], + n: [1e7], +}, { + flags: ['--expose-internals'], +}); + +function main({ n, type }) { + const { + hideStackFrames, + codes: { + ERR_INVALID_ARG_TYPE, + }, + } = require('internal/errors'); + + const testfn = (value) => { + if (typeof value !== 'number') { + throw new ERR_INVALID_ARG_TYPE('Benchmark', 'number', value); + } + }; + + const hideStackFramesTestfn = hideStackFrames((value) => { + if (typeof value !== 'number') { + throw new ERR_INVALID_ARG_TYPE.HideStackFrameError('Benchmark', 'number', value); + } + }); + + const fn = type === 'hide-stackframe' ? hideStackFramesTestfn : testfn; + + const value = 42; + + const length = 1024; + const array = []; + const errCase = false; + + for (let i = 0; i < length; ++i) { + array.push(fn(value)); + } + + bench.start(); + + for (let i = 0; i < n; i++) { + const index = i % length; + array[index] = fn(value); + } + + bench.end(n); + + // Verify the entries to prevent dead code elimination from making + // the benchmark invalid. + for (let i = 0; i < length; ++i) { + assert.strictEqual(typeof array[i], errCase ? 'object' : 'undefined'); + } +} diff --git a/benchmark/error/hidestackframes-throw.js b/benchmark/error/hidestackframes-throw.js new file mode 100644 index 00000000000000..d395bb0ffb20e1 --- /dev/null +++ b/benchmark/error/hidestackframes-throw.js @@ -0,0 +1,72 @@ +'use strict'; + +const common = require('../common.js'); +const assert = require('assert'); + +const bench = common.createBenchmark(main, { + type: [ + 'hide-stackframes', + 'direct-call', + ], + n: [1e5], +}, { + flags: ['--expose-internals'], +}); + +function main({ n, type }) { + const { + hideStackFrames, + codes: { + ERR_INVALID_ARG_TYPE, + }, + } = require('internal/errors'); + + const value = 'err'; + + const testfn = (value) => { + if (typeof value !== 'number') { + throw new ERR_INVALID_ARG_TYPE('Benchmark', 'number', value); + } + }; + + const hideStackFramesTestfn = hideStackFrames((value) => { + if (typeof value !== 'number') { + throw new ERR_INVALID_ARG_TYPE.HideStackFrameError('Benchmark', 'number', value); + } + }); + + const fn = type === 'hide-stackframe' ? hideStackFramesTestfn : testfn; + + const length = 1024; + const array = []; + let errCase = false; + + // Warm up. + for (let i = 0; i < length; ++i) { + try { + fn(value); + } catch (e) { + errCase = true; + array.push(e); + } + } + + bench.start(); + + for (let i = 0; i < n; i++) { + const index = i % length; + try { + fn(value); + } catch (e) { + array[index] = e; + } + } + + bench.end(n); + + // Verify the entries to prevent dead code elimination from making + // the benchmark invalid. + for (let i = 0; i < length; ++i) { + assert.strictEqual(typeof array[i], errCase ? 'object' : 'undefined'); + } +} diff --git a/benchmark/error/hidestackframes.js b/benchmark/error/hidestackframes.js deleted file mode 100644 index b28be725a30969..00000000000000 --- a/benchmark/error/hidestackframes.js +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -const common = require('../common.js'); - -const bench = common.createBenchmark(main, { - type: ['hide-stackframes-throw', 'direct-call-throw', - 'hide-stackframes-noerr', 'direct-call-noerr'], - n: [10e4], -}, { - flags: ['--expose-internals'], -}); - -function main({ n, type }) { - const { - hideStackFrames, - codes: { - ERR_INVALID_ARG_TYPE, - }, - } = require('internal/errors'); - - const testfn = (value) => { - if (typeof value !== 'number') { - throw new ERR_INVALID_ARG_TYPE('Benchmark', 'number', value); - } - }; - - let fn = testfn; - if (type.startsWith('hide-stackframe')) - fn = hideStackFrames(testfn); - let value = 42; - if (type.endsWith('-throw')) - value = 'err'; - - bench.start(); - - for (let i = 0; i < n; i++) { - try { - fn(value); - } catch { - // No-op - } - } - - bench.end(n); -} diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 178a3418dace0a..3830b2a5c7b91a 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -619,17 +619,17 @@ function matchHeader(self, state, field, value) { const validateHeaderName = hideStackFrames((name, label) => { if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) { - throw new ERR_INVALID_HTTP_TOKEN(label || 'Header name', name); + throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError(label || 'Header name', name); } }); const validateHeaderValue = hideStackFrames((name, value) => { if (value === undefined) { - throw new ERR_HTTP_INVALID_HEADER_VALUE(value, name); + throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name); } if (checkInvalidHeaderChar(value)) { debug('Header "%s" contains invalid characters', name); - throw new ERR_INVALID_CHAR('header content', name); + throw new ERR_INVALID_CHAR.HideStackFramesError('header content', name); } }); diff --git a/lib/buffer.js b/lib/buffer.js index 0ff7c1920adbba..cfc6d5b21febc3 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -108,7 +108,6 @@ const { ERR_UNKNOWN_ENCODING, }, genericNodeError, - hideStackFrames, } = require('internal/errors'); const { validateArray, @@ -386,19 +385,12 @@ Buffer.of = of; ObjectSetPrototypeOf(Buffer, Uint8Array); -// The 'assertSize' method will remove itself from the callstack when an error -// occurs. This is done simply to keep the internal details of the -// implementation from bleeding out to users. -const assertSize = hideStackFrames((size) => { - validateNumber(size, 'size', 0, kMaxLength); -}); - /** * Creates a new filled Buffer instance. * alloc(size[, fill[, encoding]]) */ Buffer.alloc = function alloc(size, fill, encoding) { - assertSize(size); + validateNumber(size, 'size', 0, kMaxLength); if (fill !== undefined && fill !== 0 && size > 0) { const buf = createUnsafeBuffer(size); return _fill(buf, fill, 0, buf.length, encoding); @@ -411,7 +403,7 @@ Buffer.alloc = function alloc(size, fill, encoding) { * instance. If `--zero-fill-buffers` is set, will zero-fill the buffer. */ Buffer.allocUnsafe = function allocUnsafe(size) { - assertSize(size); + validateNumber(size, 'size', 0, kMaxLength); return allocate(size); }; @@ -421,15 +413,15 @@ Buffer.allocUnsafe = function allocUnsafe(size) { * If `--zero-fill-buffers` is set, will zero-fill the buffer. */ Buffer.allocUnsafeSlow = function allocUnsafeSlow(size) { - assertSize(size); + validateNumber(size, 'size', 0, kMaxLength); return createUnsafeBuffer(size); }; // If --zero-fill-buffers command line argument is set, a zero-filled // buffer is returned. -function SlowBuffer(length) { - assertSize(length); - return createUnsafeBuffer(length); +function SlowBuffer(size) { + validateNumber(size, 'size', 0, kMaxLength); + return createUnsafeBuffer(size); } ObjectSetPrototypeOf(SlowBuffer.prototype, Uint8ArrayPrototype); diff --git a/lib/internal/errors.js b/lib/internal/errors.js index d577f8efbd63ff..19d050fc857d6f 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -87,7 +87,6 @@ const MainContextError = Error; const overrideStackTrace = new SafeWeakMap(); const kNoOverride = Symbol('kNoOverride'); let userStackTraceLimit; -const nodeInternalPrefix = '__node_internal_'; const prepareStackTrace = (globalThis, error, trace) => { // API for node internals to override error stack formatting // without interfering with userland code. @@ -97,21 +96,6 @@ const prepareStackTrace = (globalThis, error, trace) => { return f(error, trace); } - const firstFrame = trace[0]?.getFunctionName(); - if (firstFrame && StringPrototypeStartsWith(firstFrame, nodeInternalPrefix)) { - for (let l = trace.length - 1; l >= 0; l--) { - const fn = trace[l]?.getFunctionName(); - if (fn && StringPrototypeStartsWith(fn, nodeInternalPrefix)) { - ArrayPrototypeSplice(trace, 0, l + 1); - break; - } - } - // `userStackTraceLimit` is the user value for `Error.stackTraceLimit`, - // it is updated at every new exception in `captureLargerStackTrace`. - if (trace.length > userStackTraceLimit) - ArrayPrototypeSplice(trace, userStackTraceLimit); - } - const globalOverride = maybeOverridePrepareStackTrace(globalThis, error, trace); if (globalOverride !== kNoOverride) return globalOverride; @@ -372,6 +356,23 @@ function makeSystemErrorWithCode(key) { }; } +function makeNodeErrorForHideStackFrame(Base) { + class HideStackFramesError extends Base { + constructor(...args) { + if (isErrorStackTraceLimitWritable()) { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + super(...args); + Error.stackTraceLimit = limit; + } else { + super(...args); + } + } + } + + return HideStackFramesError; +} + function makeNodeErrorWithCode(Base, key) { const msg = messages.get(key); const expectedLength = typeof msg !== 'string' ? -1 : getExpectedArgumentLength(msg); @@ -479,11 +480,14 @@ function makeNodeErrorWithCode(Base, key) { * @returns {T} */ function hideStackFrames(fn) { - // We rename the functions that will be hidden to cut off the stacktrace - // at the outermost one - const hidden = nodeInternalPrefix + fn.name; - ObjectDefineProperty(fn, 'name', { __proto__: null, value: hidden }); - return fn; + return function wrappedFn() { + try { + return ReflectApply(fn, fn, arguments); + } catch (error) { + Error.stackTraceLimit && ErrorCaptureStackTrace(error, wrappedFn); + throw error; + } + }; } // Utility function for registering the error codes. Only used here. Exported @@ -503,6 +507,9 @@ function E(sym, val, def, ...otherClasses) { def[clazz.name] = makeNodeErrorWithCode(clazz, sym); }); } + + def.HideStackFramesError = makeNodeErrorForHideStackFrame(def); + codes[sym] = def; } diff --git a/lib/internal/fs/utils.js b/lib/internal/fs/utils.js index f84133296e86fb..8aaacfa805c011 100644 --- a/lib/internal/fs/utils.js +++ b/lib/internal/fs/utils.js @@ -360,30 +360,6 @@ function handleErrorFromBinding(ctx) { } } -// Check if the path contains null types if it is a string nor Uint8Array, -// otherwise return silently. -const nullCheck = hideStackFrames((path, propName, throwError = true) => { - const pathIsString = typeof path === 'string'; - const pathIsUint8Array = isUint8Array(path); - - // We can only perform meaningful checks on strings and Uint8Arrays. - if ((!pathIsString && !pathIsUint8Array) || - (pathIsString && !StringPrototypeIncludes(path, '\u0000')) || - (pathIsUint8Array && !TypedArrayPrototypeIncludes(path, 0))) { - return; - } - - const err = new ERR_INVALID_ARG_VALUE( - propName, - path, - 'must be a string, Uint8Array, or URL without null bytes', - ); - if (throwError) { - throw err; - } - return err; -}); - function preprocessSymlinkDestination(path, type, linkPath) { if (!isWindows) { // No preprocessing is needed on Unix. @@ -695,14 +671,24 @@ const validateOffsetLengthWrite = hideStackFrames( const validatePath = hideStackFrames((path, propName = 'path') => { if (typeof path !== 'string' && !isUint8Array(path)) { - throw new ERR_INVALID_ARG_TYPE(propName, ['string', 'Buffer', 'URL'], path); + throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(propName, ['string', 'Buffer', 'URL'], path); } - const err = nullCheck(path, propName, false); + const pathIsString = typeof path === 'string'; + const pathIsUint8Array = isUint8Array(path); - if (err !== undefined) { - throw err; + // We can only perform meaningful checks on strings and Uint8Arrays. + if ((!pathIsString && !pathIsUint8Array) || + (pathIsString && !StringPrototypeIncludes(path, '\u0000')) || + (pathIsUint8Array && !TypedArrayPrototypeIncludes(path, 0))) { + return; } + + throw new ERR_INVALID_ARG_VALUE.HideStackFramesError( + propName, + path, + 'must be a string, Uint8Array, or URL without null bytes', + ); }); // TODO(rafaelgss): implement the path.resolve on C++ side @@ -737,11 +723,11 @@ const getValidatedFd = hideStackFrames((fd, propName = 'fd') => { const validateBufferArray = hideStackFrames((buffers, propName = 'buffers') => { if (!ArrayIsArray(buffers)) - throw new ERR_INVALID_ARG_TYPE(propName, 'ArrayBufferView[]', buffers); + throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(propName, 'ArrayBufferView[]', buffers); for (let i = 0; i < buffers.length; i++) { if (!isArrayBufferView(buffers[i])) - throw new ERR_INVALID_ARG_TYPE(propName, 'ArrayBufferView[]', buffers); + throw new ERR_INVALID_ARG_TYPE.HideStackFramesError(propName, 'ArrayBufferView[]', buffers); } return buffers; @@ -797,7 +783,7 @@ const validateCpOptions = hideStackFrames((options) => { validateBoolean(options.verbatimSymlinks, 'options.verbatimSymlinks'); options.mode = getValidMode(options.mode, 'copyFile'); if (options.dereference === true && options.verbatimSymlinks === true) { - throw new ERR_INCOMPATIBLE_OPTION_PAIR('dereference', 'verbatimSymlinks'); + throw new ERR_INCOMPATIBLE_OPTION_PAIR.HideStackFramesError('dereference', 'verbatimSymlinks'); } if (options.filter !== undefined) { validateFunction(options.filter, 'options.filter'); @@ -822,13 +808,16 @@ const validateRmOptions = hideStackFrames((path, options, expectDir, cb) => { } if (stats.isDirectory() && !options.recursive) { - return cb(new ERR_FS_EISDIR({ + const err = new ERR_FS_EISDIR.HideStackFramesError({ code: 'EISDIR', message: 'is a directory', path, syscall: 'rm', errno: EISDIR, - })); + }); + ErrorCaptureStackTrace(err, validateRmOptions); + + return cb(err); } return cb(null, options); }); @@ -908,7 +897,7 @@ const getValidMode = hideStackFrames((mode, type) => { const validateStringAfterArrayBufferView = hideStackFrames((buffer, name) => { if (typeof buffer !== 'string') { - throw new ERR_INVALID_ARG_TYPE( + throw new ERR_INVALID_ARG_TYPE.HideStackFramesError( name, ['string', 'Buffer', 'TypedArray', 'DataView'], buffer, @@ -951,7 +940,6 @@ module.exports = { getValidatedPath, getValidMode, handleErrorFromBinding, - nullCheck, possiblyTransformPath, preprocessSymlinkDestination, realpathCacheKey: Symbol('realpathCacheKey'), diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index 2ab9c70ccd7402..7bf079900c652f 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -89,13 +89,13 @@ const assertValidHeader = hideStackFrames((name, value) => { if (name === '' || typeof name !== 'string' || StringPrototypeIncludes(name, ' ')) { - throw new ERR_INVALID_HTTP_TOKEN('Header name', name); + throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError('Header name', name); } if (isPseudoHeader(name)) { - throw new ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED(); + throw new ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED.HideStackFramesError(); } if (value === undefined || value === null) { - throw new ERR_HTTP2_INVALID_HEADER_VALUE(value, name); + throw new ERR_HTTP2_INVALID_HEADER_VALUE.HideStackFramesError(value, name); } if (!isConnectionHeaderAllowed(name, value)) { connectionHeaderMessageWarn(); diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index 6d4a7f94b3d11a..f78187ef5a6b57 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -23,7 +23,7 @@ const { codes: { ERR_HTTP2_HEADER_SINGLE_VALUE, ERR_HTTP2_INVALID_CONNECTION_HEADERS, - ERR_HTTP2_INVALID_PSEUDOHEADER, + ERR_HTTP2_INVALID_PSEUDOHEADER: { HideStackFramesError: ERR_HTTP2_INVALID_PSEUDOHEADER }, ERR_HTTP2_INVALID_SETTING_VALUE, ERR_INVALID_ARG_TYPE, ERR_INVALID_HTTP_TOKEN, diff --git a/lib/internal/util.js b/lib/internal/util.js index 558a5da69773bb..4149840b244adc 100644 --- a/lib/internal/util.js +++ b/lib/internal/util.js @@ -8,6 +8,7 @@ const { ArrayPrototypeSlice, ArrayPrototypeSort, Error, + ErrorCaptureStackTrace, FunctionPrototypeCall, ObjectDefineProperties, ObjectDefineProperty, @@ -44,11 +45,11 @@ const { } = primordials; const { - hideStackFrames, codes: { ERR_NO_CRYPTO, ERR_UNKNOWN_SIGNAL, }, + isErrorStackTraceLimitWritable, uvErrmapGet, overrideStackTrace, } = require('internal/errors'); @@ -693,10 +694,19 @@ const lazyDOMExceptionClass = () => { return _DOMException; }; -const lazyDOMException = hideStackFrames((message, name) => { +const lazyDOMException = (message, name) => { _DOMException ??= internalBinding('messaging').DOMException; + if (isErrorStackTraceLimitWritable()) { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + const ex = new _DOMException(message, name); + Error.stackTraceLimit = limit; + ErrorCaptureStackTrace(ex, lazyDOMException); + return ex; + } return new _DOMException(message, name); -}); + +}; const kEnumerableProperty = { __proto__: null }; kEnumerableProperty.enumerable = true; diff --git a/lib/internal/validators.js b/lib/internal/validators.js index c73cb213ebe72b..0879724f0dd4b7 100644 --- a/lib/internal/validators.js +++ b/lib/internal/validators.js @@ -22,11 +22,11 @@ const { const { hideStackFrames, codes: { - ERR_SOCKET_BAD_PORT, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - ERR_OUT_OF_RANGE, - ERR_UNKNOWN_SIGNAL, + ERR_SOCKET_BAD_PORT: { HideStackFramesError: ERR_SOCKET_BAD_PORT }, + ERR_INVALID_ARG_TYPE: { HideStackFramesError: ERR_INVALID_ARG_TYPE }, + ERR_INVALID_ARG_VALUE: { HideStackFramesError: ERR_INVALID_ARG_VALUE }, + ERR_OUT_OF_RANGE: { HideStackFramesError: ERR_OUT_OF_RANGE }, + ERR_UNKNOWN_SIGNAL: { HideStackFramesError: ERR_UNKNOWN_SIGNAL }, }, } = require('internal/errors'); const { normalizeEncoding } = require('internal/util'); @@ -157,10 +157,10 @@ const validateUint32 = hideStackFrames((value, name, positive = false) => { */ /** @type {validateString} */ -function validateString(value, name) { +const validateString = hideStackFrames((value, name) => { if (typeof value !== 'string') throw new ERR_INVALID_ARG_TYPE(name, 'string', value); -} +}); /** * @callback validateNumber @@ -172,7 +172,7 @@ function validateString(value, name) { */ /** @type {validateNumber} */ -function validateNumber(value, name, min = undefined, max) { +const validateNumber = hideStackFrames((value, name, min = undefined, max) => { if (typeof value !== 'number') throw new ERR_INVALID_ARG_TYPE(name, 'number', value); @@ -183,7 +183,7 @@ function validateNumber(value, name, min = undefined, max) { `${min != null ? `>= ${min}` : ''}${min != null && max != null ? ' && ' : ''}${max != null ? `<= ${max}` : ''}`, value); } -} +}); /** * @callback validateOneOf @@ -213,10 +213,10 @@ const validateOneOf = hideStackFrames((value, name, oneOf) => { */ /** @type {validateBoolean} */ -function validateBoolean(value, name) { +const validateBoolean = hideStackFrames((value, name) => { if (typeof value !== 'boolean') throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value); -} +}); /** * @param {any} options @@ -302,12 +302,12 @@ const validateArray = hideStackFrames((value, name, minLength = 0) => { */ /** @type {validateStringArray} */ -function validateStringArray(value, name) { +const validateStringArray = hideStackFrames((value, name) => { validateArray(value, name); for (let i = 0; i < value.length; i++) { validateString(value[i], `${name}[${i}]`); } -} +}); /** * @callback validateBooleanArray @@ -317,12 +317,12 @@ function validateStringArray(value, name) { */ /** @type {validateBooleanArray} */ -function validateBooleanArray(value, name) { +const validateBooleanArray = hideStackFrames((value, name) => { validateArray(value, name); for (let i = 0; i < value.length; i++) { validateBoolean(value[i], `${name}[${i}]`); } -} +}); /** * @callback validateAbortSignalArray @@ -349,7 +349,7 @@ function validateAbortSignalArray(value, name) { * @param {string} [name='signal'] * @returns {asserts signal is keyof signals} */ -function validateSignalName(signal, name = 'signal') { +const validateSignalName = hideStackFrames((signal, name = 'signal') => { validateString(signal, name); if (signals[signal] === undefined) { @@ -360,7 +360,7 @@ function validateSignalName(signal, name = 'signal') { throw new ERR_UNKNOWN_SIGNAL(signal); } -} +}); /** * @callback validateBuffer @@ -382,7 +382,7 @@ const validateBuffer = hideStackFrames((buffer, name = 'buffer') => { * @param {string} data * @param {string} encoding */ -function validateEncoding(data, encoding) { +const validateEncoding = hideStackFrames((data, encoding) => { const normalizedEncoding = normalizeEncoding(encoding); const length = data.length; @@ -390,7 +390,7 @@ function validateEncoding(data, encoding) { throw new ERR_INVALID_ARG_VALUE('encoding', encoding, `is invalid for data of length ${length}`); } -} +}); /** * Check that the port number is not NaN when coerced to a number, @@ -400,7 +400,7 @@ function validateEncoding(data, encoding) { * @param {boolean} [allowZero=true] * @returns {number} */ -function validatePort(port, name = 'Port', allowZero = true) { +const validatePort = hideStackFrames((port, name = 'Port', allowZero = true) => { if ((typeof port !== 'number' && typeof port !== 'string') || (typeof port === 'string' && StringPrototypeTrim(port).length === 0) || +port !== (+port >>> 0) || @@ -409,7 +409,7 @@ function validatePort(port, name = 'Port', allowZero = true) { throw new ERR_SOCKET_BAD_PORT(name, port, allowZero); } return port | 0; -} +}); /** * @callback validateAbortSignal @@ -492,7 +492,7 @@ const linkValueRegExp = /^(?:<[^>]*>)(?:\s*;\s*[^;"\s]+(?:=(")?[^;"\s]*\1)?)*$/; * @param {any} value * @param {string} name */ -function validateLinkHeaderFormat(value, name) { +const validateLinkHeaderFormat = hideStackFrames((value, name) => { if ( typeof value === 'undefined' || !RegExpPrototypeExec(linkValueRegExp, value) @@ -503,7 +503,7 @@ function validateLinkHeaderFormat(value, name) { 'must be an array or string of format "; rel=preload; as=style"', ); } -} +}); const validateInternalField = hideStackFrames((object, fieldKey, className) => { if (typeof object !== 'object' || object === null || !ObjectPrototypeHasOwnProperty(object, fieldKey)) { @@ -515,7 +515,7 @@ const validateInternalField = hideStackFrames((object, fieldKey, className) => { * @param {any} hints * @return {string} */ -function validateLinkHeaderValue(hints) { +const validateLinkHeaderValue = hideStackFrames((hints) => { if (typeof hints === 'string') { validateLinkHeaderFormat(hints, 'hints'); return hints; @@ -545,7 +545,7 @@ function validateLinkHeaderValue(hints) { hints, 'must be an array or string of format "; rel=preload; as=style"', ); -} +}); module.exports = { isInt32, diff --git a/lib/os.js b/lib/os.js index 34391697b5891c..e77b7f35b8dda8 100644 --- a/lib/os.js +++ b/lib/os.js @@ -65,7 +65,7 @@ function getCheckedFunction(fn) { const ctx = {}; const ret = fn(...args, ctx); if (ret === undefined) { - throw new ERR_SYSTEM_ERROR(ctx); + throw new ERR_SYSTEM_ERROR.HideStackFramesError(ctx); } return ret; }); diff --git a/lib/util.js b/lib/util.js index 7fb7994e6536ba..26a95b2a3daeff 100644 --- a/lib/util.js +++ b/lib/util.js @@ -32,6 +32,7 @@ const { DatePrototypeGetMonth, DatePrototypeGetSeconds, Error, + ErrorCaptureStackTrace, FunctionPrototypeBind, NumberIsSafeInteger, ObjectDefineProperties, @@ -53,7 +54,6 @@ const { }, errnoException, exceptionWithHostPort, - hideStackFrames, } = require('internal/errors'); const { format, @@ -278,16 +278,17 @@ function _extend(target, source) { return target; } -const callbackifyOnRejected = hideStackFrames((reason, cb) => { +const callbackifyOnRejected = (reason, cb) => { // `!reason` guard inspired by bluebird (Ref: https://goo.gl/t5IS6M). // Because `null` is a special error value in callbacks which means "no error // occurred", we error-wrap so the callback consumer can distinguish between // "the promise rejected with null" or "the promise fulfilled with undefined". if (!reason) { - reason = new ERR_FALSY_VALUE_REJECTION(reason); + reason = new ERR_FALSY_VALUE_REJECTION.HideStackFramesError(reason); + ErrorCaptureStackTrace(reason, callbackifyOnRejected); } return cb(reason); -}); +}; /** * @template {(...args: any[]) => Promise} T diff --git a/lib/zlib.js b/lib/zlib.js index 2b90c6f91fed76..194b68112fa5db 100644 --- a/lib/zlib.js +++ b/lib/zlib.js @@ -214,7 +214,7 @@ const checkFiniteNumber = hideStackFrames((number, name) => { validateNumber(number, name); // Infinite numbers - throw new ERR_OUT_OF_RANGE(name, 'a finite number', number); + throw new ERR_OUT_OF_RANGE.HideStackFramesError(name, 'a finite number', number); }); // 1. Returns def for number when it's undefined or NaN @@ -227,8 +227,8 @@ const checkRangesOrGetDefault = hideStackFrames( return def; } if (number < lower || number > upper) { - throw new ERR_OUT_OF_RANGE(name, - `>= ${lower} and <= ${upper}`, number); + throw new ERR_OUT_OF_RANGE.HideStackFramesError(name, + `>= ${lower} and <= ${upper}`, number); } return number; }, diff --git a/test/fixtures/errors/error_aggregateTwoErrors.snapshot b/test/fixtures/errors/error_aggregateTwoErrors.snapshot index a8d13c461b2cb0..a84ae702bb1e2e 100644 --- a/test/fixtures/errors/error_aggregateTwoErrors.snapshot +++ b/test/fixtures/errors/error_aggregateTwoErrors.snapshot @@ -2,7 +2,8 @@ throw aggregateTwoErrors(err, originalError); ^ -[AggregateError: original] { +AggregateError: original + at Function. (node:internal*errors:*:*) { code: 'ERR0', [errors]: [ Error: original diff --git a/test/parallel/test-errors-hide-stack-frames.js b/test/parallel/test-errors-hide-stack-frames.js new file mode 100644 index 00000000000000..ea17f82a3d3aff --- /dev/null +++ b/test/parallel/test-errors-hide-stack-frames.js @@ -0,0 +1,138 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const { hideStackFrames, codes } = require('internal/errors'); +const assert = require('assert'); + +{ + // Test that the stack trace is hidden for normal unnamed functions. + function a() { + b(); + } + + function b() { + c(); + } + + const c = hideStackFrames(function() { + throw new Error('test'); + }); + + try { + a(); + } catch (e) { + assert.strictEqual(Error.stackTraceLimit, 10); + const stack = e.stack.split('\n'); + assert.doesNotMatch(stack[1], /at c/); + assert.match(stack[1], /at b/); + assert.strictEqual(Error.stackTraceLimit, 10); + } +} + +{ + // Test that the stack trace is hidden for normal functions. + function a() { + b(); + } + + function b() { + c(); + } + + const c = hideStackFrames(function c() { + throw new Error('test'); + }); + + try { + a(); + } catch (e) { + assert.strictEqual(Error.stackTraceLimit, 10); + const stack = e.stack.split('\n'); + assert.doesNotMatch(stack[1], /at c/); + assert.match(stack[1], /at b/); + assert.strictEqual(Error.stackTraceLimit, 10); + } +} + +{ + // Test that the stack trace is hidden for arrow functions. + function a() { + b(); + } + + function b() { + c(); + } + + const c = hideStackFrames(() => { + throw new Error('test'); + }); + + try { + a(); + } catch (e) { + assert.strictEqual(Error.stackTraceLimit, 10); + const stack = e.stack.split('\n'); + assert.doesNotMatch(stack[1], /at c/); + assert.match(stack[1], /at b/); + assert.strictEqual(Error.stackTraceLimit, 10); + } +} + +{ + // Creating a new Error object without stack trace, then throwing it + // should get a stack trace by hideStackFrames. + function a() { + b(); + } + + function b() { + c(); + } + + const c = hideStackFrames(function() { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + const err = new Error('test'); + Error.stackTraceLimit = limit; + throw err; + }); + + try { + a(); + } catch (e) { + assert.strictEqual(Error.stackTraceLimit, 10); + const stack = e.stack.split('\n'); + assert.doesNotMatch(stack[1], /at c/); + assert.match(stack[1], /at b/); + assert.strictEqual(Error.stackTraceLimit, 10); + } +} + +{ + const ERR_ACCESS_DENIED = codes.ERR_ACCESS_DENIED; + // Creating a new Error object without stack trace, then throwing it + // should get a stack trace by hideStackFrames. + function a() { + b(); + } + + function b() { + c(); + } + + const c = hideStackFrames(function() { + throw new ERR_ACCESS_DENIED.NoStackError('test'); + }); + + try { + a(); + } catch (e) { + assert.strictEqual(Error.stackTraceLimit, 10); + const stack = e.stack.split('\n'); + assert.doesNotMatch(stack[1], /at c/); + assert.match(stack[1], /at b/); + assert.strictEqual(Error.stackTraceLimit, 10); + } +}