Skip to content

Commit 0414c68

Browse files
committed
Cleanup error codes
This commit makes sure direct and routing driver throw exceptions with correct error codes. Direct driver throws errors with code `ServiceUnavailable` on connection failures. Routing driver throws errors with code `ServiceUnavailable` only when rediscovery fails or no servers are available. It also throws errors with code `SessionExpired` on connection failures.
1 parent 3b1c28f commit 0414c68

9 files changed

+106
-31
lines changed

src/v1/driver.js

+9-3
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ class Driver {
6868
*/
6969
_createConnection(url, release) {
7070
let sessionId = this._sessionIdGenerator++;
71-
let conn = connect(url, this._config);
71+
let conn = connect(url, this._config, this._connectionErrorCode());
7272
let streamObserver = new _ConnectionStreamObserver(this, conn);
7373
conn.initialize(this._userAgent, this._token, streamObserver);
7474
conn._id = sessionId;
@@ -126,16 +126,22 @@ class Driver {
126126
return mode;
127127
}
128128

129-
//Extension point
129+
// Extension point
130130
_createConnectionProvider(address, connectionPool, driverOnErrorCallback) {
131131
return new DirectConnectionProvider(address, connectionPool, driverOnErrorCallback);
132132
}
133133

134-
//Extension point
134+
// Extension point
135135
_createSession(mode, connectionProvider, bookmark, config) {
136136
return new Session(mode, connectionProvider, bookmark, config);
137137
}
138138

139+
// Extension point
140+
_connectionErrorCode() {
141+
// connection errors might result in different error codes depending on the driver
142+
return SERVICE_UNAVAILABLE;
143+
}
144+
139145
_driverOnErrorCallback(error) {
140146
const userDefinedOnErrorCallback = this.onError;
141147
if (userDefinedOnErrorCallback && error.code === SERVICE_UNAVAILABLE) {

src/v1/internal/ch-node.js

+11-10
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19-
import net from "net";
20-
import tls from "tls";
21-
import fs from "fs";
22-
import path from "path";
23-
import {EOL} from "os";
24-
import {NodeBuffer} from "./buf";
25-
import {ENCRYPTION_OFF, isEmptyObjectOrNull} from "./util";
26-
import {newError, SESSION_EXPIRED} from "./../error";
19+
import net from 'net';
20+
import tls from 'tls';
21+
import fs from 'fs';
22+
import path from 'path';
23+
import {EOL} from 'os';
24+
import {NodeBuffer} from './buf';
25+
import {ENCRYPTION_OFF, isEmptyObjectOrNull} from './util';
26+
import {newError, SERVICE_UNAVAILABLE} from './../error';
2727

2828
let _CONNECTION_IDGEN = 0;
2929

@@ -291,6 +291,7 @@ class NodeChannel {
291291
this._error = null;
292292
this._handleConnectionError = this._handleConnectionError.bind(this);
293293
this._handleConnectionTerminated = this._handleConnectionTerminated.bind(this);
294+
this._errorCode = opts.errorCode || SERVICE_UNAVAILABLE;
294295

295296
this._encrypted = opts.encrypted;
296297
this._conn = connect(opts, () => {
@@ -318,14 +319,14 @@ class NodeChannel {
318319

319320
_handleConnectionError( err ) {
320321
let msg = err.message || 'Failed to connect to server';
321-
this._error = newError(msg, SESSION_EXPIRED);
322+
this._error = newError(msg, this._errorCode);
322323
if( this.onerror ) {
323324
this.onerror(this._error);
324325
}
325326
}
326327

327328
_handleConnectionTerminated() {
328-
this._error = newError('Connection was closed by server', SESSION_EXPIRED);
329+
this._error = newError('Connection was closed by server', this._errorCode);
329330
if( this.onerror ) {
330331
this.onerror(this._error);
331332
}

src/v1/internal/ch-websocket.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
*/
1919

2020
import {HeapBuffer} from './buf';
21-
import {newError} from './../error';
22-
import {ENCRYPTION_ON, ENCRYPTION_OFF} from './util';
21+
import {newError, SERVICE_UNAVAILABLE} from './../error';
22+
import {ENCRYPTION_OFF, ENCRYPTION_ON} from './util';
2323

2424
/**
2525
* Create a new WebSocketChannel to be used in web browsers.
@@ -39,6 +39,7 @@ class WebSocketChannel {
3939
this._pending = [];
4040
this._error = null;
4141
this._handleConnectionError = this._handleConnectionError.bind(this);
42+
this._errorCode = opts.errorCode || SERVICE_UNAVAILABLE;
4243

4344
this._encrypted = opts.encrypted;
4445

@@ -95,7 +96,7 @@ class WebSocketChannel {
9596
"the root cause of the failure. Common reasons include the database being " +
9697
"unavailable, using the wrong connection URL or temporary network problems. " +
9798
"If you have enabled encryption, ensure your browser is configured to trust the " +
98-
"certificate Neo4j is configured to use. WebSocket `readyState` is: " + this._ws.readyState );
99+
"certificate Neo4j is configured to use. WebSocket `readyState` is: " + this._ws.readyState, this._errorCode );
99100
if (this.onerror) {
100101
this.onerror(this._error);
101102
}

src/v1/internal/connection-providers.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
* limitations under the License.
1818
*/
1919

20-
import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from '../error';
20+
import {newError, SERVICE_UNAVAILABLE} from '../error';
2121
import {READ, WRITE} from '../driver';
2222
import Session from '../session';
2323
import RoundRobinArray from './round-robin-array';
@@ -96,7 +96,7 @@ export class LoadBalancer extends ConnectionProvider {
9696
_acquireConnectionToServer(serversRoundRobinArray, serverName) {
9797
const address = serversRoundRobinArray.next();
9898
if (!address) {
99-
return Promise.reject(newError('No ' + serverName + ' servers available', SESSION_EXPIRED));
99+
return Promise.reject(newError('No ' + serverName + ' servers available', SERVICE_UNAVAILABLE));
100100
}
101101
return this._connectionPool.acquire(address);
102102
}

src/v1/internal/connector.js

+6-4
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import hasFeature from './features';
2323
import {Packer, Unpacker} from './packstream';
2424
import {alloc} from './buf';
2525
import {Node, Path, PathSegment, Relationship, UnboundRelationship} from '../graph-types';
26-
import {newError} from './../error';
26+
import {newError, SERVICE_UNAVAILABLE} from './../error';
2727

2828
let Channel;
2929
if( NodeChannel.available ) {
@@ -475,10 +475,11 @@ class Connection {
475475
* Crete new connection to the provided url.
476476
* @access private
477477
* @param {string} url - 'neo4j'-prefixed URL to Neo4j Bolt endpoint
478-
* @param {object} config
478+
* @param {object} config - this driver configuration
479+
* @param {string} errorCode - error code for errors raised on connection errors
479480
* @return {Connection} - New connection
480481
*/
481-
function connect( url, config = {}) {
482+
function connect( url, config = {}, errorCode = SERVICE_UNAVAILABLE) {
482483
let Ch = config.channel || Channel;
483484
const host = parseHost(url);
484485
const port = parsePort(url) || 7687;
@@ -492,7 +493,8 @@ function connect( url, config = {}) {
492493
// Default to using TRUST_ALL_CERTIFICATES if it is available
493494
trust : config.trust || (hasFeature("trust_all_certificates") ? "TRUST_ALL_CERTIFICATES" : "TRUST_CUSTOM_CA_SIGNED_CERTIFICATES"),
494495
trustedCertificates : config.trustedCertificates || [],
495-
knownHosts : config.knownHosts
496+
knownHosts : config.knownHosts,
497+
errorCode: errorCode
496498
}), completeUrl);
497499
}
498500

src/v1/routing-driver.js

+22-7
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919

2020
import Session from './session';
2121
import {Driver} from './driver';
22-
import {newError, SERVICE_UNAVAILABLE, SESSION_EXPIRED} from './error';
22+
import {newError, SESSION_EXPIRED} from './error';
2323
import {LoadBalancer} from './internal/connection-providers';
2424

2525
/**
@@ -37,13 +37,10 @@ class RoutingDriver extends Driver {
3737

3838
_createSession(mode, connectionProvider, bookmark, config) {
3939
return new RoutingSession(mode, connectionProvider, bookmark, config, (error, conn) => {
40-
if (error.code === SERVICE_UNAVAILABLE || error.code === SESSION_EXPIRED) {
41-
// connection is undefined if error happened before connection was acquired
42-
if (conn) {
43-
this._connectionProvider.forget(conn.url);
44-
}
40+
if (error.code === SESSION_EXPIRED) {
41+
this._forgetConnection(conn);
4542
return error;
46-
} else if (error.code === 'Neo.ClientError.Cluster.NotALeader') {
43+
} else if (RoutingDriver._isFailureToWrite(error)) {
4744
let url = 'UNKNOWN';
4845
// connection is undefined if error happened before connection was acquired
4946
if (conn) {
@@ -57,12 +54,30 @@ class RoutingDriver extends Driver {
5754
});
5855
}
5956

57+
_connectionErrorCode() {
58+
// connection errors mean SERVICE_UNAVAILABLE for direct driver but for routing driver they should only
59+
// result in SESSION_EXPIRED because there might still exist other servers capable of serving the request
60+
return SESSION_EXPIRED;
61+
}
62+
63+
_forgetConnection(connection) {
64+
// connection is undefined if error happened before connection was acquired
65+
if (connection) {
66+
this._connectionProvider.forget(connection.url);
67+
}
68+
}
69+
6070
static _validateConfig(config) {
6171
if(config.trust === 'TRUST_ON_FIRST_USE') {
6272
throw newError('The chosen trust mode is not compatible with a routing driver');
6373
}
6474
return config;
6575
}
76+
77+
static _isFailureToWrite(error) {
78+
return error.code === 'Neo.ClientError.Cluster.NotALeader' ||
79+
error.code === 'Neo.ClientError.General.ForbiddenOnReadOnlyDatabase';
80+
}
6681
}
6782

6883
class RoutingSession extends Session {

test/internal/connection-providers.test.js

+24
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,30 @@ describe('LoadBalancer', () => {
996996
});
997997
});
998998

999+
it('throws service unavailable when refreshed routing table has no readers', done => {
1000+
const pool = newPool();
1001+
const updatedRoutingTable = newRoutingTable(
1002+
['server-A', 'server-B'],
1003+
[],
1004+
['server-C', 'server-D']
1005+
);
1006+
const loadBalancer = newLoadBalancer(
1007+
['server-1', 'server-2'],
1008+
['server-3', 'server-4'],
1009+
['server-5', 'server-6'],
1010+
pool,
1011+
int(0), // expired routing table
1012+
{
1013+
'server-1': updatedRoutingTable,
1014+
}
1015+
);
1016+
1017+
loadBalancer.acquireConnection(READ).catch(error => {
1018+
expect(error.code).toEqual(SERVICE_UNAVAILABLE);
1019+
done();
1020+
});
1021+
});
1022+
9991023
});
10001024

10011025
function newDirectConnectionProvider(address, pool) {

test/v1/direct.driver.boltkit.it.js

+25
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,31 @@ describe('direct driver', () => {
245245
});
246246
});
247247
});
248+
249+
it('should throw service unavailable when server dies', done => {
250+
if (!boltkit.BoltKitSupport) {
251+
done();
252+
return;
253+
}
254+
255+
const kit = new boltkit.BoltKit();
256+
const server = kit.start('./test/resources/boltkit/dead_read_server.script', 9001);
257+
258+
kit.run(() => {
259+
const driver = createDriver();
260+
const session = driver.session();
261+
session.run('MATCH (n) RETURN n.name').catch(error => {
262+
expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE);
263+
264+
driver.close();
265+
server.exit(code => {
266+
expect(code).toEqual(0);
267+
done();
268+
});
269+
});
270+
});
271+
});
272+
248273
});
249274

250275
function createDriver() {

test/v1/driver.test.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ describe('driver', () => {
5050
driver = neo4j.driver("bolt://localhoste", neo4j.auth.basic("neo4j", "neo4j"));
5151

5252
// Expect
53-
driver.onError = err => {
53+
driver.onError = error => {
5454
//the error message is different whether in browser or node
55-
expect(err.message).not.toBeNull();
55+
expect(error.message).not.toBeNull();
56+
expect(error.code).toEqual(neo4j.error.SERVICE_UNAVAILABLE);
5657
done();
5758
};
5859

0 commit comments

Comments
 (0)