acemq_amqp.headers

The names AceMQ puts on the wire.

These are the contract between languages, not an implementation detail of this one. A Python consumer reads what a Java producer wrote because both agree on the strings in this module and on the types of their values, so nothing here is renamed for Python's benefit — x-acemq-first-seen stays hyphenated and stays epoch milliseconds, however un-Pythonic that looks.

The set is pinned by tests/fixtures/envelope-fixtures.json, which is generated by the Java implementation and shared with the Go and .NET ones.

  1# Copyright 2026 AceMQ.
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     https://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""The names AceMQ puts on the wire.
 16
 17These are the contract between languages, not an implementation detail of this
 18one. A Python consumer reads what a Java producer wrote because both agree on
 19the strings in this module and on the types of their values, so nothing here is
 20renamed for Python's benefit — ``x-acemq-first-seen`` stays hyphenated and
 21stays epoch milliseconds, however un-Pythonic that looks.
 22
 23The set is pinned by ``tests/fixtures/envelope-fixtures.json``, which is
 24generated by the Java implementation and shared with the Go and .NET ones.
 25"""
 26
 27from __future__ import annotations
 28
 29#: The unique message identifier, and the default idempotency key.
 30ID = "x-acemq-id"
 31
 32#: The logical message type, for example ``order.placed.v2``.
 33TYPE = "x-acemq-type"
 34
 35#: The schema version of the payload, as an integer starting at 1.
 36VERSION = "x-acemq-version"
 37
 38#: The business correlation identifier, propagated across hops.
 39CORRELATION = "x-acemq-correlation"
 40
 41#: The message that caused this one, absent when there was none.
 42CAUSATION = "x-acemq-causation"
 43
 44#: The delivery attempt, as an integer starting at 1.
 45ATTEMPT = "x-acemq-attempt"
 46
 47#: When the message was first published, as epoch milliseconds.
 48FIRST_SEEN = "x-acemq-first-seen"
 49
 50#: The publishing process, conventionally ``service@host``.
 51ORIGIN = "x-acemq-origin"
 52
 53#: Why a message was dead-lettered, present only when it was.
 54ERROR = "x-acemq-error"
 55
 56#: The claim-check URI, when the payload is stored outside the message.
 57CLAIM = "x-acemq-claim"
 58
 59#: The steps of a declared route, by name, comma-separated and in order.
 60#:
 61#: The form Java's ``Pipeline`` writes: a route declared once and resolved
 62#: against that declaration at each hop, so the wire carries
 63#: ``validate,enrich,dispatch`` and nothing about where those steps live. The
 64#: self-describing form, which carries an exchange and a routing key per step,
 65#: is the ``acemq-routing-slip`` application header that
 66#: :mod:`acemq_amqp.patterns.routingslip` writes by default. Python reads both
 67#: and can write either.
 68ROUTE = "x-acemq-route"
 69
 70#: Which step of :data:`ROUTE` this message is for, counting from zero.
 71ROUTE_POSITION = "x-acemq-route-position"
 72
 73#: One run through a route, carried by every hop, and surviving a dead-letter
 74#: and a replay so that the whole run can be found from any message in it.
 75ROUTE_ID = "x-acemq-route-id"
 76
 77#: The prefix every reserved name shares.
 78PREFIX = "x-acemq-"
 79
 80#: W3C trace context. **Deliberately not prefixed**, unlike everything above.
 81#:
 82#: These two are not AceMQ's names to choose. They are the ones the W3C
 83#: specification defines and every piece of tracing tooling already reads, so a
 84#: message published here joins up in a consumer written with no knowledge of
 85#: this library, and a message published by such a consumer joins up here.
 86#: Prefixing them would have been consistent with the rest of this module and
 87#: would have made the trace context private to AceMQ, which is the exact
 88#: opposite of what a propagation format is for. Java, Go and .NET write these
 89#: same two names for the same reason.
 90#:
 91#: They are application headers, not reserved ones: they belong to whatever
 92#: wrote them, they are copied rather than re-derived, and
 93#: :mod:`acemq_amqp.tracing` is what puts them on a message.
 94TRACEPARENT = "traceparent"
 95
 96#: The vendor-specific half of the W3C trace context. See :data:`TRACEPARENT`.
 97TRACESTATE = "tracestate"
 98
 99#: Every name this library writes and understands.
100RESERVED = frozenset(
101    {
102        ID,
103        TYPE,
104        VERSION,
105        CORRELATION,
106        CAUSATION,
107        ATTEMPT,
108        FIRST_SEEN,
109        ORIGIN,
110        ERROR,
111        CLAIM,
112        ROUTE,
113        ROUTE_POSITION,
114        ROUTE_ID,
115    }
116)
117
118
119def is_reserved(name: str) -> bool:
120    """Whether a header name belongs to AceMQ rather than to the application.
121
122    Matches on the prefix rather than on the known set: a header from a newer
123    version of another language's library is still not the application's, and
124    handing it back as though it were would have an application unknowingly
125    copy it onto a message it publishes.
126
127    :param name: a header name
128    :returns: whether AceMQ reserves it
129    """
130    return name.startswith(PREFIX)
ID = 'x-acemq-id'
TYPE = 'x-acemq-type'
VERSION = 'x-acemq-version'
CORRELATION = 'x-acemq-correlation'
CAUSATION = 'x-acemq-causation'
ATTEMPT = 'x-acemq-attempt'
FIRST_SEEN = 'x-acemq-first-seen'
ORIGIN = 'x-acemq-origin'
ERROR = 'x-acemq-error'
CLAIM = 'x-acemq-claim'
ROUTE = 'x-acemq-route'
ROUTE_POSITION = 'x-acemq-route-position'
ROUTE_ID = 'x-acemq-route-id'
PREFIX = 'x-acemq-'
TRACEPARENT = 'traceparent'
TRACESTATE = 'tracestate'
RESERVED = frozenset({'x-acemq-claim', 'x-acemq-origin', 'x-acemq-type', 'x-acemq-attempt', 'x-acemq-error', 'x-acemq-correlation', 'x-acemq-causation', 'x-acemq-id', 'x-acemq-route', 'x-acemq-route-position', 'x-acemq-route-id', 'x-acemq-first-seen', 'x-acemq-version'})
def is_reserved(name: str) -> bool:
120def is_reserved(name: str) -> bool:
121    """Whether a header name belongs to AceMQ rather than to the application.
122
123    Matches on the prefix rather than on the known set: a header from a newer
124    version of another language's library is still not the application's, and
125    handing it back as though it were would have an application unknowingly
126    copy it onto a message it publishes.
127
128    :param name: a header name
129    :returns: whether AceMQ reserves it
130    """
131    return name.startswith(PREFIX)

Whether a header name belongs to AceMQ rather than to the application.

Matches on the prefix rather than on the known set: a header from a newer version of another language's library is still not the application's, and handing it back as though it were would have an application unknowingly copy it onto a message it publishes.

Parameters
  • name: a header name :returns: whether AceMQ reserves it