Skip to content

Commit d3edd44

Browse files
authored
Merge pull request #204 from lutovich/1.1-single-err-format
Unpack failure messages to Neo4jErrors
2 parents aff3b1d + 517e47d commit d3edd44

11 files changed

+127
-110
lines changed

src/v1/internal/buf.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
*(via Buffer API).
2323
*/
2424

25-
import {newError} from './../error';
2625
let _node = require("buffer");
2726
/**
2827
* Common base with default implementation for most buffer methods.
@@ -551,7 +550,7 @@ class NodeBuffer extends BaseBuffer {
551550
val.position + bytesToCopy );
552551
val.position += bytesToCopy;
553552
} else {
554-
throw newError("Copying not yet implemented.");
553+
super.putBytes(position, val);
555554
}
556555
};
557556

src/v1/internal/connector.js

+22-18
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
*/
1919
import WebSocketChannel from './ch-websocket';
2020
import NodeChannel from './ch-node';
21-
import {Dechunker, Chunker} from "./chunking";
21+
import {Chunker, Dechunker} from './chunking';
2222
import hasFeature from './features';
2323
import {Packer, Unpacker} from './packstream';
2424
import {alloc} from './buf';
25-
import {Node, Relationship, UnboundRelationship, Path, PathSegment} from '../graph-types'
25+
import {Node, Path, PathSegment, Relationship, UnboundRelationship} from '../graph-types';
2626
import {newError} from './../error';
2727

