The envelope
What travels with a message besides its body: identity, causation, and the counters the retry engine keeps. It is the one thing every AceMQ library must agree on exactly, in every language, or two services stop understanding each other.
The headers
| Header | Type | Meaning |
|---|---|---|
x-acemq-id |
string | Unique message identifier, and the default idempotency key |
x-acemq-type |
string | Logical message type, for example order.placed |
x-acemq-version |
integer | Schema version of the payload |
x-acemq-correlation |
string | Business correlation, propagated unchanged across hops |
x-acemq-causation |
string | The message that caused this one |
x-acemq-attempt |
integer | Delivery attempt, starting at 1 |
x-acemq-first-seen |
integer | Epoch milliseconds of the first publish |
x-acemq-origin |
string | The publishing process, conventionally
service@host |
x-acemq-error |
string | Why a message was dead-lettered |
x-acemq-claim |
string | Where the payload is, when the message carries a reference to it |
acemq-replayed-from |
string | Queue a message was replayed out of |
acemq-replayed-at |
string | RFC 3339 instant, when last replayed |
acemq-replay-count |
integer | How many times it has been replayed |
traceparent / tracestate |
string | W3C trace context |
Note the two timestamps are encoded differently —
first-seen is an integer, replayed-at is a
string. That is not an inconsistency to tidy up in a port. It is the
contract, and a port that "fixes" it produces messages Java cannot
read.
acemq- is
defined but not reserved
The three replay stamps do not carry the
x-acemq- prefix, and neither do acemq-reply-to
or acemq-routing-slip. They are written by a pattern rather
than by the engine, and they have to reach the handler: a header in the
reserved namespace is stripped on consume, which for these would mean
writing them and finding them gone. AceHeaders.SharedPrefix
names this namespace, and AceHeaders.IsAceHeader
deliberately does not match it.
These three changed spelling after 0.5.0. Up to and
including that release this library wrote
x-acemq-replayed-from, x-acemq-replayed-at and
x-acemq-replay-count — the reserved namespace, which every
AceMQ library including this one strips before a handler sees it. A
message a .NET operator replayed therefore reached a Java, Go, Python or
Ruby handler with its provenance silently removed, and reached a .NET
handler the same way. Java made the same move in 0.5.0 and was the
second-to-last to make it. Anything matching on the old names —
a shovel policy, a dashboard, a firehose consumer — needs the new
ones. The old spelling is still read, so a message a
0.5.0 service replayed keeps its replay count when a newer service
replays it again; it is never written.
x-acemq- is reserved
A header carrying that prefix belongs to the engine. It is materialised onto the envelope if this version knows it, and dropped from the application's headers either way.
Use your own namespace — x-yourcompany- — for anything
that must survive the round trip. In this library, writing into the
reserved namespace throws at the call site rather than vanishing in
transit:
Envelope.Of("t").Header("x-acemq-id", "mine"); // ArgumentExceptionJava drops it silently on consume. Failing early is kinder than a header that disappears with nothing reporting the loss.
The defaults are contract, not convenience
Generated fixtures pinned these. Every one would have been a guess if transcribed from prose:
| When you do not set | It becomes |
|---|---|
type |
the routing key |
correlation |
the message id |
origin |
acemq@{hostname} |
version, attempt |
1 |
causation |
absent — the header is omitted, not written as null |
claim |
absent — likewise |
The AMQP messageId property also mirrors
x-acemq-id.
The claim
x-acemq-claim is an optional field for a message whose
payload is somewhere else and is named here — a URI, a bucket key,
whatever the two ends have agreed:
var outgoing = Envelope.Of("order.placed")
.Claim("s3://payloads/2026/09/A-1")
.Build();It reads back as message.Envelope.Claim, survives every
retry and every hop, and can be replaced on a copy with
envelope.WithClaim(...) the way Ruby's
envelope.with(claim:) does. The engine never reads
it and never writes it — it is the application's field, carried
in the engine's namespace so that it cannot be mistaken for an
application header and so that all five libraries agree on the
spelling.
It is not the claim-check pattern.
ClaimCheckCodec is a different answer to a related
question: it frames the body itself — a marker byte, then the key — so a
message either is a claim check or is not, and no header is
involved. See serialization.
This is a field a message may carry alongside a body it already has, and
the two are usable together or separately.
This field is new here. Python and Ruby have carried
claim as part of the envelope since their first release.
This library reserved the name and materialised nothing, so a claim a
Python or Ruby publisher set was dropped on the way in, like every other
reserved header a version does not understand — silently, with nothing
reporting the loss.
How this is kept honest
tests/AceMq.Amqp.Tests/fixtures/envelope-fixtures.json
is generated, never written. A program publishes
through acemq-java-amqp and pulls the message back at the
transport level — the only place the engine's own headers are still
visible, since the consumer API strips them by design.
The C# tests then assert against those bytes, in
EnvelopeConformanceTests.
The generator lives in the Java repository's own test suite, so CI regenerates the fixtures on every change and fails when they differ from what is committed. That turns "the port drifted" from something a customer finds into a red build on the commit that caused it.
A second fixture,
tests/AceMq.Amqp.Tests/fixtures/contract-fixtures.json,
pins the behaviour either side of the wire rather than the wire itself:
the retry schedule, the queue names, the rung arguments and the declared
topology. It is generated the same way, carried byte-identically by all
five libraries, and asserted in ContractConformanceTests.
../scripts/check-fixtures.sh compares every copy and fails
on a single changed character. See Testing for what it
pins and where this library still disagrees with it.