Skip to content

Commit 99a58d2

Browse files
bigmontzrobsdedude
andauthored
Fix serialization of broken objects in the driver logs (#983)
* Fix serialization of broken objects in the driver logs Trying to logging broken objects was causing the driver fails in unexpected moments. This broken behaviour makes the driver fail different when the `debug` log is enabled. Co-authored-by: Robsdedude <[email protected]>
1 parent a678bdc commit 99a58d2

File tree

8 files changed

+257
-39
lines changed

8 files changed

+257
-39
lines changed

packages/bolt-connection/src/packstream/packstream-v1.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
internal
3333
} from 'neo4j-driver-core'
3434

35-
const { util } = internal
35+
const { objectUtil } = internal
3636
const { PROTOCOL_ERROR } = error
3737

3838
const TINY_STRING = 0x80
@@ -575,7 +575,7 @@ class Unpacker {
575575
return null
576576
}
577577
} catch (error) {
578-
return util.createBrokenObject(error)
578+
return objectUtil.createBrokenObject(error)
579579
}
580580

581581
}

packages/core/src/internal/index.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import * as urlUtil from './url-util'
3131
import * as serverAddress from './server-address'
3232
import * as resolver from './resolver'
3333
import * as retryStrategy from './retry-strategy'
34+
import * as objectUtil from './object-util'
3435

