Add IDeadLetterInterceptor: an exception-handling seam for durable dead letters#3046
Merged
Conversation
Invoke registered IDeadLetterInterceptor implementations for each envelope just before it is written to durable dead-letter storage, at the single MessageContext.MoveToDeadLetterQueueAsync chokepoint. Interceptors may mutate the envelope (for example to redact or encrypt the body) and/or return a replacement exception to persist; they run in registration order, each receiving the exception returned by the previous one. This is a transformation seam for durable dead letters - useful for redacting PII or secrets, encrypting dead-letter payloads at rest, or scrubbing sensitive exception text before storage. It is resolved from the application container and is a no-op (zero overhead, no behavior change) when none are registered. Not invoked for broker-native dead-lettering.
Add IDeadLetterExceptionInfo so an exception can declare the exception type and message written to dead-letter storage independently of its runtime type, resolved through DeadLetterExceptionExtensions at each store's capture site (RDBMS, RavenDB, Oracle, Cosmos DB). Together with IDeadLetterInterceptor this lets a redacting interceptor scrub the exception message while preserving the original exception type, so operators keep the ability to filter and triage dead letters by type. Behavior is unchanged for exceptions that do not implement the marker.
Clarify that the hook's distinctive use is scrubbing or replacing the exception recorded with a dead letter (which is stored outside the message serializer), and that encrypting or redacting the message body at rest is usually better done with a custom IMessageSerializer, which also covers the wire and the durable inbox/outbox. The hook can still mutate the body for dead-letter-only transforms.
The remarks recommended a custom IMessageSerializer over this hook for body-at-rest encryption. That is editorial steering toward a different feature and a tradeoff judgement that does not belong in the interface contract; it fits the PR discussion instead. Keep the factual note that the hook can mutate the envelope body.
There was a problem hiding this comment.
Pull request overview
This PR introduces a new extensibility seam for durable dead-letter persistence, allowing applications to intercept a failing envelope just before it’s written to Wolverine’s database-backed dead-letter storage so they can redact/replace exception info (and optionally mutate the envelope).
Changes:
- Add
IDeadLetterInterceptor(DI-resolved, ordered) invoked before persisting to durable dead-letter storage. - Add
IDeadLetterExceptionInfo+DeadLetterExceptionExtensionsto decouple persisted exception type/message from the runtime exception type/message. - Update durable dead-letter persistence implementations (RDBMS, Oracle, RavenDB, CosmosDB) to persist exception strings via the new extension methods; add core tests for chaining + exception info behavior.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/Wolverine/Runtime/MessageContext.cs | Adds interceptor invocation before calling durable dead-letter storage (for the non-native DLQ path). |
| src/Wolverine/Persistence/Durability/IDeadLetterInterceptor.cs | Introduces the interceptor contract + documentation for redaction/mutation use cases. |
| src/Wolverine/Persistence/Durability/IDeadLetterExceptionInfo.cs | Adds opt-in exception info contract + helpers to compute persisted type/message. |
| src/Testing/CoreTests/ErrorHandling/dead_letter_interceptor.cs | Tests that interceptors run in order and chain the exception result. |
| src/Testing/CoreTests/ErrorHandling/dead_letter_exception_info.cs | Tests fallback behavior + IDeadLetterExceptionInfo override behavior. |
| src/Persistence/Wolverine.RDBMS/DatabasePersistence.cs | Uses DeadLetterExceptionType/Message() for persisted exception strings. |
| src/Persistence/Wolverine.RavenDb/Internals/DeadLetterMessage.cs | Uses DeadLetterExceptionType/Message() when building Raven dead-letter docs. |
| src/Persistence/Wolverine.CosmosDb/Internals/DeadLetterMessage.cs | Uses DeadLetterExceptionType/Message() when building Cosmos dead-letter docs. |
| src/Persistence/Oracle/Wolverine.Oracle/OracleMessageStore.Incoming.cs | Uses DeadLetterExceptionType/Message() for Oracle dead-letter persistence. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+396
to
+404
| var ex = await interceptDeadLetterAsync(envelope, exception).ConfigureAwait(false); | ||
| await Storage.Inbox.MoveToDeadLetterStorageAsync(envelope, ex).ConfigureAwait(false); | ||
| } | ||
| } | ||
| else | ||
| { | ||
| // If persistable, persist | ||
| await Storage.Inbox.MoveToDeadLetterStorageAsync(Envelope, exception).ConfigureAwait(false); | ||
| var ex = await interceptDeadLetterAsync(Envelope, exception).ConfigureAwait(false); | ||
| await Storage.Inbox.MoveToDeadLetterStorageAsync(Envelope, ex).ConfigureAwait(false); |
Comment on lines
+415
to
+424
| private async ValueTask<Exception?> interceptDeadLetterAsync(Envelope envelope, Exception? exception) | ||
| { | ||
| foreach (var interceptor in Runtime.Services.GetServices<IDeadLetterInterceptor>()) | ||
| { | ||
| exception = await interceptor.BeforeStoreAsync(envelope, exception, Runtime.Cancellation) | ||
| .ConfigureAwait(false); | ||
| } | ||
|
|
||
| return exception; | ||
| } |
This was referenced Jun 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add
IDeadLetterInterceptor, a hook invoked before an envelope is written to durable dead-letter storage. Lets you scrub or replace the exception recorded with a dead letter (handlers sometimes put sensitive data in exception messages, which are stored outside the serializer) and optionally mutate the body. Registered via DI, runs in order chaining the returned exception, no-op when unused, durable path only.IDeadLetterExceptionInfo(opt-in) lets a replacement exception keep the originalexception_typewhile redacting the message, so type-based triage still works.