Skip to content

Commit d16bfb9

Browse files
Gql Errors (#822)
* Errors desrialized into failure message * testkit tests passing * XML doc * Remove unused using * Fix unit test * Fix test * Fix failing testkit test; movew gql errors into preview
1 parent c826f5f commit d16bfb9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+811
-225
lines changed

Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/Exceptions/ExceptionManager.cs

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
1414
// limitations under the License.
1515

1616
using System;
17+
using System.Linq;
1718
using System.Collections.Generic;
1819
using System.Diagnostics;
20+
using Neo4j.Driver.Internal;
1921
using Neo4j.Driver.Internal.Connector;
22+
using Neo4j.Driver.Preview.GqlErrors;
2023
using Neo4j.Driver.Tests.TestBackend.Protocol;
24+
using Neo4j.Driver.Tests.TestBackend.Types;
2125

2226
namespace Neo4j.Driver.Tests.TestBackend.Exceptions;
2327
//TransientException = DriverError
@@ -50,7 +54,7 @@ internal static class ExceptionManager
5054
{ typeof(DatabaseException), "DatabaseError" },
5155
{ typeof(ServiceUnavailableException), "ServiceUnavailableError" },
5256
{ typeof(SessionExpiredException), "SessionExpiredError" },
53-
{ typeof(Driver.ProtocolException), "ProtocolError" },
57+
{ typeof(ProtocolException), "ProtocolError" },
5458
{ typeof(SecurityException), "SecurityError" },
5559
{ typeof(AuthenticationException), "AuthenticationError" },
5660
{ typeof(AuthorizationException), "AuthorizationExpired" },
@@ -70,34 +74,23 @@ internal static class ExceptionManager
7074
{ typeof(TypeException), "TypeError" },
7175
{ typeof(ForbiddenException), "ForbiddenError" },
7276
{ typeof(UnknownSecurityException), "OtherSecurityException" },
73-
{ typeof(ReauthException), "UnsupportedFeatureException"},
77+
{ typeof(ReauthException), "UnsupportedFeatureException" },
7478
{ typeof(TransactionTerminatedException), "TransactionTerminatedError" }
7579
};
7680

7781
internal static ProtocolResponse GenerateExceptionResponse(Exception ex)
7882
{
79-
var outerExceptionMessage = ex.Message;
80-
var exceptionMessage = ex.InnerException != null ? ex.InnerException.Message : ex.Message;
81-
8283
var type = TypeMap.GetValueOrDefault(ex.GetType());
84+
//var exceptionMessage = ex.InnerException != null ? ex.InnerException.Message : ex.Message;
8385

84-
//if (ex is Neo4jException || ex is NotSupportedException)
8586
if (type is not null)
8687
{
8788
var newError = ProtocolObjectFactory.CreateObject<Protocol.ProtocolException>();
8889
newError.ExceptionObj = ex;
89-
var errorCode = ex is Neo4jException ? ((Neo4jException)ex).Code : type;
90-
var isRetriable = ex is Neo4jException ? ((Neo4jException)ex).IsRetriable : false;
91-
return new ProtocolResponse(
92-
"DriverError",
93-
new
94-
{
95-
id = newError.uniqueId,
96-
errorType = type,
97-
msg = exceptionMessage,
98-
code = errorCode,
99-
retryable = isRetriable
100-
});
90+
var data = CreateExceptionDictionary(ex, type, ex.Message);
91+
data["id"] = newError.uniqueId;
92+
93+
return new ProtocolResponse("DriverError", data);
10194
}
10295

10396
if (ex is DriverExceptionWrapper)
@@ -109,7 +102,7 @@ internal static ProtocolResponse GenerateExceptionResponse(Exception ex)
109102
{
110103
id = newError.uniqueId,
111104
errorType = ex.InnerException?.GetType().Name ?? ex.GetType().Name,
112-
msg = exceptionMessage,
105+
msg = ex.InnerException != null ? ex.InnerException.Message : ex.Message,
113106
retryable = false
114107
});
115108
}
@@ -123,9 +116,51 @@ internal static ProtocolResponse GenerateExceptionResponse(Exception ex)
123116
msg = ex.Message
124117
});
125118
}
126-
119+
127120
Trace.WriteLine($"Unhandled exception thrown {ex}");
128121

