Skip to content

Commit 64f3126

Browse files
committed
feat: Add centralized exception handling and new exception types for authentication and authorization
1 parent 1e5c9b3 commit 64f3126

File tree

7 files changed

+261
-37
lines changed

7 files changed

+261
-37
lines changed

src/Weaviate.Client/Errors.cs

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,78 @@ public override string ToString()
132132
}
133133
}
134134

135-
// TODO WeaviateUnauthorizedException, WeaviateUnauthenticatedException, WeaviateBadRequestException
135+
/// <summary>
136+
/// Exception thrown when authentication fails (HTTP 401 Unauthorized or gRPC UNAUTHENTICATED).
137+
/// This indicates that the request lacks valid authentication credentials.
138+
/// </summary>
139+
public class WeaviateAuthenticationException : WeaviateServerException
140+
{
141+
public const string DefaultMessage =
142+
"Authentication failed. Please check your API key or authentication credentials.";
143+
144+
public WeaviateAuthenticationException(string? message = null, Exception? innerException = null)
145+
: base(message ?? DefaultMessage, innerException) { }
146+
}
147+
148+
/// <summary>
149+
/// Exception thrown when authorization fails (HTTP 403 Forbidden or gRPC PERMISSION_DENIED).
150+
/// This indicates that the authenticated user does not have permission to perform the requested operation.
151+
/// </summary>
152+
public class WeaviateAuthorizationException : WeaviateServerException
153+
{
154+
public const string DefaultMessage =
155+
"Authorization failed. You do not have permission to perform this operation.";
156+
157+
public WeaviateAuthorizationException(string? message = null, Exception? innerException = null)
158+
: base(message ?? DefaultMessage, innerException) { }
159+
}
160+
161+
/// <summary>
162+
/// Exception thrown when a collection limit has been reached (HTTP 422 Unprocessable Entity).
163+
/// This typically occurs when trying to create more collections than allowed by the server configuration.
164+
/// </summary>
165+
public class WeaviateCollectionLimitReachedException : WeaviateServerException
166+
{
167+
public const string DefaultMessage =
168+
"Collection limit reached. Cannot create more collections than allowed by the server configuration.";
169+
170+
public WeaviateCollectionLimitReachedException(
171+
string? message = null,
172+
Exception? innerException = null
173+
)
174+
: base(message ?? DefaultMessage, innerException) { }
175+
}
176+
177+
/// <summary>
178+
/// Exception thrown when a required module is not available or enabled (HTTP 422 Unprocessable Entity).
179+
/// This occurs when attempting to use a feature that requires a module that is not configured on the server.
180+
/// </summary>
181+
public class WeaviateModuleNotAvailableException : WeaviateServerException
182+
{
183+
public const string DefaultMessage =
184+
"Required module is not available or enabled on the Weaviate server. Please check the server's module configuration.";
185+
186+
public WeaviateModuleNotAvailableException(
187+
string? message = null,
188+
Exception? innerException = null
189+
)
190+
: base(message ?? DefaultMessage, innerException) { }
191+
}
192+
193+
/// <summary>
194+
/// Exception thrown when an external module encounters a problem (HTTP 500 Internal Server Error).
195+
/// This typically indicates an issue with a vectorizer, generative module, or other external service integration.
196+
/// </summary>
197+
public class WeaviateExternalModuleProblemException : WeaviateServerException
198+
{
199+
public const string DefaultMessage =
200+
"An external module encountered a problem. This may be related to vectorizers, generative modules, or other external service integrations.";
201+
202+
public WeaviateExternalModuleProblemException(
203+
string? message = null,
204+
Exception? innerException = null
205+
)
206+
: base(message ?? DefaultMessage, innerException) { }
207+
}
208+
209+
// TODO WeaviateBadRequestException
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System.Net;
2+
3+
namespace Weaviate.Client;
4+
5+
/// <summary>
6+
/// Helper class to centralize exception mapping logic for both REST and gRPC APIs.
7+
/// Provides consistent exception handling across different protocols.
8+
/// </summary>
9+
internal static class ExceptionHelper
10+
{
11+
/// <summary>
12+
/// Maps an HTTP status code and error message to the appropriate Weaviate exception.
13+
/// </summary>
14+
/// <param name="statusCode">The HTTP status code</param>
15+
/// <param name="errorMessage">The error message from the server</param>
16+
/// <param name="innerException">The original exception</param>
17+
/// <param name="resourceType">Optional resource type for context</param>
18+
/// <returns>The appropriate WeaviateException based on status code and message</returns>
19+
public static WeaviateException MapHttpException(
20+
HttpStatusCode statusCode,
21+
string errorMessage,
22+
Exception innerException,
23+
ResourceType resourceType = ResourceType.Unknown
24+
)
25+
{
26+
// Check status code first
27+
WeaviateException? exception = statusCode switch
28+
{
29+
HttpStatusCode.Unauthorized => new WeaviateAuthenticationException(
30+
null,
31+
innerException
32+
),
33+
HttpStatusCode.Forbidden => new WeaviateAuthorizationException(null, innerException),
34+
HttpStatusCode.NotFound => new WeaviateNotFoundException(
35+
(Rest.WeaviateUnexpectedStatusCodeException)innerException,
36+
resourceType
37+
),
38+
HttpStatusCode.Conflict => new WeaviateConflictException(
39+
$"Conflict accessing {resourceType}",
40+
innerException
41+
),
42+
_ => null,
43+
};
44+
45+
if (exception != null)
46+
{
47+
return exception;
48+
}
49+
50+
// For 422 and 500, try to map based on error message
51+
if (
52+
statusCode == HttpStatusCode.UnprocessableEntity
53+
|| statusCode == HttpStatusCode.InternalServerError
54+
)
55+
{
56+
var messageBasedException = MapErrorMessage(errorMessage, innerException);
57+
if (messageBasedException != null)
58+
{
59+
return messageBasedException;
60+
}
61+
}
62+
63+
// Re-throw the original exception if we can't map it
64+
throw innerException;
65+
}
66+
67+
/// <summary>
68+
/// Maps a gRPC RpcException to the appropriate Weaviate exception.
69+
/// </summary>
70+
/// <param name="rpcException">The gRPC RpcException</param>
71+
/// <param name="defaultMessage">The default error message if no specific exception applies</param>
72+
/// <returns>The appropriate WeaviateException based on gRPC status and message</returns>
73+
public static WeaviateException MapGrpcException(
74+
global::Grpc.Core.RpcException rpcException,
75+
string defaultMessage
76+
)
77+
{
78+
// Check status code first
79+
switch (rpcException.StatusCode)
80+
{
81+
case global::Grpc.Core.StatusCode.Unauthenticated:
82+
return new WeaviateAuthenticationException(null, rpcException);
83+
84+
case global::Grpc.Core.StatusCode.PermissionDenied:
85+
return new WeaviateAuthorizationException(null, rpcException);
86+
87+
case global::Grpc.Core.StatusCode.Unimplemented:
88+
// This is typically used for feature not supported scenarios
89+
return new WeaviateFeatureNotSupportedException(null, rpcException);
90+
}
91+
92+
// Parse error message for specific error types
93+
var errorMessage = rpcException.Status.Detail ?? rpcException.Message;
94+
var messageBasedException = MapErrorMessage(errorMessage, rpcException);
95+
96+
if (messageBasedException != null)
97+
{
98+
return messageBasedException;
99+
}
100+
101+
// Default to generic server exception
102+
return new WeaviateServerException(defaultMessage, rpcException);
103+
}
104+
105+
/// <summary>
106+
/// Maps error messages to specific exceptions regardless of protocol.
107+
/// Returns null if no specific exception type matches.
108+
/// </summary>
109+
private static WeaviateException? MapErrorMessage(string errorMessage, Exception innerException)
110+
{
111+
if (string.IsNullOrEmpty(errorMessage))
112+
{
113+
return null;
114+
}
115+
116+
// Check for collection limit error
117+
if (
118+
errorMessage.Contains(
119+
"maximum number of collections",
120+
StringComparison.OrdinalIgnoreCase
121+
)
122+
)
123+
{
124+
return new WeaviateCollectionLimitReachedException(null, innerException);
125+
}
126+
127+
// Check for module not available error
128+
if (errorMessage.Contains("no module with name", StringComparison.OrdinalIgnoreCase))
129+
{
130+
return new WeaviateModuleNotAvailableException(null, innerException);
131+
}
132+
133+
// Check for vectorization/module execution errors
134+
if (errorMessage.Contains("could not vectorize", StringComparison.OrdinalIgnoreCase))
135+
{
136+
return new WeaviateExternalModuleProblemException(null, innerException);
137+
}
138+
139+
return null;
140+
}
141+
}

