1/** 2 * Copyright (c) 650 Industries. 3 * Copyright (c) Meta Platforms, Inc. and affiliates. 4 * 5 * This source code is licensed under the MIT license found in the 6 * LICENSE file in the root directory of this source tree. 7 */ 8 9/** 10 * Tries to stringify with JSON.stringify and toString, but catches exceptions 11 * (e.g. from circular objects) and always returns a string and never throws. 12 */ 13export function createStringifySafeWithLimits(limits: { 14 maxDepth?: number; 15 maxStringLimit?: number; 16 maxArrayLimit?: number; 17 maxObjectKeysLimit?: number; 18}): (foo: any) => string { 19 const { 20 maxDepth = Number.POSITIVE_INFINITY, 21 maxStringLimit = Number.POSITIVE_INFINITY, 22 maxArrayLimit = Number.POSITIVE_INFINITY, 23 maxObjectKeysLimit = Number.POSITIVE_INFINITY, 24 } = limits; 25 const stack: any[] = []; 26 function replacer(this: unknown, _key: string, value: any): any { 27 while (stack.length && this !== stack[0]) { 28 stack.shift(); 29 } 30 31 if (typeof value === 'string') { 32 const truncatedString = '...(truncated)...'; 33 if (value.length > maxStringLimit + truncatedString.length) { 34 return value.substring(0, maxStringLimit) + truncatedString; 35 } 36 return value; 37 } 38 if (typeof value !== 'object' || value === null) { 39 return value; 40 } 41 42 let retval = value; 43 if (Array.isArray(value)) { 44 if (stack.length >= maxDepth) { 45 retval = `[ ... array with ${value.length} values ... ]`; 46 } else if (value.length > maxArrayLimit) { 47 retval = value 48 .slice(0, maxArrayLimit) 49 .concat([`... extra ${value.length - maxArrayLimit} values truncated ...`]); 50 } 51 } else { 52 // Add refinement after Array.isArray call. 53 if (typeof value !== 'object') { 54 throw new Error('This was already found earlier'); 55 } 56 const keys = Object.keys(value); 57 if (stack.length >= maxDepth) { 58 retval = `{ ... object with ${keys.length} keys ... }`; 59 } else if (keys.length > maxObjectKeysLimit) { 60 // Return a sample of the keys. 61 retval = {}; 62 for (const k of keys.slice(0, maxObjectKeysLimit)) { 63 retval[k] = value[k]; 64 } 65 const truncatedKey = '...(truncated keys)...'; 66 retval[truncatedKey] = keys.length - maxObjectKeysLimit; 67 } 68 } 69 stack.unshift(retval); 70 return retval; 71 } 72 73 return function stringifySafe(arg: any): string { 74 if (arg === undefined) { 75 return 'undefined'; 76 } else if (arg === null) { 77 return 'null'; 78 } else if (typeof arg === 'function') { 79 try { 80 return arg.toString(); 81 } catch { 82 return '[function unknown]'; 83 } 84 } else if (arg instanceof Error) { 85 return arg.name + ': ' + arg.message; 86 } else { 87 // Perform a try catch, just in case the object has a circular 88 // reference or stringify throws for some other reason. 89 try { 90 const ret = JSON.stringify(arg, replacer); 91 if (ret === undefined) { 92 return '["' + typeof arg + '" failed to stringify]'; 93 } 94 return ret; 95 } catch { 96 if (typeof arg.toString === 'function') { 97 try { 98 // $FlowFixMe[incompatible-use] : toString shouldn't take any arguments in general. 99 return arg.toString(); 100 } catch {} 101 } 102 } 103 } 104 return '["' + typeof arg + '" failed to stringify]'; 105 }; 106} 107 108const stringifySafe = createStringifySafeWithLimits({ 109 maxDepth: 10, 110 maxStringLimit: 100, 111 maxArrayLimit: 50, 112 maxObjectKeysLimit: 50, 113}); 114 115export default stringifySafe; 116