Skip to content

Revert message headers to their original state before auditing#7880

Open
warwickschroeder wants to merge 2 commits into
masterfrom
7739-revert-audit-message-headers
Open

Revert message headers to their original state before auditing#7880
warwickschroeder wants to merge 2 commits into
masterfrom
7739-revert-audit-message-headers

Conversation

@warwickschroeder

@warwickschroeder warwickschroeder commented Jul 23, 2026

Copy link
Copy Markdown

Closes: #7739
Built off PR: #7860

Problem

Mutations made to IncomingMessage.Headers during the incoming pipeline leak into the audit copy of the message. Anything that writes to the headers dictionary, e.g., a mutator, custom behavior, or core pipeline steps, mutates the same dictionary instance that InvokeAuditPipelineBehavior later snapshots into the OutgoingMessage sent to the audit queue.

The result is that the audit queue receives the processed headers rather than the headers as they arrived off the transport, so the audited message is not a faithful record of what was received.

IncomingMessage already guards against exactly this for the body: UpdateBody stashes the original and RevertToOriginalBodyIfNeeded restores it before the audit copy is taken. Headers had no equivalent.

Solution

IncomingMessage now captures the headers as they were at construction time and exposes an internal RevertToOriginalHeadersIfNeeded() that restores the dictionary to that exact state, reverting changed values, dropping added keys, and reinstating removed ones.

InvokeAuditPipelineBehavior calls it alongside the existing RevertToOriginalBodyIfNeeded(), immediately before building the OutgoingMessage, so the audit copy always reflects the headers as received.

Note that the snapshot is taken after GetOrSetMessageIdFromHeaders runs, so a message arriving without a NServiceBus.MessageId header still gets that header populated from the native ID in the audit copy, matching existing behavior.

Considerations

  • The allocation. IncomingMessage is constructed once per received message, so this adds a dictionary copy to the receive hot path unconditionally. An alternative is snapshotting at the top of InvokeAuditPipelineBehavior.Invoke instead of in the constructor. The audit fork is the only consumer, and it's only active when auditing is enabled.
  • RevertToOriginalHeadersIfNeeded() method name has no condition, its just a no-op so the name potentially overstates what it does.
  • "Fixing" this might lead to unexpected behavioural changes to systems. Audit headers have worked this way for some time. Additionally the audit feature itself adds headers. It may be useful to be able to see headers as they were received and as they were processed, and that will be easier to accomplish once there is some kind of snapshotting dictionary in place, but it will require changes to the audit pipeline in NServiceBus as well as in ServiceControl and ServicePulse.

Tests

  • unit coverage for the three mutation shapes: value changed, key added, key removed.
  • acceptance test registering an IMutateIncomingTransportMessages that overwrites a header, and asserting the audit spy endpoint observes the original value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Headers modified during message processing are not reverted for audited messages

2 participants