129-
return new ProtocolResponse("BackendError", new { msg = exceptionMessage });
122+
return new ProtocolResponse("BackendError", new { msg = ex.Message });
123+
}
124+
125+
private static Dictionary<string, object> CreateExceptionDictionary(
126+
Exception ex,
127+
string type,
128+
string exceptionMessage,
129+
bool isCause = false)
130+
{
131+
var ne = ex as Neo4jException;
132+
var gqlError = ne?.GetGqlErrorPreview();
133+
var diagnosticRecord = gqlError?.GqlDiagnosticRecord?.ToDictionary(y => y.Key, y => NativeToCypher.Convert(y.Value));
134+
var data = new Dictionary<string, object>();
135+
136+
if (!isCause)
137+
{
138+
data.OverwriteFrom(
139+
("errorType", type),
140+
("code", ne?.Code ?? type),
141+
("retryable", ne?.IsRetriable ?? false));
142+
}
143+
144+
data.OverwriteFrom(
145+
null,
146+
("msg", exceptionMessage),
147+
("gqlStatus", gqlError?.GqlStatus),
148+
("statusDescription", gqlError?.GqlStatusDescription),
149+
("diagnosticRecord", diagnosticRecord),
150+
("rawClassification", gqlError?.GqlRawClassification),
151+
("classification", gqlError?.GqlClassification));
152+
153+
if (ne?.InnerException != null)
154+
{
155+
var exceptionDictionary = CreateExceptionDictionary(
156+
ne.InnerException,
157+
"GqlError",
158+
ne.InnerException.Message,
159+
true);
160+
161+
data["cause"] = new { name = "GqlError", data = exceptionDictionary };
162+
}
163+
164+
return data;
130165
}
131166
}

Neo4j.Driver/Neo4j.Driver.Tests.TestBackend/SupportedFeatures.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ static SupportedFeatures()
6666
"Feature:Bolt:5.4",
6767
"Feature:Bolt:5.5",
6868
"Feature:Bolt:5.6",
69+
"Feature:Bolt:5.7",
6970
"Feature:Bolt:Patch:UTC",
7071
"Feature:Impersonation",
7172
//"Feature:TLS:1.1",

Neo4j.Driver/Neo4j.Driver.Tests/Exceptions/Neo4jExceptionFactoryTests.cs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,37 +17,40 @@
1717

1818
using System;
1919
using System.Collections.Generic;
20+
using System.Diagnostics.CodeAnalysis;
2021
using FluentAssertions;
2122
using Neo4j.Driver.Internal.ExceptionHandling;
23+
using Neo4j.Driver.Internal.Messaging;
2224
using Xunit;
2325

2426
namespace Neo4j.Driver.Tests.Exceptions;
2527

