diff --git a/docs/guide/messaging/header-propagation.md b/docs/guide/messaging/header-propagation.md
index 436c97fd5..eee49f1b2 100644
--- a/docs/guide/messaging/header-propagation.md
+++ b/docs/guide/messaging/header-propagation.md
@@ -1,6 +1,32 @@
# Header Propagation
-When consuming messages from external systems, those messages may carry custom headers that need to flow through to any downstream messages your handlers produce. Use `PropagateIncomingHeadersToOutgoing` to declare which headers should be forwarded automatically:
+Wolverine can automatically forward headers from an incoming message to all outgoing messages produced within the same handler context. This is useful for propagating correlation identifiers, tracing metadata, or contextual information like "on-behalf-of" across a chain of messages.
+
+## Propagating a Single Header
+
+Use `PropagateIncomingHeaderToOutgoing` when you need to forward just one header:
+
+```csharp
+builder.Host.UseWolverine(opts =>
+{
+ // Forward the "on-behalf-of" header to all downstream messages
+ opts.Policies.PropagateIncomingHeaderToOutgoing("x-on-behalf-of");
+});
+```
+
+This is ideal for scenarios like middleware that detects delegated user actions and marks outgoing messages to support downstream logging and auditing:
+
+```csharp
+// In your middleware, set the header on the incoming envelope:
+context.Envelope.Headers["x-on-behalf-of"] = impersonatedUser;
+
+// Any messages published within this handler context will
+// automatically carry the "x-on-behalf-of" header
+```
+
+## Propagating Multiple Headers
+
+Use `PropagateIncomingHeadersToOutgoing` to forward several headers at once:
```csharp
builder.Host.UseWolverine(opts =>
@@ -9,6 +35,46 @@ builder.Host.UseWolverine(opts =>
});
```
-When a handler receives a message carrying any of the named headers, Wolverine will copy those headers onto every outgoing message cascaded within that handler context. Headers not present on the incoming message are silently skipped.
+## Behavior
+
+When a handler receives a message carrying any of the named headers, Wolverine will copy those headers onto every outgoing message cascaded within that handler context. Headers not present on the incoming message are silently skipped — no errors are thrown.
+
+This works across all transports — Kafka, RabbitMQ, Azure Service Bus, or any other.
+
+::: warning
+The headers must be present on the incoming `Envelope` at the point the handler runs. Wolverine's default envelope mappers only carry Wolverine's own metadata headers, so if you need to propagate custom headers from an external producer you will need a custom envelope mapper that explicitly reads those headers from the transport message and sets them on the envelope.
+:::
+
+## Custom Envelope Rules
+
+For more advanced header manipulation, you can implement a custom `IEnvelopeRule`:
+
+```csharp
+public class OnBehalfOfRule : IEnvelopeRule
+{
+ // Called when publishing outside a handler context
+ public void Modify(Envelope envelope) { }
+
+ // Called within a handler context with access to the incoming message
+ public void ApplyCorrelation(IMessageContext originator, Envelope outgoing)
+ {
+ var incoming = originator.Envelope;
+ if (incoming is null) return;
+
+ if (incoming.Headers.TryGetValue("x-on-behalf-of", out var value))
+ {
+ outgoing.Headers["x-on-behalf-of"] = value;
+ outgoing.Headers["x-audit-trail"] = $"delegated:{value}";
+ }
+ }
+}
+```
+
+Register your custom rule as a metadata rule:
-This works across all transports — Kafka, RabbitMQ, Azure Service Bus, or any other. The headers must be present on the incoming `Envelope` at the point the handler runs. Wolverine's default envelope mappers only carry Wolverine's own metadata headers, so if you need to propagate custom headers from an external producer you will need a custom envelope mapper that explicitly reads those headers from the transport message and sets them on the envelope.
+```csharp
+builder.Host.UseWolverine(opts =>
+{
+ opts.MetadataRules.Add(new OnBehalfOfRule());
+});
+```
diff --git a/docs/guide/messaging/message-bus.md b/docs/guide/messaging/message-bus.md
index 3bf9f59cb..0706cc60d 100644
--- a/docs/guide/messaging/message-bus.md
+++ b/docs/guide/messaging/message-bus.md
@@ -456,6 +456,10 @@ public static async Task SendMessagesWithDeliveryOptions(IMessageBus bus)
snippet source | anchor
+::: tip
+If you need to automatically forward headers from incoming messages to all outgoing messages within a handler context, see [Header Propagation](/guide/messaging/header-propagation).
+:::
+
## Sending Raw Message Data
In some particular cases, you may want to use Wolverine to send a message to another system (or the same system)
diff --git a/src/Testing/CoreTests/PropagateHeadersRuleTests.cs b/src/Testing/CoreTests/PropagateHeadersRuleTests.cs
index 469529014..244d4c8c0 100644
--- a/src/Testing/CoreTests/PropagateHeadersRuleTests.cs
+++ b/src/Testing/CoreTests/PropagateHeadersRuleTests.cs
@@ -68,3 +68,66 @@ public void no_op_when_there_is_no_incoming_envelope()
outgoing.Headers.ContainsKey("x-custom").ShouldBeFalse();
}
}
+
+public class PropagateOneHeaderRuleTests
+{
+ [Fact]
+ public void modify_is_a_no_op_outside_of_a_handler_context()
+ {
+ var rule = new PropagateOneHeaderRule("x-on-behalf-of");
+ var envelope = ObjectMother.Envelope();
+
+ rule.Modify(envelope);
+
+ envelope.Headers.ContainsKey("x-on-behalf-of").ShouldBeFalse();
+ }
+
+ [Fact]
+ public void copies_single_header_from_incoming_to_outgoing()
+ {
+ var rule = new PropagateOneHeaderRule("x-on-behalf-of");
+
+ var incoming = ObjectMother.Envelope();
+ incoming.Headers["x-on-behalf-of"] = "admin-user";
+ incoming.Headers["x-other"] = "should-not-propagate";
+
+ var context = Substitute.For();
+ context.Envelope.Returns(incoming);
+
+ var outgoing = ObjectMother.Envelope();
+ rule.ApplyCorrelation(context, outgoing);
+
+ outgoing.Headers["x-on-behalf-of"].ShouldBe("admin-user");
+ outgoing.Headers.ContainsKey("x-other").ShouldBeFalse();
+ }
+
+ [Fact]
+ public void header_not_present_on_incoming_is_silently_skipped()
+ {
+ var rule = new PropagateOneHeaderRule("x-on-behalf-of");
+
+ var incoming = ObjectMother.Envelope();
+
+ var context = Substitute.For();
+ context.Envelope.Returns(incoming);
+
+ var outgoing = ObjectMother.Envelope();
+ rule.ApplyCorrelation(context, outgoing);
+
+ outgoing.Headers.ContainsKey("x-on-behalf-of").ShouldBeFalse();
+ }
+
+ [Fact]
+ public void no_op_when_there_is_no_incoming_envelope()
+ {
+ var rule = new PropagateOneHeaderRule("x-on-behalf-of");
+
+ var context = Substitute.For();
+ context.Envelope.Returns((Envelope?)null);
+
+ var outgoing = ObjectMother.Envelope();
+ rule.ApplyCorrelation(context, outgoing);
+
+ outgoing.Headers.ContainsKey("x-on-behalf-of").ShouldBeFalse();
+ }
+}
diff --git a/src/Wolverine/IEnvelopeRule.cs b/src/Wolverine/IEnvelopeRule.cs
index 916173f8b..e3ebf900f 100644
--- a/src/Wolverine/IEnvelopeRule.cs
+++ b/src/Wolverine/IEnvelopeRule.cs
@@ -178,6 +178,34 @@ public void ApplyCorrelation(IMessageContext originator, Envelope outgoing)
}
}
+///
+/// Propagates a single named header from the incoming envelope to the outgoing envelope
+/// if it exists. This is a convenience rule for the common case of forwarding just one header.
+///
+internal class PropagateOneHeaderRule : IEnvelopeRule
+{
+ private readonly string _headerName;
+
+ public PropagateOneHeaderRule(string headerName)
+ {
+ _headerName = headerName;
+ }
+
+ // No incoming context available outside a handler — nothing to propagate
+ public void Modify(Envelope envelope) { }
+
+ public void ApplyCorrelation(IMessageContext originator, Envelope outgoing)
+ {
+ var incoming = originator.Envelope;
+ if (incoming is null) return;
+
+ if (incoming.Headers.TryGetValue(_headerName, out var value))
+ {
+ outgoing.Headers[_headerName] = value;
+ }
+ }
+}
+
internal class LambdaEnvelopeRule : IEnvelopeRule
{
private readonly Action _configure;
diff --git a/src/Wolverine/IPolicies.cs b/src/Wolverine/IPolicies.cs
index d9da229ec..64f21db1c 100644
--- a/src/Wolverine/IPolicies.cs
+++ b/src/Wolverine/IPolicies.cs
@@ -185,4 +185,11 @@ public interface IPolicies : IEnumerable, IWithFailurePolicies
/// message are silently skipped.
///
void PropagateIncomingHeadersToOutgoing(params string[] headerNames);
+
+ ///
+ /// Automatically propagate a single named header from an incoming message to all outgoing
+ /// messages cascaded within the same handler context. If the header is not present on the
+ /// incoming message it is silently skipped.
+ ///
+ void PropagateIncomingHeaderToOutgoing(string headerName);
}
\ No newline at end of file
diff --git a/src/Wolverine/WolverineOptions.Policies.cs b/src/Wolverine/WolverineOptions.Policies.cs
index 9e5e40b62..193454258 100644
--- a/src/Wolverine/WolverineOptions.Policies.cs
+++ b/src/Wolverine/WolverineOptions.Policies.cs
@@ -293,6 +293,14 @@ void IPolicies.PropagateIncomingHeadersToOutgoing(params string[] headerNames)
MetadataRules.Add(new PropagateHeadersRule(headerNames));
}
+ void IPolicies.PropagateIncomingHeaderToOutgoing(string headerName)
+ {
+ if (string.IsNullOrWhiteSpace(headerName))
+ throw new ArgumentException("A header name is required", nameof(headerName));
+
+ MetadataRules.Add(new PropagateOneHeaderRule(headerName));
+ }
+
internal MiddlewarePolicy FindOrCreateMiddlewarePolicy()
{
var policy = RegisteredPolicies.OfType().FirstOrDefault();