Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add category field to Neo4jError #916

Open
wants to merge 6 commits into
base: 5.0
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* limitations under the License.
*/

import { newError, error, int, Session, internal } from 'neo4j-driver-core'
import { newError, error, newFatalDiscoveryError, int, Session, internal } from 'neo4j-driver-core'
import Rediscovery, { RoutingTable } from '../rediscovery'
import { HostNameResolver } from '../channel'
import SingleConnectionProvider from './connection-provider-single'
Expand Down Expand Up @@ -536,7 +536,9 @@ export default class RoutingConnectionProvider extends PooledConnectionProvider
}

_handleRediscoveryError(error, routerAddress) {
if (_isFailFastError(error) || _isFailFastSecurityError(error)) {
if (error.code === DATABASE_NOT_FOUND_CODE) {
throw newFatalDiscoveryError(error.message, error.code)
} else if (_isFailFastError(error) || _isFailFastSecurityError(error)) {
throw error
} else if (error.code === PROCEDURE_NOT_FOUND_CODE) {
// throw when getServers procedure not found because this is clearly a configuration issue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
Integer,
int,
internal,
ServerInfo
ServerInfo,
newFatalDiscoveryError
} from 'neo4j-driver-core'
import { RoutingTable } from '../../src/rediscovery/'
import { Pool } from '../../src/pool'
Expand Down Expand Up @@ -1660,7 +1661,6 @@ describe('#unit RoutingConnectionProvider', () => {

describe('when rediscovery.lookupRoutingTableOnRouter fails', () => {
describe.each([
'Neo.ClientError.Database.DatabaseNotFound',
'Neo.ClientError.Transaction.InvalidBookmark',
'Neo.ClientError.Transaction.InvalidBookmarkMixture',
'Neo.ClientError.Security.Forbidden',
Expand All @@ -1685,6 +1685,29 @@ describe('#unit RoutingConnectionProvider', () => {
})
})

describe.each([
'Neo.ClientError.Database.DatabaseNotFound'
])('with "%s"', errorCode => {
it('should fail with `FatalDiscoveryError`', async () => {
const error = newError('something wrong', errorCode)
const expectedThownError = newFatalDiscoveryError('something wrong', errorCode)
const connectionProvider = newRoutingConnectionProviderWithFakeRediscovery(
new FakeRediscovery(null, error),
newPool()
)

let completed = false
try {
await connectionProvider.acquireConnection({ accessMode: READ })
completed = true
} catch (capturedError) {
expect(capturedError).toEqual(expectedThownError)
}

expect(completed).toBe(false)
})
})

describe('with "Neo.ClientError.Procedure.ProcedureNotFound"', () => {
it('should fail with SERVICE_UNAVAILABLE and a message about the need for a causal cluster', async () => {
const error = newError('something wrong', 'Neo.ClientError.Procedure.ProcedureNotFound')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe('ChannelConnection', () => {
expect(loggerFunction).toHaveBeenCalledWith(
'error',
`${connection} experienced a fatal error caused by Neo4jError: some error ` +
'({"code":"C","name":"Neo4jError","retriable":false})'
`(${JSON.stringify(thrownError)})`
)
})
})
Expand Down Expand Up @@ -217,7 +217,7 @@ describe('ChannelConnection', () => {
expect(loggerFunction).toHaveBeenCalledWith(
'error',
`${connection} experienced a fatal error caused by Neo4jError: current failure ` +
'({"code":"ongoing","name":"Neo4jError","retriable":false})'
`(${JSON.stringify(currentFailure)})`
)
})
})
Expand Down
179 changes: 175 additions & 4 deletions packages/core/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const PROTOCOL_ERROR: string = 'ProtocolError'
* Error code representing an no classified error. Used by {@link Neo4jError#code}.
* @type {string}
*/
const NOT_AVAILABLE: string = 'N/A'
const NOT_AVAILABLE: 'N/A' = 'N/A'

/**
* Possible error codes in the {@link Neo4jError}
Expand All @@ -53,6 +53,97 @@ type Neo4jErrorCode =
| typeof PROTOCOL_ERROR
| typeof NOT_AVAILABLE

/**
* Represents an category of error ocurrred in the Neo4j driver.
*
* Categories are used to classify errors in a way that makes it possible to
* distinguish between different types of errors and give the user a hint on how
* to handle them.
*
* The categories are:
* - **{@link Neo4jErrorCategory.AUTHORIZATION_EXPIRED_ERROR}** - Indicates that the authorization has expired in the Neo4j server.
* This error is recoverable and the driver will try to re-authenticate the user in next requests.
* - Serialized as `AutorizationExpiredError`
* - **{@link Neo4jErrorCategory.CLIENT_ERROR}** - Generically representing client errors.
* Usually errors code started with `Neo.ClientError.` not security related.
* - Serialized as `ClientError`
* - **{@link Neo4jErrorCategory.FATAL_DISCOVERY_ERROR}** - non-recorverable errors related to the Neo4j cluster topology.
* Usually happens when the server return `Neo.ClientError.Database.DatabaseNotFound` during the discovery.
* - Serialized as `FatalDiscoveryError`
* - **{@link Neo4jErrorCategory.ILLEGAL_ARGUMENT_ERROR}** - errors that are caused by illegal arguments
* - Serialized as `IllegalArgumentError`
* - **{@link Neo4jErrorCategory.PROTOCOL_ERROR}** - errors that are caused by protocol errors
* - Serialized as `ProtocolError`
* - **{@link Neo4jErrorCategory.RESULT_CONSUMED_ERROR}** - errors that are caused by consuming a already consumed result
* - Serialized as `ResultConsumedError`
* - **{@link Neo4jErrorCategory.SECURITY_ERROR}** - errors that are caused by security related issues.
* Usually errors code started with `Neo.Client.SecurityError.` which are not categorized in other categories.
* - Serialized as `SecurityError`
* - **{@link Neo4jErrorCategory.SERVICE_UNAVAILABLE_ERROR}** - errors that are caused by service unavailable.
* - Serialized as `ServiceUnavailableError`
* - **{@link Neo4jErrorCategory.SESSION_EXPIRED}** - errors that are caused by session expired
* - Serialized as `SessionExpiredError`
* - **{@link Neo4jErrorCategory.TOKEN_EXPIRED_ERROR}** - errors that are caused by token expired
* The user must re-authenticate in this case.
* - Serialized as `TokenExpiredError`
* - **{@link Neo4jErrorCategory.TRANSIENT_ERROR}** - errors which are transient and can be retried.
* - Serialized as `TransientError`
*
* @public
* @see {@link Neo4jError} for more information about errors.
* @see {@link Neo4jError.isRetriable}, {@link Neo4jError#retriable} and {@link isRetriable} for more information about retries.
* @see {@link Neo4jError#code} for more information about error codes.
*/
class Neo4jErrorCategory {
private readonly _value: string

/**
* @type {Neo4jErrorCategory} Neo4jErrorCategory.AUTHORIZATION_EXPIRED_ERROR - The authorization token has expired.
*/
public static readonly AUTHORIZATION_EXPIRED_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('AuthorizationExpiredError')
public static readonly CLIENT_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('ClientError')
public static readonly FATAL_DISCOVERY_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('FatalDiscoveryError')
public static readonly ILLEGAL_ARGUMENT_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('IllegalArgumentError')
public static readonly PROTOCOL_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('ProtocolError')
public static readonly RESULT_CONSUMED_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('ResultConsumedError')
public static readonly SECURITY_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('SecurityError')
public static readonly SERVICE_UNAVAILABLE_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('ServiceUnavailableError')
public static readonly SESSION_EXPIRED_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('SessionExpiredError')
public static readonly TOKEN_EXPIRED_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('TokenExpiredError')
public static readonly TRANSIENT_ERROR: Neo4jErrorCategory = new Neo4jErrorCategory('TransientError')

/**
* @private
* @param value The category value
*/
private constructor(value: string) {
this._value = value
}

/**
* @override
* @returns {string} The value of the category
*/
valueOf(): string {
return this._value
}

/**
* @override
* @returns {string} The JSON representation of the category
*/
toJSON(): string {
return this._value
}

/**
* @override
* @returns {string} The string representation of the category
*/
toString(): string {
return this._value
}
}
/// TODO: Remove definitions of this.constructor and this.__proto__
/**
* Class for all errors thrown/returned by the driver.
Expand All @@ -63,25 +154,46 @@ class Neo4jError extends Error {
*/
code: Neo4jErrorCode
retriable: boolean
category?: Neo4jErrorCategory
__proto__: Neo4jError

/**
* @constructor
* @param {string} message - the error message
* @param {string} code - Optional error code. Will be populated when error originates in the database.
* @param {Neo4jErrorCategory|undefined} category - Optional error category. Will be populated when error originates in the database.
*/
constructor (message: string, code: Neo4jErrorCode) {
constructor (message: string, code: Neo4jErrorCode, category?: Neo4jErrorCategory) {
super(message)
this.constructor = Neo4jError
// eslint-disable-next-line no-proto
this.__proto__ = Neo4jError.prototype

/**
* Indicates the code the error originated from the database.
*
* Read more about error code in https://neo4j.com/docs/status-codes/current/
*
* @type {string}
*/
this.code = code
this.name = 'Neo4jError'
/**
* Indicates if the error is retriable.
* @type {boolean} - true if the error is retriable
*/
this.retriable = _isRetriableCode(code)

/**
* Indicates if the category of the error occurred.
*
* The category is meant to be used to classify errors in a way that makes it possible to
* distinguish between different types of errors and give the user a hint on how to handle them.
*
* More details about the error could be found in {@link Neo4jError#code} and {@link Neo4jError#message}.
* @type {Neo4jErrorCategory} - the category of the error
*/
this.category = category || _categorizeErrorCode(code)
}

/**
Expand All @@ -102,11 +214,42 @@ class Neo4jError extends Error {
* Create a new error from a message and error code
* @param message the error message
* @param code the error code
* @param category the error category
* @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, category?: Neo4jErrorCategory): Neo4jError {
return new Neo4jError(message, code || NOT_AVAILABLE, category)
}

/**
* @private
* @param message the error message
* @param code the error code
* @returns {Neo4jError} an {@link Neo4jError} with {@link Neo4jErrorCategory.ILLEGAL_ARGUMENT_ERROR} as category
*/
function newIllegalArgumentError (message: string, code?: Neo4jErrorCode): Neo4jError {
return newError(message, code, Neo4jErrorCategory.ILLEGAL_ARGUMENT_ERROR )
}

/**
* @private
* @param message the error message
* @param code the error code
* @returns {Neo4jError} an {@link Neo4jError} with {@link Neo4jErrorCategory.RESULT_CONSUMED_ERROR} as category
*/
function newResultConsumedError (message: string, code?: Neo4jErrorCode): Neo4jError {
return newError(message, code, Neo4jErrorCategory.RESULT_CONSUMED_ERROR)
}

/**
* @private
* @param message the error message
* @param code the error code
* @returns {Neo4jError} an {@link Neo4jError} with {@link Neo4jErrorCategory.FATAL_DISCOVERY_ERROR} as category
*/
function newFatalDiscoveryError (message: string, code?: Neo4jErrorCode): Neo4jError {
return newError(message, code, Neo4jErrorCategory.FATAL_DISCOVERY_ERROR)
}

/**
Expand Down Expand Up @@ -153,6 +296,30 @@ function _isRetriableTransientError (code?: Neo4jErrorCode): boolean {
return false
}

function _categorizeErrorCode (code?: Neo4jErrorCode): Neo4jErrorCategory | undefined {
if (code === undefined) {
return undefined
} else if (code === 'Neo.ClientError.Security.AuthorizationExpired') {
return Neo4jErrorCategory.AUTHORIZATION_EXPIRED_ERROR
} else if (code === SERVICE_UNAVAILABLE) {
return Neo4jErrorCategory.SERVICE_UNAVAILABLE_ERROR
} else if (code === PROTOCOL_ERROR) {
return Neo4jErrorCategory.PROTOCOL_ERROR
} else if (code === SESSION_EXPIRED) {
return Neo4jErrorCategory.SESSION_EXPIRED_ERROR
} else if (_isRetriableTransientError(code)) {
return Neo4jErrorCategory.TRANSIENT_ERROR
} else if (code === 'Neo.ClientError.Security.TokenExpired') {
return Neo4jErrorCategory.TOKEN_EXPIRED_ERROR
} else if (code?.startsWith('Neo.ClientError.Security')) {
return Neo4jErrorCategory.SECURITY_ERROR
} else if (code?.startsWith('Neo.ClientError.') || code?.startsWith('Neo.TransientError.')) {
return Neo4jErrorCategory.CLIENT_ERROR
} else {
return undefined
}
}

/**
* @private
* @param {string} code the error to check
Expand All @@ -164,8 +331,12 @@ function _isAuthorizationExpired (code?: Neo4jErrorCode): boolean {

export {
newError,
newIllegalArgumentError,
newResultConsumedError,
newFatalDiscoveryError,
isRetriableError,
Neo4jError,
Neo4jErrorCategory,
SERVICE_UNAVAILABLE,
SESSION_EXPIRED,
PROTOCOL_ERROR
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@

import {
newError,
newIllegalArgumentError,
newFatalDiscoveryError,
Neo4jError,
Neo4jErrorCategory,
isRetriableError,
PROTOCOL_ERROR,
SERVICE_UNAVAILABLE,
Expand Down Expand Up @@ -93,7 +96,10 @@ const error = {
*/
const forExport = {
newError,
newIllegalArgumentError,
newFatalDiscoveryError,
Neo4jError,
Neo4jErrorCategory,
isRetriableError,
error,
Integer,
Expand Down Expand Up @@ -151,7 +157,10 @@ const forExport = {

export {
newError,
newIllegalArgumentError,
newFatalDiscoveryError,
Neo4jError,
Neo4jErrorCategory,
isRetriableError,
error,
Integer,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import ResultSummary from './result-summary'
import Record from './record'
import { Query, PeekableAsyncIterator } from './types'
import { observer, util, connectionHolder } from './internal'
import { newError } from './error'
import { newResultConsumedError } from './error'

const { EMPTY_CONNECTION_HOLDER } = connectionHolder

Expand Down Expand Up @@ -240,7 +240,7 @@ class Result implements Promise<QueryResult> {
*/
[Symbol.asyncIterator](): PeekableAsyncIterator<Record, ResultSummary> {
if (!this.isOpen()) {
const error = newError('Result is already consumed')
const error = newResultConsumedError('Result is already consumed')
return {
next: () => Promise.reject(error),
peek: () => Promise.reject(error),
Expand Down
Loading