2828
let Channel;
@@ -201,7 +201,9 @@ class Connection {
201201
this._chunker = new Chunker( channel );
202202
this._packer = new Packer( this._chunker );
203203
this._unpacker = new Unpacker();
204+
204205
this._isHandlingFailure = false;
206+
this._currentFailure = null;
205207

206208
// Set to true on fatal errors, to get this out of session pool.
207209
this._isBroken = false;
@@ -288,54 +290,56 @@ class Connection {
288290
}
289291

290292
_handleMessage( msg ) {
293+
const payload = msg.fields[0];
294+
291295
switch( msg.signature ) {
292296
case RECORD:
293-
log("S", "RECORD", msg.fields[0]);
294-
this._currentObserver.onNext( msg.fields[0] );
297+
log("S", "RECORD", msg);
298+
this._currentObserver.onNext( payload );
295299
break;
296300
case SUCCESS:
297-
log("S", "SUCCESS", msg.fields[0]);
301+
log("S", "SUCCESS", msg);
298302
try {
299-
this._currentObserver.onCompleted( msg.fields[0] );
303+
this._currentObserver.onCompleted( payload );
300304
} finally {
301305
this._currentObserver = this._pendingObservers.shift();
302306
}
303307
break;
304308
case FAILURE:
305309
log("S", "FAILURE", msg);
306310
try {
307-
this._currentObserver.onError( msg );
308-
this._errorMsg = msg;
311+
this._currentFailure = newError(payload.message, payload.code);
312+
this._currentObserver.onError( this._currentFailure );
309313
} finally {
310314
this._currentObserver = this._pendingObservers.shift();
311315
// Things are now broken. Pending observers will get FAILURE messages routed until
312316
// We are done handling this failure.
313317
if( !this._isHandlingFailure ) {
314318
this._isHandlingFailure = true;
315-
let self = this;
316319

317320
// isHandlingFailure was false, meaning this is the first failure message
318321
// we see from this failure. We may see several others, one for each message
319322
// we had "optimistically" already sent after whatever it was that failed.
320323
// We only want to and need to ACK the first one, which is why we are tracking
321324
// this _isHandlingFailure thing.
322325
this._ackFailure({
323-
onNext: NO_OP,
324-
onError: NO_OP,
325-
onCompleted: () => {
326-
self._isHandlingFailure = false;
327-
}
326+
onNext: NO_OP,
327+
onError: NO_OP,
328+
onCompleted: () => {
329+
this._isHandlingFailure = false;
330+
this._currentFailure = null;
331+
}
328332
});
329333
}
330334
}
331335
break;
332336
case IGNORED:
333-
log("S", "IGNORED");
337+
log("S", "IGNORED", msg);
334338
try {
335-
if (this._errorMsg && this._currentObserver.onError)
336-
this._currentObserver.onError(this._errorMsg);
339+
if (this._currentFailure && this._currentObserver.onError)
340+
this._currentObserver.onError(this._currentFailure);
337341
else if(this._currentObserver.onError)
338-
this._currentObserver.onError(msg);
342+
this._currentObserver.onError(payload);
339343
} finally {
340344
this._currentObserver = this._pendingObservers.shift();
341345
}

src/v1/internal/get-servers-util.js

+1-13
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default class GetServersUtil {
3131
session.close();
3232
return result.records;
3333
}).catch(error => {
34-
if (this._isProcedureNotFoundError(error)) {
34+
if (error.code === PROCEDURE_NOT_FOUND_CODE) {
3535
// throw when getServers procedure not found because this is clearly a configuration issue
3636
throw newError('Server ' + routerAddress + ' could not perform routing. ' +
3737
'Make sure you are connecting to a causal cluster', SERVICE_UNAVAILABLE);
@@ -92,16 +92,4 @@ export default class GetServersUtil {
9292
PROTOCOL_ERROR);
9393
}
9494
}
95-
96-
_isProcedureNotFoundError(error) {
97-
let errorCode = error.code;
98-
if (!errorCode) {
99-
try {
100-
errorCode = error.fields[0].code;
101-
} catch (e) {
102-
errorCode = 'UNKNOWN';
103-
}
104-
}
105-
return errorCode === PROCEDURE_NOT_FOUND_CODE;
106-
}
10795
}

src/v1/routing-driver.js

+6-26
Original file line numberDiff line numberDiff line change
@@ -36,37 +36,17 @@ class RoutingDriver extends Driver {
3636
}
3737

3838
_createSession(connectionPromise, cb) {
39-
return new RoutingSession(connectionPromise, cb, (err, conn) => {
40-
let code = err.code;
41-
let msg = err.message;
42-
if (!code) {
43-
try {
44-
code = err.fields[0].code;
45-
} catch (e) {
46-
code = 'UNKNOWN';
47-
}
48-
}
49-
if (!msg) {
50-
try {
51-
msg = err.fields[0].message;
52-
} catch (e) {
53-
msg = 'Unknown failure occurred';
54-
}
55-
}
56-
//just to simplify later error handling
57-
err.code = code;
58-
err.message = msg;
59-
60-
if (code === SERVICE_UNAVAILABLE || code === SESSION_EXPIRED) {
39+
return new RoutingSession(connectionPromise, cb, (error, conn) => {
40+
if (error.code === SERVICE_UNAVAILABLE || error.code === SESSION_EXPIRED) {
6141
if (conn) {
6242
this._forget(conn.url)
6343
} else {
6444
connectionPromise.then((conn) => {
6545
this._forget(conn.url);
6646
}).catch(() => {/*ignore*/});
6747
}
68-
return err;
69-
} else if (code === 'Neo.ClientError.Cluster.NotALeader') {
48+
return error;
49+
} else if (error.code === 'Neo.ClientError.Cluster.NotALeader') {
7050
let url = 'UNKNOWN';
7151
if (conn) {
7252
url = conn.url;
@@ -76,9 +56,9 @@ class RoutingDriver extends Driver {
7656
this._routingTable.forgetWriter(conn.url);
7757
}).catch(() => {/*ignore*/});
7858
}
79-
return newError("No longer possible to write to server at " + url, SESSION_EXPIRED);
59+
return newError('No longer possible to write to server at ' + url, SESSION_EXPIRED);
8060
} else {
81-
return err;
61+
return error;
8262
}
8363
});
8464
}

test/internal/connector.test.js

+81-34
Original file line numberDiff line numberDiff line change
@@ -16,75 +16,78 @@
1616
* See the License for the specific language governing permissions and
1717
* limitations under the License.
1818
*/
19-
var DummyChannel = require('../../lib/v1/internal/ch-dummy.js');
20-
var connect = require("../../lib/v1/internal/connector.js").connect;
2119

22-
describe('connector', function() {
20+
import * as DummyChannel from '../../src/v1/internal/ch-dummy';
21+
import {connect, Connection} from '../../src/v1/internal/connector';
22+
import {Packer} from '../../src/v1/internal/packstream';
23+
import {Chunker} from '../../src/v1/internal/chunking';
24+
import {alloc} from '../../src/v1/internal/buf';
25+
import {Neo4jError} from '../../src/v1/error';
2326

24-
it('should read/write basic messages', function(done) {
27+
describe('connector', () => {
28+
29+
it('should read/write basic messages', done => {
2530
// Given
26-
var conn = connect("bolt://localhost")
31+
const conn = connect("bolt://localhost");
2732

2833
// When
29-
conn.initialize( "mydriver/0.0.0", {scheme: "basic", principal: "neo4j", credentials: "neo4j"}, {
30-
onCompleted: function( msg ) {
31-
expect( msg ).not.toBeNull();
34+
conn.initialize("mydriver/0.0.0", {scheme: "basic", principal: "neo4j", credentials: "neo4j"}, {
35+
onCompleted: msg => {
36+
expect(msg).not.toBeNull();
3237
conn.close();
3338
done();
3439
},
35-
onError: function(err) {
36-
console.log(err);
37-
}
40+
onError: console.log
3841
});
3942
conn.sync();
4043

4144
});
42-
it('should retrieve stream', function(done) {
45+
46+
it('should retrieve stream', done => {
4347
// Given
44-
var conn = connect("bolt://localhost")
48+
const conn = connect("bolt://localhost");
4549

4650
// When
47-
var records = [];
48-
conn.initialize( "mydriver/0.0.0", {scheme: "basic", principal: "neo4j", credentials: "neo4j"} );
49-
conn.run( "RETURN 1.0", {} );
50-
conn.pullAll( {
51-
onNext: function( record ) {
52-
records.push( record );
51+
const records = [];
52+
conn.initialize("mydriver/0.0.0", {scheme: "basic", principal: "neo4j", credentials: "neo4j"});
53+
conn.run("RETURN 1.0", {});
54+
conn.pullAll({
55+
onNext: record => {
56+
records.push(record);
5357
},
54-
onCompleted: function( tail ) {
55-
expect( records[0][0] ).toBe( 1 );
58+
onCompleted: () => {
59+
expect(records[0][0]).toBe(1);
5660
conn.close();
5761
done();
5862
}
5963
});
6064
conn.sync();
6165
});
6266

63-
it('should use DummyChannel to read what gets written', function(done) {
67+
it('should use DummyChannel to read what gets written', done => {
6468
// Given
65-
var observer = DummyChannel.observer;
66-
var conn = connect("bolt://localhost", {channel:DummyChannel.channel});
69+
const observer = DummyChannel.observer;
70+
const conn = connect("bolt://localhost", {channel: DummyChannel.channel});
6771

6872
// When
69-
var records = [];
70-
conn.initialize( "mydriver/0.0.0", {scheme: "basic", principal: "neo4j", credentials: "neo4j"} );
71-
conn.run( "RETURN 1", {} );
73+
conn.initialize("mydriver/0.0.0", {scheme: "basic", principal: "neo4j", credentials: "neo4j"});
74+
conn.run("RETURN 1", {});
7275
conn.sync();
73-
expect( observer.instance.toHex() ).toBe( '60 60 b0 17 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 41 b2 01 8e 6d 79 64 72 69 76 65 72 2f 30 2e 30 2e 30 a3 86 73 63 68 65 6d 65 85 62 61 73 69 63 89 70 72 69 6e 63 69 70 61 6c 85 6e 65 6f 34 6a 8b 63 72 65 64 65 6e 74 69 61 6c 73 85 6e 65 6f 34 6a 00 00 00 0c b2 10 88 52 45 54 55 52 4e 20 31 a0 00 00 ' );
76+
expect(observer.instance.toHex()).toBe('60 60 b0 17 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 41 b2 01 8e 6d 79 64 72 69 76 65 72 2f 30 2e 30 2e 30 a3 86 73 63 68 65 6d 65 85 62 61 73 69 63 89 70 72 69 6e 63 69 70 61 6c 85 6e 65 6f 34 6a 8b 63 72 65 64 65 6e 74 69 61 6c 73 85 6e 65 6f 34 6a 00 00 00 0c b2 10 88 52 45 54 55 52 4e 20 31 a0 00 00 ');
7477
done();
7578
});
7679

77-
it('should provide error message when connecting to http-port', function(done) {
80+
it('should provide error message when connecting to http-port', done => {
7881
// Given
79-
var conn = connect("bolt://localhost:7474", {encrypted:false});
82+
const conn = connect("bolt://localhost:7474", {encrypted: false});
8083

8184
// When
82-
conn.initialize( "mydriver/0.0.0", {scheme: "basic", principal: "neo4j", credentials: "neo4j"}, {
83-
onCompleted: function( msg ) {
85+
conn.initialize("mydriver/0.0.0", {scheme: "basic", principal: "neo4j", credentials: "neo4j"}, {
86+
onCompleted: msg => {
8487
},
85-
onError: function(err) {
88+
onError: err => {
8689
//only node gets the pretty error message
87-
if( require('../../lib/v1/internal/ch-node.js').available ) {
90+
if (require('../../lib/v1/internal/ch-node.js').available) {
8891
expect(err.message).toBe("Server responded HTTP. Make sure you are not trying to connect to the http endpoint " +
8992
"(HTTP defaults to port 7474 whereas BOLT defaults to port 7687)");
9093
}
@@ -95,4 +98,48 @@ describe('connector', function() {
9598

9699
});
97100

101+
it('should convert failure messages to errors', done => {
102+
const channel = new DummyChannel.channel;
103+
const connection = new Connection(channel, 'bolt://localhost');
104+
105+
const errorCode = 'Neo.ClientError.Schema.ConstraintValidationFailed';
106+
const errorMessage = 'Node 0 already exists with label User and property "email"=[[email protected]]';
107+
108+
connection._queueObserver({
109+
onError: error => {
110+
expectNeo4jError(error, errorCode, errorMessage);
111+
done();
112+
}
113+
});
114+
115+
channel.onmessage(packedHandshakeMessage());
116+
channel.onmessage(packedFailureMessage(errorCode, errorMessage));
117+
});
118+
119+
function packedHandshakeMessage() {
120+
const result = alloc(4);
121+
result.putInt32(0, 1);
122+
result.reset();
123+
return result;
124+
}
125+
126+
function packedFailureMessage(code, message) {
127+
const channel = new DummyChannel.channel;
128+
const chunker = new Chunker(channel);
129+
const packer = new Packer(chunker);
130+
packer.packStruct(0x7F, [packer.packable({code: code, message: message})]);
131+
chunker.messageBoundary();
132+
chunker.flush();
133+
const data = channel.toBuffer();
134+
const result = alloc(data.length);
135+
result.putBytes(0, data);
136+
return result;
137+
}
138+
139+
function expectNeo4jError(error, expectedCode, expectedMessage) {
140+
expect(() => {
141+
throw error;
142+
}).toThrow(new Neo4jError(expectedMessage, expectedCode));
143+
}
144+
98145
});

test/v1/driver.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('driver', function() {
7979
// Expect
8080
driver.onError = function (err) {
8181
//the error message is different whether in browser or node
82-
expect(err.fields[0].code).toEqual('Neo.ClientError.Security.Unauthorized');
82+
expect(err.code).toEqual('Neo.ClientError.Security.Unauthorized');
8383
done();
8484
};
8585

test/v1/examples.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ describe('examples', function() {
306306
// end::handle-cypher-error[]
307307

308308
testResultPromise.then(function(loggedError){
309-
expect(loggedError.fields[0].code).toBe( "Neo.ClientError.Statement.SyntaxError" );
309+
expect(loggedError.code).toBe( 'Neo.ClientError.Statement.SyntaxError' );
310310
done();
311311
});
312312
});

test/v1/session.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ describe('session', function () {
9494
it('should call observers onError on error ', function (done) {
9595

9696
// When & Then
97-
session.run("RETURN 1 AS").subscribe({
97+
session.run('RETURN 1 AS').subscribe({
9898
onError: function (error) {
99-
expect(error.fields.length).toBe(1);
99+
expect(error.code).toEqual('Neo.ClientError.Statement.SyntaxError');
100100
done();
101101
}
102102
});
@@ -139,9 +139,9 @@ describe('session', function () {
139139

140140
it('should expose basic run/catch ', function (done) {
141141
// When & Then
142-
session.run("RETURN 1 AS").catch(
142+
session.run('RETURN 1 AS').catch(
143143
function (error) {
144-
expect(error.fields.length).toBe(1);
144+
expect(error.code).toEqual('Neo.ClientError.Statement.SyntaxError');
145145
done();
146146
}
147147
)

0 commit comments

Comments
 (0)