src/Weaviate.Client/Rest/Http.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -64,19 +64,11 @@ await response.EnsureExpectedStatusCodeAsync(
6464

6565
// TODO
6666
// HttpStatusCode.BadRequest
67-
// HttpStatusCode.Unauthorized
68-
// HttpStatusCode.Forbidden
69-
// HttpStatusCode.InternalServerError
7067
}
7168
catch (WeaviateUnexpectedStatusCodeException ex)
72-
when (ex.StatusCode == HttpStatusCode.NotFound)
7369
{
74-
throw new WeaviateNotFoundException(ex, resourceType);
75-
}
76-
catch (WeaviateUnexpectedStatusCodeException ex)
77-
when (ex.StatusCode == HttpStatusCode.Conflict)
78-
{
79-
throw new WeaviateConflictException($"Conflict accessing {resourceType}", ex);
70+
// Use centralized exception mapping helper
71+
throw ExceptionHelper.MapHttpException(ex.StatusCode, ex.Message, ex, resourceType);
8072
}
8173
}
8274

src/Weaviate.Client/gRPC/Aggregate.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,8 @@ private async Task<AggregateReply> Aggregate(
136136
}
137137
catch (global::Grpc.Core.RpcException ex)
138138
{
139-
// Provide clearer diagnostics if the server does not implement the Aggregate RPC yet.
140-
if (ex.StatusCode == global::Grpc.Core.StatusCode.Unimplemented)
141-
{
142-
throw new WeaviateFeatureNotSupportedException(
143-
"gRPC Aggregate method is not implemented on the connected Weaviate server.",
144-
ex
145-
);
146-
}
147-
148-
throw new WeaviateServerException("Aggregate request failed", ex);
139+
// Use centralized exception mapping helper
140+
throw ExceptionHelper.MapGrpcException(ex, "Aggregate request failed");
149141
}
150142
}
151143

