Skip to content

Commit 1330243

Browse files
committed
Correctly unpack failure message to Neo4jError
Previously raw FAILURE message was passed to `onError`-like callbacks and thrown as is. This means thrown error contained packstream related fields and actually was a packstream structure. Example: ``` Structure { signature: 127, fields: [ { code: 'Neo.ClientError.Schema.ConstraintValidationFailed', message: 'Node 0 already exists with label A and property "a"=[42]' } ] } ``` This commit makes sure we unpack/convert FAILURE messages into `Neo4jError`s and pass those further to callbacks.
1 parent a454f26 commit 1330243

11 files changed

+96
-78
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

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

20-
import * as DummyChannel from "../../src/v1/internal/ch-dummy";
21-
import {connect} from "../../src/v1/internal/connector";
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';
2226

2327
describe('connector', () => {
2428

@@ -94,4 +98,48 @@ describe('connector', () => {
9498

9599
});
96100

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+
97145
});

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
)

test/v1/tck/steps/authsteps.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ module.exports = function () {
6464
});
6565

6666
this.Then(/^a `Protocol Error` is raised$/, function () {
67-
var message = this.err.fields[0].message;
68-
var code = this.err.fields[0].code;
67+
var message = this.err.message;
68+
var code = this.err.code;
6969

7070
var expectedStartOfMessage = 'The client is unauthorized due to authentication failure.';
7171
var expectedCode = 'Neo.ClientError.Security.Unauthorized';

test/v1/tck/steps/erroreportingsteps.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ module.exports = function () {
5858

5959
this.When(/^I run a non valid cypher statement$/, function (callback) {
6060
var self = this;
61-
this.session.run("CRETE (:n)").then(function(result) {callback()}).catch(function(err) {self.error = err.fields[0]; callback()})
61+
this.session.run("CRETE (:n)").then(function(result) {callback()}).catch(function(err) {self.error = err; callback()})
6262
});
6363

6464
this.When(/^I set up a driver to an incorrect port$/, function (callback) {

test/v1/transaction.test.js

+7-8
Original file line numberDiff line numberDiff line change
@@ -77,20 +77,20 @@ describe('transaction', () => {
7777

7878
it('should handle failures with subscribe', done => {
7979
const tx = session.beginTransaction();
80-
tx.run("THIS IS NOT CYPHER")
80+
tx.run('THIS IS NOT CYPHER')
8181
.catch(error => {
82-
expect(error.fields.length).toBe(1);
82+
expect(error.code).toEqual('Neo.ClientError.Statement.SyntaxError');
8383
driver.close();
8484
done();
8585
});
8686
});
8787

8888
it('should handle failures with catch', done => {
8989
const tx = session.beginTransaction();
90-
tx.run("THIS IS NOT CYPHER")
90+
tx.run('THIS IS NOT CYPHER')
9191
.subscribe({
9292
onError: error => {
93-
expect(error.fields.length).toBe(1);
93+
expect(error.code).toEqual('Neo.ClientError.Statement.SyntaxError');
9494
driver.close();
9595
done();
9696
}
@@ -297,7 +297,7 @@ describe('transaction', () => {
297297
const invalidBookmark = 'hi, this is an invalid bookmark';
298298
const tx = session.beginTransaction(invalidBookmark);
299299
tx.run('RETURN 1').catch(error => {
300-
expect(error.fields[0].code).toBe('Neo.ClientError.Transaction.InvalidBookmark');
300+
expect(error.code).toBe('Neo.ClientError.Transaction.InvalidBookmark');
301301
done();
302302
});
303303
});
@@ -317,7 +317,7 @@ describe('transaction', () => {
317317
const unreachableBookmark = session.lastBookmark() + "0";
318318
const tx2 = session.beginTransaction(unreachableBookmark);
319319
tx2.run('CREATE ()').catch(error => {
320-
const message = error.fields[0].message;
320+
const message = error.message;
321321
const expectedPrefix = message.indexOf('Database not up to the requested version') === 0;
322322
expect(expectedPrefix).toBeTruthy();
323323
done();
@@ -445,8 +445,7 @@ describe('transaction', () => {
445445
});
446446

447447
function expectSyntaxError(error) {
448-
const code = error.fields[0].code;
449-
expect(code).toBe('Neo.ClientError.Statement.SyntaxError');
448+
expect(error.code).toBe('Neo.ClientError.Statement.SyntaxError');
450449
}
451450

452451
function expectValidLastBookmark(session) {

0 commit comments

Comments
 (0)