diff --git a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js index d8f573a0a..7a4da0a25 100644 --- a/packages/bolt-connection/src/connection-provider/connection-provider-routing.js +++ b/packages/bolt-connection/src/connection-provider/connection-provider-routing.js @@ -121,7 +121,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this.forgetWriter(address, database || DEFAULT_DB_NAME) return newError( 'No longer possible to write to server at ' + address, - SESSION_EXPIRED + SESSION_EXPIRED, + error ) } @@ -338,7 +339,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider ) { // we start with seed router, no routers were probed before const seenRouters = [] - let newRoutingTable = await this._fetchRoutingTableUsingSeedRouter( + let [newRoutingTable, error] = await this._fetchRoutingTableUsingSeedRouter( seenRouters, this._seedRouter, currentRoutingTable, @@ -350,18 +351,21 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider this._useSeedRouter = false } else { // seed router did not return a valid routing table - try to use other known routers - newRoutingTable = await this._fetchRoutingTableUsingKnownRouters( + const [newRoutingTable2, error2] = await this._fetchRoutingTableUsingKnownRouters( knownRouters, currentRoutingTable, bookmarks, impersonatedUser ) + newRoutingTable = newRoutingTable2 + error = error2 || error } return await this._applyRoutingTableIfPossible( currentRoutingTable, newRoutingTable, - onDatabaseNameResolved + onDatabaseNameResolved, + error ) } @@ -372,7 +376,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider impersonatedUser, onDatabaseNameResolved ) { - let newRoutingTable = await this._fetchRoutingTableUsingKnownRouters( + let [newRoutingTable, error] = await this._fetchRoutingTableUsingKnownRouters( knownRouters, currentRoutingTable, bookmarks, @@ -381,7 +385,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider if (!newRoutingTable) { // none of the known routers returned a valid routing table - try to use seed router address for rediscovery - newRoutingTable = await this._fetchRoutingTableUsingSeedRouter( + [newRoutingTable, error] = await this._fetchRoutingTableUsingSeedRouter( knownRouters, this._seedRouter, currentRoutingTable, @@ -393,7 +397,8 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return await this._applyRoutingTableIfPossible( currentRoutingTable, newRoutingTable, - onDatabaseNameResolved + onDatabaseNameResolved, + error ) } @@ -403,7 +408,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider bookmarks, impersonatedUser ) { - const newRoutingTable = await this._fetchRoutingTable( + const [newRoutingTable, error] = await this._fetchRoutingTable( knownRouters, currentRoutingTable, bookmarks, @@ -412,7 +417,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider if (newRoutingTable) { // one of the known routers returned a valid routing table - use it - return newRoutingTable + return [newRoutingTable, null] } // returned routing table was undefined, this means a connection error happened and the last known @@ -424,7 +429,7 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider lastRouterIndex ) - return null + return [null, error] } async _fetchRoutingTableUsingSeedRouter ( @@ -453,14 +458,14 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider return [].concat.apply([], dnsResolvedAddresses) } - _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser) { + async _fetchRoutingTable (routerAddresses, routingTable, bookmarks, impersonatedUser) { return routerAddresses.reduce( async (refreshedTablePromise, currentRouter, currentIndex) => { - const newRoutingTable = await refreshedTablePromise + const [newRoutingTable] = await refreshedTablePromise if (newRoutingTable) { // valid routing table was fetched - just return it, try next router otherwise - return newRoutingTable + return [newRoutingTable, null] } else { // returned routing table was undefined, this means a connection error happened and we need to forget the // previous router and try the next one @@ -473,19 +478,19 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider } // try next router - const session = await this._createSessionForRediscovery( + const [session, error] = await this._createSessionForRediscovery( currentRouter, bookmarks, impersonatedUser ) if (session) { try { - return await this._rediscovery.lookupRoutingTableOnRouter( + return [await this._rediscovery.lookupRoutingTableOnRouter( session, routingTable.database, currentRouter, impersonatedUser - ) + ), null] } catch (error) { return this._handleRediscoveryError(error, currentRouter) } finally { @@ -494,10 +499,10 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider } else { // unable to acquire connection and create session towards the current router // return null to signal that the next router should be tried - return null + return [null, error] } }, - Promise.resolve(null) + Promise.resolve([null, null]) ) } @@ -515,20 +520,20 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider const protocolVersion = connection.protocol().version if (protocolVersion < 4.0) { - return new Session({ + return [new Session({ mode: WRITE, bookmarks: Bookmarks.empty(), connectionProvider - }) + }), null] } - return new Session({ + return [new Session({ mode: READ, database: SYSTEM_DB_NAME, bookmarks, connectionProvider, impersonatedUser - }) + }), null] } catch (error) { return this._handleRediscoveryError(error, routerAddress) } @@ -541,21 +546,23 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider // throw when getServers procedure not found because this is clearly a configuration issue throw newError( `Server at ${routerAddress.asHostPort()} can't perform routing. Make sure you are connecting to a causal cluster`, - SERVICE_UNAVAILABLE + SERVICE_UNAVAILABLE, + error ) } this._log.warn( `unable to fetch routing table because of an error ${error}` ) - return null + return [null, error] } - async _applyRoutingTableIfPossible (currentRoutingTable, newRoutingTable, onDatabaseNameResolved) { + async _applyRoutingTableIfPossible (currentRoutingTable, newRoutingTable, onDatabaseNameResolved, error) { if (!newRoutingTable) { // none of routing servers returned valid routing table, throw exception throw newError( `Could not perform discovery. No routing servers available. Known routing table: ${currentRoutingTable}`, - SERVICE_UNAVAILABLE + SERVICE_UNAVAILABLE, + error ) } diff --git a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js index e765f342f..3055be8b1 100644 --- a/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js +++ b/packages/bolt-connection/test/connection-provider/connection-provider-routing.test.js @@ -41,7 +41,15 @@ const { SERVICE_UNAVAILABLE, SESSION_EXPIRED } = error const READ = 'READ' const WRITE = 'WRITE' -describe('#unit RoutingConnectionProvider', () => { +describe.each([ + 3, + 4.0, + 4.1, + 4.2, + 4.3, + 4.4, + 5.0 +])('#unit RoutingConnectionProvider (PROTOCOL_VERSION=%d)', (PROTOCOL_VERSION) => { const server0 = ServerAddress.fromUrl('server0') const server1 = ServerAddress.fromUrl('server1') const server2 = ServerAddress.fromUrl('server2') @@ -1701,6 +1709,8 @@ describe('#unit RoutingConnectionProvider', () => { 'Server at server-non-existing-seed-router:7687 can\'t ' + 'perform routing. Make sure you are connecting to a causal cluster' ) + // Error should be the cause of the given capturedError + expect(capturedError).toEqual(newError(capturedError.message, capturedError.code, error)) } expect(completed).toBe(false) @@ -1728,6 +1738,9 @@ describe('#unit RoutingConnectionProvider', () => { 'Could not perform discovery. ' + 'No routing servers available. Known routing table: ' )) + + // Error should be the cause of the given capturedError + expect(capturedError).toEqual(newError(capturedError.message, capturedError.code, error)) } expect(completed).toBe(false) @@ -2917,71 +2930,88 @@ describe('#unit RoutingConnectionProvider', () => { }) }) }) -}) -function newRoutingConnectionProvider ( - routingTables, - pool = null, - routerToRoutingTable = { null: {} } -) { - const seedRouter = ServerAddress.fromUrl('server-non-existing-seed-router') - return newRoutingConnectionProviderWithSeedRouter( + function newPool ({ create, config } = {}) { + const _create = (address, release) => { + if (create) { + try { + return Promise.resolve(create(address, release)) + } catch (e) { + return Promise.reject(e) + } + } + return Promise.resolve(new FakeConnection(address, release, 'version', PROTOCOL_VERSION)) + } + return new Pool({ + config, + create: (address, release) => _create(address, release) + }) + } + + function newRoutingConnectionProviderWithSeedRouter ( seedRouter, - [seedRouter], + seedRouterResolved, routingTables, - routerToRoutingTable, - pool - ) -} + routerToRoutingTable = { null: {} }, + connectionPool = null, + routingTablePurgeDelay = null, + fakeRediscovery = null + ) { + const pool = connectionPool || newPool() + const connectionProvider = new RoutingConnectionProvider({ + id: 0, + address: seedRouter, + routingContext: {}, + hostNameResolver: new SimpleHostNameResolver(), + config: {}, + log: Logger.noOp(), + routingTablePurgeDelay: routingTablePurgeDelay + }) + connectionProvider._connectionPool = pool + routingTables.forEach(r => { + connectionProvider._routingTableRegistry.register(r) + }) + connectionProvider._rediscovery = + fakeRediscovery || new FakeRediscovery(routerToRoutingTable) + connectionProvider._hostNameResolver = new FakeDnsResolver(seedRouterResolved) + connectionProvider._useSeedRouter = routingTables.every( + r => r.expirationTime !== Integer.ZERO + ) + return connectionProvider + } -function newRoutingConnectionProviderWithFakeRediscovery ( - fakeRediscovery, - pool = null, - routerToRoutingTable = { null: {} } -) { - const seedRouter = ServerAddress.fromUrl('server-non-existing-seed-router') - return newRoutingConnectionProviderWithSeedRouter( - seedRouter, - [seedRouter], - [], - routerToRoutingTable, - pool, - null, - fakeRediscovery - ) -} + function newRoutingConnectionProvider ( + routingTables, + pool = null, + routerToRoutingTable = { null: {} } + ) { + const seedRouter = ServerAddress.fromUrl('server-non-existing-seed-router') + return newRoutingConnectionProviderWithSeedRouter( + seedRouter, + [seedRouter], + routingTables, + routerToRoutingTable, + pool + ) + } -function newRoutingConnectionProviderWithSeedRouter ( - seedRouter, - seedRouterResolved, - routingTables, - routerToRoutingTable = { null: {} }, - connectionPool = null, - routingTablePurgeDelay = null, - fakeRediscovery = null -) { - const pool = connectionPool || newPool() - const connectionProvider = new RoutingConnectionProvider({ - id: 0, - address: seedRouter, - routingContext: {}, - hostNameResolver: new SimpleHostNameResolver(), - config: {}, - log: Logger.noOp(), - routingTablePurgeDelay: routingTablePurgeDelay - }) - connectionProvider._connectionPool = pool - routingTables.forEach(r => { - connectionProvider._routingTableRegistry.register(r) - }) - connectionProvider._rediscovery = - fakeRediscovery || new FakeRediscovery(routerToRoutingTable) - connectionProvider._hostNameResolver = new FakeDnsResolver(seedRouterResolved) - connectionProvider._useSeedRouter = routingTables.every( - r => r.expirationTime !== Integer.ZERO - ) - return connectionProvider -} + function newRoutingConnectionProviderWithFakeRediscovery ( + fakeRediscovery, + pool = null, + routerToRoutingTable = { null: {} } + ) { + const seedRouter = ServerAddress.fromUrl('server-non-existing-seed-router') + return newRoutingConnectionProviderWithSeedRouter( + seedRouter, + [seedRouter], + [], + routerToRoutingTable, + pool, + null, + fakeRediscovery + ) + } +}) function newRoutingTableWithUser ({ database, @@ -3029,23 +3059,6 @@ function setupRoutingConnectionProviderToRememberRouters ( connectionProvider._fetchRoutingTable = rememberingFetch } -function newPool ({ create, config } = {}) { - const _create = (address, release) => { - if (create) { - try { - return Promise.resolve(create(address, release)) - } catch (e) { - return Promise.reject(e) - } - } - return Promise.resolve(new FakeConnection(address, release, 'version', 4.0)) - } - return new Pool({ - config, - create: (address, release) => _create(address, release) - }) -} - function expectRoutingTable ( connectionProvider, database, diff --git a/packages/core/src/error.ts b/packages/core/src/error.ts index 1af6464e7..d1212acc3 100644 --- a/packages/core/src/error.ts +++ b/packages/core/src/error.ts @@ -70,8 +70,9 @@ class Neo4jError extends Error { * @param {string} message - the error message * @param {string} code - Optional error code. Will be populated when error originates in the database. */ - constructor (message: string, code: Neo4jErrorCode) { - super(message) + constructor (message: string, code: Neo4jErrorCode, cause?: Error) { + // @ts-expect-error + super(message, cause != null ? { cause } : undefined) this.constructor = Neo4jError // eslint-disable-next-line no-proto this.__proto__ = Neo4jError.prototype @@ -105,8 +106,8 @@ class Neo4jError extends Error { * @return {Neo4jError} an {@link Neo4jError} * @private */ -function newError (message: string, code?: Neo4jErrorCode): Neo4jError { - return new Neo4jError(message, code ?? NOT_AVAILABLE) +function newError (message: string, code?: Neo4jErrorCode, cause?: Error): Neo4jError { + return new Neo4jError(message, code ?? NOT_AVAILABLE, cause) } /** diff --git a/packages/core/test/error.test.ts b/packages/core/test/error.test.ts index 9e7ab7087..535a21613 100644 --- a/packages/core/test/error.test.ts +++ b/packages/core/test/error.test.ts @@ -26,6 +26,14 @@ import { } from '../src/error' describe('newError', () => { + let supportsCause = false + beforeAll(() => { + // @ts-expect-error + const error = new Error('a', { cause: new Error('a') }) + // @ts-expect-error + supportsCause = error.cause != null + }) + ;[PROTOCOL_ERROR, SERVICE_UNAVAILABLE, SESSION_EXPIRED].forEach( expectedCode => { test(`should create Neo4jError for code ${expectedCode}`, () => { @@ -43,6 +51,31 @@ describe('newError', () => { expect(error.message).toEqual('some error') expect(error.code).toEqual('N/A') }) + + test('should create Neo4jErro with cause', () => { + const cause = new Error('cause') + const error: Neo4jError = newError('some error', undefined, cause) + + expect(error.message).toEqual('some error') + expect(error.code).toEqual('N/A') + if (supportsCause) { + // @ts-expect-error + expect(error.cause).toBe(cause) + } else { + // @ts-expect-error + expect(error.cause).toBeUndefined() + } + }) + + test.each([null, undefined])('should create Neo4jError without cause (%s)', (cause) => { + // @ts-expect-error + const error: Neo4jError = newError('some error', undefined, cause) + + expect(error.message).toEqual('some error') + expect(error.code).toEqual('N/A') + // @ts-expect-error + expect(error.cause).toBeUndefined() + }) }) describe('isRetriableError()', () => {