src/Weaviate.Client/gRPC/Batch.cs

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,20 @@ internal async Task<BatchDeleteReply> DeleteMany(
2929
request.ConsistencyLevel = MapConsistencyLevel(consistencyLevel.Value);
3030
}
3131

32-
BatchDeleteReply reply = await _grpcClient.BatchDeleteAsync(
33-
request,
34-
CreateCallOptions(cancellationToken)
35-
);
32+
try
33+
{
34+
BatchDeleteReply reply = await _grpcClient.BatchDeleteAsync(
35+
request,
36+
CreateCallOptions(cancellationToken)
37+
);
3638

37-
return reply;
39+
return reply;
40+
}
41+
catch (global::Grpc.Core.RpcException ex)
42+
{
43+
// Use centralized exception mapping helper
44+
throw ExceptionHelper.MapGrpcException(ex, "Batch delete request failed");
45+
}
3846
}
3947

4048
internal async Task<BatchObjectsReply> InsertMany(
@@ -44,11 +52,19 @@ internal async Task<BatchObjectsReply> InsertMany(
4452
{
4553
var request = new BatchObjectsRequest { Objects = { objects } };
4654

47-
BatchObjectsReply reply = await _grpcClient.BatchObjectsAsync(
48-
request,
49-
CreateCallOptions(cancellationToken)
50-
);
55+
try
56+
{
57+
BatchObjectsReply reply = await _grpcClient.BatchObjectsAsync(
58+
request,
59+
CreateCallOptions(cancellationToken)
60+
);
5161

52-
return reply;
62+
return reply;
63+
}
64+
catch (global::Grpc.Core.RpcException ex)
65+
{
66+
// Use centralized exception mapping helper
67+
throw ExceptionHelper.MapGrpcException(ex, "Batch insert request failed");
68+
}
5369
}
5470
}

src/Weaviate.Client/gRPC/Search.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ private async Task<SearchReply> Search(
2222
}
2323
catch (global::Grpc.Core.RpcException ex)
2424
{
25-
throw new WeaviateServerException("Search request failed", ex);
25+
// Use centralized exception mapping helper
26+
throw ExceptionHelper.MapGrpcException(ex, "Search request failed");
2627
}
2728
}
2829

src/Weaviate.Client/gRPC/Tenant.cs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,19 @@ internal partial class WeaviateGrpcClient
1717
request.Names = new TenantNames { Values = { tenantNames } };
1818
}
1919

20-
TenantsGetReply reply = await _grpcClient.TenantsGetAsync(
21-
request,
22-
CreateCallOptions(cancellationToken)
23-
);
20+
try
21+
{
22+
TenantsGetReply reply = await _grpcClient.TenantsGetAsync(
23+
request,
24+
CreateCallOptions(cancellationToken)
25+
);
2426

25-
return reply.Tenants;
27+
return reply.Tenants;
28+
}
29+
catch (global::Grpc.Core.RpcException ex)
30+
{
31+
// Use centralized exception mapping helper
32+
throw ExceptionHelper.MapGrpcException(ex, "Tenants get request failed");
33+
}
2634
}
2735
}

0 commit comments

Comments
 (0)