3536
export {
3637
util,
@@ -46,5 +47,6 @@ export {
4647
urlUtil,
4748
serverAddress,
4849
resolver,
49-
retryStrategy
50+
retryStrategy,
51+
objectUtil
5052
}
+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
// eslint-disable-next-line @typescript-eslint/naming-convention
20+
const __isBrokenObject__ = '__isBrokenObject__'
21+
// eslint-disable-next-line @typescript-eslint/naming-convention
22+
const __reason__ = '__reason__'
23+
24+
/**
25+
* Creates a object on which all method calls will throw the given error
26+
*
27+
* @param {Error} error The error
28+
* @param {any} object The object. Default: {}
29+
* @returns {any} A broken object
30+
*/
31+
function createBrokenObject<T extends object> (error: Error, object: any = {}): T {
32+
const fail: <T>() => T = () => {
33+
throw error
34+
}
35+
36+
return new Proxy(object, {
37+
get: (_: T, p: string | Symbol): any => {
38+
if (p === __isBrokenObject__) {
39+
return true
40+
} else if (p === __reason__) {
41+
return error
42+
} else if (p === 'toJSON') {
43+
return undefined
44+
}
45+
fail()
46+
},
47+
set: fail,
48+
apply: fail,
49+
construct: fail,
50+
defineProperty: fail,
51+
deleteProperty: fail,
52+
getOwnPropertyDescriptor: fail,
53+
getPrototypeOf: fail,
54+
has: fail,
55+
isExtensible: fail,
56+
ownKeys: fail,
57+
preventExtensions: fail,
58+
setPrototypeOf: fail
59+
})
60+
}
61+
62+
/**
63+
* Verifies if it is a Broken Object
64+
* @param {any} object The object
65+
* @returns {boolean} If it was created with createBrokenObject
66+
*/
67+
function isBrokenObject (object: any): boolean {
68+
return object !== null && typeof object === 'object' && object[__isBrokenObject__] === true
69+
}
70+
71+
/**
72+
* Returns if the reason the object is broken.
73+
*
74+
* This method should only be called with instances create with {@link createBrokenObject}
75+
*
76+
* @param {any} object The object
77+
* @returns {Error} The reason the object is broken
78+
*/
79+
function getBrokenObjectReason (object: any): Error {
80+
return object[__reason__]
81+
}
82+
83+
export {
84+
createBrokenObject,
85+
isBrokenObject,
86+
getBrokenObjectReason
87+
}

packages/core/src/internal/util.ts

+1-32
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import { stringify } from '../json'
2424

2525
const ENCRYPTION_ON: EncryptionLevel = 'ENCRYPTION_ON'
2626
const ENCRYPTION_OFF: EncryptionLevel = 'ENCRYPTION_OFF'
27-
2827
/**
2928
* Verifies if the object is null or empty
3029
* @param obj The subject object
@@ -223,35 +222,6 @@ function isString(str: any): str is string {
223222
return Object.prototype.toString.call(str) === '[object String]'
224223
}
225224

226-
/**
227-
* Creates a object which all method call will throw the given error
228-
*
229-
* @param {Error} error The error
230-
* @param {any} object The object. Default: {}
231-
* @returns {any} A broken object
232-
*/
233-
function createBrokenObject<T extends object> (error: Error, object: any = {}): T {
234-
const fail = () => {
235-
throw error
236-
}
237-
238-
return new Proxy(object, {
239-
get: fail,
240-
set: fail,
241-
apply: fail,
242-
construct: fail,
243-
defineProperty: fail,
244-
deleteProperty: fail,
245-
getOwnPropertyDescriptor: fail,
246-
getPrototypeOf: fail,
247-
has: fail,
248-
isExtensible: fail,
249-
ownKeys: fail,
250-
preventExtensions: fail,
251-
setPrototypeOf: fail,
252-
})
253-
}
254-
255225
export {
256226
isEmptyObjectOrNull,
257227
isObject,
@@ -263,6 +233,5 @@ export {
263233
assertValidDate,
264234
validateQueryAndParameters,
265235
ENCRYPTION_ON,
266-
ENCRYPTION_OFF,
267-
createBrokenObject
236+
ENCRYPTION_OFF
268237
}

packages/core/src/json.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,25 @@
1717
* limitations under the License.
1818
*/
1919

20+
import { isBrokenObject, getBrokenObjectReason } from './internal/object-util'
21+
2022
/**
2123
* Custom version on JSON.stringify that can handle values that normally don't support serialization, such as BigInt.
2224
* @private
2325
* @param val A JavaScript value, usually an object or array, to be converted.
2426
* @returns A JSON string representing the given value.
2527
*/
26-
export function stringify (val: any) {
27-
return JSON.stringify(val, (_, value) =>
28-
typeof value === 'bigint' ? `${value}n` : value
29-
)
28+
export function stringify (val: any): string {
29+
return JSON.stringify(val, (_, value) => {
30+
if (isBrokenObject(value)) {
31+
return {
32+
__isBrokenObject__: true,
33+
__reason__: getBrokenObjectReason(value)
34+
}
35+
}
36+
if (typeof value === 'bigint') {
37+
return `${value}n`
38+
}
39+
return value
40+
})
3041
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`json .stringify should handle BigInt 1`] = `"\\"42n\\""`;
4+
5+
exports[`json .stringify should handle BigInt in a list 1`] = `"[\\"42n\\",\\"-24n\\"]"`;
6+
7+
exports[`json .stringify should handle BigInt in a object 1`] = `"{\\"theResponse\\":\\"42n\\"}"`;
8+
9+
exports[`json .stringify should handle objects created with createBrokenObject 1`] = `"{\\"__isBrokenObject__\\":true,\\"__reason__\\":{\\"code\\":\\"N/A\\",\\"name\\":\\"Neo4jError\\"}}"`;
10+
11+
exports[`json .stringify should handle objects created with createBrokenObject in list 1`] = `"[{\\"__isBrokenObject__\\":true,\\"__reason__\\":{\\"code\\":\\"N/A\\",\\"name\\":\\"Neo4jError\\"}}]"`;
12+
13+
exports[`json .stringify should handle objects created with createBrokenObject inside other object 1`] = `"{\\"number\\":1,\\"broken\\":{\\"__isBrokenObject__\\":true,\\"__reason__\\":{\\"code\\":\\"N/A\\",\\"name\\":\\"Neo4jError\\"}}}"`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
import { newError } from '../../src'
20+
import {
21+
createBrokenObject,
22+
isBrokenObject,
23+
getBrokenObjectReason
24+
} from '../../src/internal/object-util'
25+
26+
describe('isBrokenObject', () => {
27+
it('should return true when object created with createBrokenObject', () => {
28+
const object = createBrokenObject(newError('error'), {})
29+
30+
expect(isBrokenObject(object)).toBe(true)
31+
})
32+
33+
it('should return false for regular objects', () => {
34+
const object = {}
35+
36+
expect(isBrokenObject(object)).toBe(false)
37+
})
38+
39+
it('should return false for non-objects', () => {
40+
expect(isBrokenObject(null)).toBe(false)
41+
expect(isBrokenObject(undefined)).toBe(false)
42+
expect(isBrokenObject(1)).toBe(false)
43+
expect(isBrokenObject(() => {})).toBe(false)
44+
expect(isBrokenObject('string')).toBe(false)
45+
})
46+
})
47+
48+
describe('getBrokenObjectReason', () => {
49+
it('should return the reason the object is broken', () => {
50+
const reason = newError('error')
51+
const object = createBrokenObject(reason, {})
52+
53+
expect(getBrokenObjectReason(object)).toBe(reason)
54+
})
55+
})
56+
57+
describe('createBrokenObject', () => {
58+
describe('toJSON', () => {
59+
it('should return undefined', () => {
60+
const reason = newError('error')
61+
const object = createBrokenObject(reason, {})
62+
63+
// @ts-expect-error
64+
expect(object.toJSON).toBeUndefined()
65+
})
66+
})
67+
})

packages/core/test/json.test.ts

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* Copyright (c) "Neo4j"
3+
* Neo4j Sweden AB [http://neo4j.com]
4+
*
5+
* This file is part of Neo4j.
6+
*
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
import { json, newError } from '../src'
21+
import { createBrokenObject } from '../src/internal/object-util'
22+
23+
describe('json', () => {
24+
describe('.stringify', () => {
25+
it('should handle objects created with createBrokenObject', () => {
26+
const reason = newError('some error')
27+
const broken = createBrokenObject(reason, { })
28+
29+
expect(json.stringify(broken)).toMatchSnapshot()
30+
})
31+
32+
it('should handle objects created with createBrokenObject in list', () => {
33+
const reason = newError('some error')
34+
const broken = createBrokenObject(reason, { })
35+
36+
expect(json.stringify([broken])).toMatchSnapshot()
37+
})
38+
39+
it('should handle objects created with createBrokenObject inside other object', () => {
40+
const reason = newError('some error')
41+
const broken = createBrokenObject(reason, { })
42+
43+
expect(json.stringify({
44+
number: 1,
45+
broken
46+
})).toMatchSnapshot()
47+
})
48+
49+
it('should handle BigInt', () => {
50+
const bigint = BigInt(42)
51+
52+
expect(json.stringify(bigint)).toMatchSnapshot()
53+
})
54+
55+
it('should handle BigInt in a list', () => {
56+
const bigintList = [BigInt(42), BigInt(-24)]
57+
58+
expect(json.stringify(bigintList)).toMatchSnapshot()
59+
})
60+
61+
it('should handle BigInt in a object', () => {
62+
const bigintInObject = {
63+
theResponse: BigInt(42)
64+
}
65+
66+
expect(json.stringify(bigintInObject)).toMatchSnapshot()
67+
})
68+
})
69+
})

0 commit comments

Comments
 (0)