2628
public class Neo4jExceptionFactoryTests
2729
{
28-
public static IEnumerable<object[]> CodeToTypeMapping = new[]
29-
{
30-
new object[] { "Neo.ClientError.Statement.ArgumentError", typeof(StatementArgumentException) },
31-
new object[] { "Neo.ClientError.Security.Unauthorized", typeof(AuthenticationException) },
32-
new object[] { "Neo.ClientError.Security.AuthorizationExpired", typeof(AuthorizationException) },
33-
new object[] { "Neo.ClientError.Database.DatabaseNotFound", typeof(FatalDiscoveryException) },
34-
new object[] { "Neo.ClientError.Security.Forbidden", typeof(ForbiddenException) },
35-
new object[] { "Neo.ClientError.Transaction.InvalidBookmark", typeof(InvalidBookmarkException) },
36-
new object[] { "Neo.ClientError.Transaction.InvalidBookmarkMixture", typeof(InvalidBookmarkMixtureException) },
37-
new object[] { "Neo.ClientError.Request.Invalid", typeof(ProtocolException) },
38-
new object[] { "Neo.ClientError.Request.InvalidFormat", typeof(ProtocolException) },
39-
new object[] { "Neo.ClientError.Security.TokenExpired", typeof(TokenExpiredException) },
40-
new object[] { "Neo.ClientError.Statement.TypeError", typeof(TypeException) },
41-
new object[] { "Neo.ClientError.Security.##unknown##", typeof(UnknownSecurityException) },
42-
new object[] { "Neo.DatabaseError.blah", typeof(DatabaseException) },
43-
new object[] { "Neo.TransientError.TemporaryDisabled", typeof(TransientException) },
44-
};
30+
[SuppressMessage("Usage", "CA2211:Non-constant fields should not be visible")]
31+
public static object[][] CodeToTypeMapping =
32+
[
33+
["Neo.ClientError.Statement.ArgumentError", typeof(StatementArgumentException)],
34+
["Neo.ClientError.Security.Unauthorized", typeof(AuthenticationException)],
35+
["Neo.ClientError.Security.AuthorizationExpired", typeof(AuthorizationException)],
36+
["Neo.ClientError.Database.DatabaseNotFound", typeof(FatalDiscoveryException)],
37+
["Neo.ClientError.Security.Forbidden", typeof(ForbiddenException)],
38+
["Neo.ClientError.Transaction.InvalidBookmark", typeof(InvalidBookmarkException)],
39+
["Neo.ClientError.Transaction.InvalidBookmarkMixture", typeof(InvalidBookmarkMixtureException)],
40+
["Neo.ClientError.Request.Invalid", typeof(ProtocolException)],
41+
["Neo.ClientError.Request.InvalidFormat", typeof(ProtocolException)],
42+
["Neo.ClientError.Security.TokenExpired", typeof(TokenExpiredException)],
43+
["Neo.ClientError.Statement.TypeError", typeof(TypeException)],
44+
["Neo.ClientError.Security.##unknown##", typeof(UnknownSecurityException)],
45+
["Neo.DatabaseError.blah", typeof(DatabaseException)],
46+
["Neo.TransientError.TemporaryDisabled", typeof(TransientException)]
47+
];
4548

4649
[Theory, MemberData(nameof(CodeToTypeMapping))]
4750
public void ShouldCreateCorrectExceptionType(string code, Type exceptionType)
4851
{
4952
var subject = new Neo4jExceptionFactory();
50-
var exception = subject.GetException(code, "test message");
53+
var exception = subject.GetException(new FailureMessage(code, "test message"));
5154
exception.Should().BeOfType(exceptionType);
5255
exception.Code.Should().Be(code);
5356
exception.Message.Should().Be("test message");
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) "Neo4j"
2+
// Neo4j Sweden AB [https://neo4j.com]
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License").
5+
// You may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
using System.Collections.Generic;
17+
using FluentAssertions;
18+
using Neo4j.Driver.Internal.ExceptionHandling;
19+
using Neo4j.Driver.Internal.IO.MessageSerializers;
20+
using Neo4j.Driver.Internal.Messaging;
21+
using Neo4j.Driver.Preview.GqlErrors;
22+
using Xunit;
23+
24+
namespace Neo4j.Driver.Tests.GqlCompliance;
25+
26+
public class GqlErrorTests
27+
{
28+
[Fact]
29+
public void BuildFailureMessage_ShouldDeserializeCorrectly()
30+
{
31+
// Arrange
32+
var values = new Dictionary<string, object>
33+
{
34+
{ "code", "Neo.ClientError.Transaction.Terminated" },
35+
{ "message", "Transaction terminated" },
36+
{ "gql_status", "GQL_STATUS" },
37+
{ "description", "Status description" },
38+
{ "diagnostic_record", new Dictionary<string, object> { { "key", "value" } } },
39+
{
40+
"cause",
41+
new Dictionary<string, object>
42+
{
43+
{ "code", "Neo.ClientError.Transaction.LockClientStopped" }, { "message", "Lock client stopped" }
44+
}
45+
}
46+
};
47+
48+
int majorVersion = 4;
49+
50+
// Act
51+
var result = FailureMessageSerializer.BuildFailureMessage(values, majorVersion);
52+
53+
// Assert
54+
result.Code.Should().Be("Neo.ClientError.Transaction.Terminated");
55+
result.Message.Should().Be("Transaction terminated");
56+
result.GqlStatus.Should().Be("GQL_STATUS");
57+
result.GqlStatusDescription.Should().Be("Status description");
58+
result.GqlDiagnosticRecord.Should().NotBeNull();
59+
result.GqlDiagnosticRecord["key"].Should().Be("value");
60+
result.GqlCause.Should().NotBeNull();
61+
result.GqlCause.Code.Should().Be("Neo.ClientError.Transaction.LockClientStopped");
62+
result.GqlCause.Message.Should().Be("Lock client stopped");
63+
64+
// Assert defaults added by FillGqlDefaults
65+
result.GqlDiagnosticRecord.Should().ContainKey("OPERATION");
66+
result.GqlDiagnosticRecord["OPERATION"].Should().Be("");
67+
result.GqlDiagnosticRecord.Should().ContainKey("OPERATION_CODE");
68+
result.GqlDiagnosticRecord["OPERATION_CODE"].Should().Be("0");
69+
result.GqlDiagnosticRecord.Should().ContainKey("CURRENT_SCHEMA");
70+
result.GqlDiagnosticRecord["CURRENT_SCHEMA"].Should().Be("/");
71+
72+
result.GqlRawClassification.Should().Be(null);
73+
result.GqlClassification.Should().Be("UNKNOWN");
74+
}
75+
76+
[Fact]
77+
public void GetException_ShouldHandleNestedFailureMessages()
78+
{
79+
// Arrange
80+
var innerFailureMessage = new FailureMessage
81+
{
82+
Code = "Neo.ClientError.Transaction.LockClientStopped",
83+
Message = "Lock client stopped",
84+
GqlStatus = "GQL_STATUS_INNER",
85+
GqlStatusDescription = "Inner status description",
86+
GqlDiagnosticRecord = new Dictionary<string, object> { { "inner_key", "inner_value" } },
87+
GqlRawClassification = "INNER_CLASSIFICATION",
88+
GqlClassification = "CLIENT_ERROR"
89+
};
90+
91+
var outerFailureMessage = new FailureMessage
92+
{
93+
Code = "Neo.ClientError.Transaction.Terminated",
94+
Message = "Transaction terminated",
95+
GqlStatus = "GQL_STATUS_OUTER",
96+
GqlStatusDescription = "Outer status description",
97+
GqlDiagnosticRecord = new Dictionary<string, object> { { "outer_key", "outer_value" } },
98+
GqlRawClassification = "OUTER_CLASSIFICATION",
99+
GqlClassification = "DATABASE_ERROR",
100+
GqlCause = innerFailureMessage
101+
};
102+
103+
var exceptionFactory = new Neo4jExceptionFactory();
104+
105+
// Act
106+
var result = exceptionFactory.GetException(outerFailureMessage);
107+
108+
// Assert
109+
result.Code.Should().Be("Neo.ClientError.Transaction.Terminated");
110+
result.Message.Should().Be("Transaction terminated");
111+
112+
var gqlError = result.GetGqlErrorPreview();
113+
gqlError.GqlStatus.Should().Be("GQL_STATUS_OUTER");
114+
gqlError.GqlStatusDescription.Should().Be("Outer status description");
115+
gqlError.GqlDiagnosticRecord.Should().NotBeNull();
116+
gqlError.GqlDiagnosticRecord["outer_key"].Should().Be("outer_value");
117+
gqlError.GqlRawClassification.Should().Be("OUTER_CLASSIFICATION");
118+
gqlError.GqlClassification.Should().Be("DATABASE_ERROR");
119+
120+
var innerException = (Neo4jException)result.InnerException;
121+
gqlError = innerException.GetGqlErrorPreview();
122+
innerException.Should().NotBeNull();
123+
innerException.Should().BeOfType<ClientException>();
124+
innerException.Code.Should().Be("Neo.ClientError.Transaction.LockClientStopped");
125+
innerException.Message.Should().Be("Lock client stopped");
126+
gqlError.GqlStatus.Should().Be("GQL_STATUS_INNER");
127+
gqlError.GqlStatusDescription.Should().Be("Inner status description");
128+
gqlError.GqlDiagnosticRecord.Should().NotBeNull();
129+
gqlError.GqlDiagnosticRecord["inner_key"].Should().Be("inner_value");
130+
gqlError.GqlRawClassification.Should().Be("INNER_CLASSIFICATION");
131+
gqlError.GqlClassification.Should().Be("CLIENT_ERROR");
132+
}
133+
}

Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/InMemoryPipelinedMessageReaderTests.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
using Moq;
2525
using Neo4j.Driver.Internal.IO;
2626
using Neo4j.Driver.Internal.MessageHandling;
27+
using Neo4j.Driver.Internal.Messaging;
2728
using Neo4j.Driver.Internal.Protocol;
2829
using Xunit;
2930

@@ -182,7 +183,7 @@ public async Task ShouldStopReadAllMessagesAfterAFailure()
182183
var pipeline = MockPipeline();
183184
await pipereader.ReadAsync(pipeline.Object, new MessageFormat(BoltProtocolVersion.V5_0,
184185
TestDriverContext.MockContext));
185-
pipeline.Verify(x => x.OnFailure("a", "b"), Times.Once);
186+
pipeline.Verify(x => x.OnFailure(It.IsAny<FailureMessage>()), Times.Once);
186187
pipeline.Verify(x => x.OnIgnored(), Times.Never);
187188
}
188189

Neo4j.Driver/Neo4j.Driver.Tests/Internal/MessageHandling/Messages/FailureMessageTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@ public void ShouldInvokeOnFailure()
4545
var message = new FailureMessage("e.g.Code", "e.g.Message");
4646
message.Dispatch(mockPipeline.Object);
4747

48-
mockPipeline.Verify(x => x.OnFailure("e.g.Code", "e.g.Message"));
48+
mockPipeline.Verify(x => x.OnFailure(message));
4949
}
5050
}

0 commit comments

Comments
 (0)