fix: prevent NRE on transport level gRPC errors#405
Conversation
Review Summary by QodoHandle null GetRpcStatus() to prevent NRE on transport errors
WalkthroughsDescription• Replace null-forgiving operator with null-coalescing throw in exception factories • Use null-conditional operator in MultiAppend catch block for safe status access • Prevent NullReferenceException on transport-level gRPC errors • Surface original RpcException instead of masking with NRE Diagramflowchart LR
A["RpcException with null Status"] -->|GetRpcStatus()!| B["NullReferenceException"]
A -->|GetRpcStatus() ?? throw ex| C["Original RpcException surfaced"]
D["MultiAppend catch block"] -->|ex.GetRpcStatus()!| E["NRE on null status"]
D -->|ex.GetRpcStatus()?| F["Safe null-conditional access"]
File Changes1. src/KurrentDB.Client/Core/Exceptions/AccessDeniedException.cs
|
Code Review by Qodo
|
There was a problem hiding this comment.
Pull request overview
Fixes a crash path in the client’s gRPC error handling where RpcException.GetRpcStatus() can be null (transport-level failures), previously causing NullReferenceException and masking the original RpcException.
Changes:
- Update
MultiStreamAppendAsyncto use null-conditional access when readingErrorInfofromGetRpcStatus(). - Update multiple
FromRpcExceptionfactory methods to avoid null-forgivingGetRpcStatus()!and instead fall back to throwing theRpcExceptionwhen noGoogle.Rpc.Statusis present.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs | Avoid NRE when GetRpcStatus() is null during multi-append error mapping. |
| src/KurrentDB.Client/Streams/MaximumAppendSizeExceededException.cs | Guard against null GetRpcStatus() in exception factory method. |
| src/KurrentDB.Client/Core/Exceptions/WrongExpectedVersionException.cs | Guard against null GetRpcStatus() in exception factory method. |
| src/KurrentDB.Client/Core/Exceptions/StreamTombstonedException.cs | Guard against null GetRpcStatus() in exception factory method. |
| src/KurrentDB.Client/Core/Exceptions/NotLeaderException.cs | Guard against null GetRpcStatus() in exception factory method. |
| src/KurrentDB.Client/Core/Exceptions/AppendTransactionMaxSizeExceededException.cs | Guard against null GetRpcStatus() in exception factory method. |
| src/KurrentDB.Client/Core/Exceptions/AccessDeniedException.cs | Guard against null GetRpcStatus() in exception factory method. |
Comments suppressed due to low confidence (1)
src/KurrentDB.Client/Streams/KurrentDBClient.MultiAppend.cs:88
- The default arm returns
ex, which results inthrow ex;and resets the original gRPC stack trace. Since this is inside acatch (RpcException ex), consider restructuring (e.g., assign mapped exception, and usethrow;when no mapping applies) so transport-level failures truly rethrow the original exception without losing stack information.
{ Reason: "STREAM_TOMBSTONED" } => StreamTombstonedException.FromRpcException(ex),
{ Reason: "APPEND_RECORD_SIZE_EXCEEDED" } => AppendRecordSizeExceededException.FromRpcException(ex),
{ Reason: "APPEND_TRANSACTION_SIZE_EXCEEDED" } => AppendTransactionMaxSizeExceededException.FromRpcException(ex),
_ => ex
};
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
GetRpcStatus()can return null for transport-level gRPC errors (connection drops, timeouts), but the code assumed it never would. This caused aNullReferenceExceptionthat masked the actual error. Now the originalRpcExceptionis surfaced instead.