Destination mapping
How the KubeMQ STOMP connector maps a destination to a pattern and channel — the prefix grammar, slash-to-dot joining, aliases, and Events-only wildcards.
The STOMP destination is the single most important mental model in this connector. A
destination's first segment selects the KubeMQ pattern; the remaining segments are
slash→dot joined into the KubeMQ channel. This is how an unmodified STOMP application reaches all
five KubeMQ patterns by changing only the address it dials.
The grammar
The connector resolves a destination in these steps:
- The raw destination length is checked first — ≤ 512 bytes, on the raw string before the leading-slash strip.
- Strip exactly one leading
/(both/queue/xandqueue/xare accepted). - Split on
/. Any empty segment (from//, a trailing/, etc.) is rejected asinvalid destination. - The first segment selects the pattern via a case-sensitive map.
- The remaining segments are
.-joined into the KubeMQ channel. - An unknown first segment falls back to
DefaultPattern(see below).
Primary prefix → pattern → channel
Lead with these primary names in every application and example:
| STOMP destination (ingress) | Pattern | KubeMQ channel | Canonical egress (always primary) |
|---|---|---|---|
/queue/orders/new | Queues | orders.new | /queue/orders/new |
/topic/a/b/c | Events | a.b.c | /topic/a/b/c |
/topic-store/audit | Events-Store | audit | /topic-store/audit |
/command/exec | Commands (RPC) | exec | /command/exec |
/query/lookup | Queries (RPC) | lookup | /query/lookup |
/reply/r1 | reply (connection-local) | r1 | /reply/r1 |
The Events-Store prefix is /topic-store/ (primary). There is no /topic_store/ or
/eventstore/.
Slash → dot, and the literal-dot trap
The channel join is slash→dot: /topic/a/b/c → channel a.b.c. Egress reverses it: channel
a.b.c → /topic/a/b/c. A literal . inside a segment passes through unchanged — which makes
it lossy.
A literal . in a destination segment is lossy. Both /topic/a.b and /topic/a/b map to the
same KubeMQ channel a.b, and egress always emits the slash form /topic/a/b. So
/topic/a.b is not round-trip safe — a subscriber will see /topic/a/b on the MESSAGE
destination. Prefer slashes; avoid literal dots in destination segments.
SEND /topic/a.b ─┐
├─► channel "a.b" ─► MESSAGE destination /topic/a/b
SEND /topic/a/b ─┘MQTT-name aliases (never lead with them)
For migration convenience, each primary prefix has an MQTT-style alias. They route identically, but
egress always canonicalizes to the primary name — a subscriber never sees the alias on the
delivered MESSAGE destination.
| Primary (lead with this) | Alias | Pattern |
|---|---|---|
/queue/ | /queues/ | Queues |
/topic/ | /events/ | Events |
/topic-store/ | /store/ | Events-Store |
/command/ | /commands/ | Commands (RPC) |
/query/ | /queries/ | Queries (RPC) |
/reply/ | (none) | reply (connection-local) |
The aliases exist only so an in-flight migration from MQTT-style naming keeps working; because
egress canonicalizes, a fleet that subscribes via /events/x and a fleet that subscribes via
/topic/x both receive /topic/x on the wire. Always lead docs, examples, and application code
with the primary names.
DefaultPattern (bare destinations)
A destination whose first segment is not a known prefix is treated as bare and routed by the
connector's DefaultPattern config:
DefaultPattern | Bare sensor/temp resolves to | Channel |
|---|---|---|
events (config default) | Events | sensor.temp |
queues | Queues | sensor.temp |
store | Events-Store | sensor.temp |
none | rejected (invalid destination) | — |
There is no commands / queries default — a bare destination can never resolve to an RPC
pattern.
Examples always use explicit prefixes. Relying on DefaultPattern couples your application to a
server-side config you do not control. Write /topic/sensor/temp, not sensor/temp.
Wildcards — Events only, subscribe only
Wildcards are pass-through to the message broker's native wildcard syntax — the connector does not translate them:
*matches one segment, in any position.>matches the tail, and must be the final segment.
Wildcards are Events-only and SUBSCRIBE-only, and use the broker's native syntax (* = one
segment, > = final tail). A wildcard is allowed only on SUBSCRIBE and only for the
Events pattern. A wildcard on SEND, or on Queues / Events-Store / RPC, is rejected as invalid destination. There is no MQTT-style + / #. And egress delivers the concrete matched
channel, not the filter — a /topic/orders/* subscriber receiving on orders.eu gets
destination:/topic/orders/eu.
SUBSCRIBE /topic/orders/* → channel filter "orders.*" (Events, OK)
SUBSCRIBE /topic/orders/> → channel filter "orders.>" (> must be final, OK)
SEND /topic/orders/* → invalid destination
SUBSCRIBE /queue/jobs/* → invalid destination
# Egress delivers the concrete channel:
SUBSCRIBE /topic/orders/* ──► MESSAGE destination /topic/orders/eu (NOT /topic/orders/*)Slash→dot still applies inside wildcard filters: /topic/a/*/c → a.*.c. Wildcards do not apply
to Events-Store.
Header ⇄ tag interop
The body is byte-exact in both directions (the writer always stamps content-length on egress, so
it is binary-safe). Metadata is mapped between STOMP headers and KubeMQ Tags by a precise
convention.
The five standard headers ↔ reserved stomp.* tags
These five round-trip in both directions:
| STOMP header (ingress & egress) | KubeMQ Tag key |
|---|---|
content-type | stomp.content-type |
correlation-id | stomp.correlation-id |
reply-to | stomp.reply-to |
priority | stomp.priority |
type | stomp.type |
A SEND content-type:application/json becomes Tag stomp.content-type=application/json; on egress
that tag is restored to a content-type header.
Custom headers pass through as tags
Any header not in the standard set and not protocol machinery passes through name-as-is
as a KubeMQ tag. A custom x-trace:abc on SEND becomes Tag x-trace=abc, delivered back as an
x-trace header. Duplicate header names are first-wins.
Tag limits are fatal on SEND. At most 32 custom tags, at most 4096 bytes per tag value.
Exceeding either returns ERROR "frame too large" and closes the connection.
Collision rule, machinery headers, and the spoofing guard
- Collision rule —
stomp.*wins. If a native producer sets both a barecontent-typetag and astomp.content-typetag, thestomp.*tag wins on egress, and exactly onecontent-typeheader is emitted. - Machinery headers never become tags:
destination,receipt,transaction,content-length,message-id,subscription,ack,id,timeout. - Spoofing guard. A client cannot inject
stomp.*tags directly. On ingress, any header whose name starts withstomp.or equalsx-kubemq-metadatais silently stripped — the only way to set astomp.*tag is through the corresponding standard header. x-kubemq-metadatais egress-only. STOMP ingress never sets the KubeMQMetadatafield. On egress, if a native producer left a non-emptyMetadata, it surfaces as a read-onlyx-kubemq-metadataheader. A STOMP client can observe it but cannot write it.
There is no content-type frame default. A native producer (gRPC/REST/MQTT/AMQP) that sets no
content-type tag produces a MESSAGE with no content-type header — the STOMP subscriber must
assume binary / octet-stream. content-length is always present, so the body is still framed
correctly. To surface a proper content-type, a native producer should set
Tags["stomp.content-type"].
Egress representability per version
Not every header value can be serialized on every negotiated STOMP version. The connector drops unrepresentable headers on egress rather than corrupting the frame:
| Negotiated version | Drops on egress |
|---|---|
| 1.0 | :, CR, or LF in a name; CR or LF in a value |
| 1.1 | CR (1.1 has no escape for it) |
| 1.2 | nothing — always representable |
CR/LF in a header value is silently dropped to 1.0/1.1 subscribers. STOMP 1.1 cannot represent a
raw CR; the guard drops the unrepresentable header without corrupting the frame, and the connection
stays alive. Use accept-version:1.2 and keep CR/LF out of header values; structured or multiline
metadata belongs in the body.
Destination errors (all sanitized)
| Trigger | Wire message |
|---|---|
//, trailing /, /queue/ (empty segment) | invalid destination |
/queue (prefix, no channel) | invalid destination |
Bare destination with DefaultPattern=none | invalid destination |
| Raw destination > 512 bytes | invalid destination |
| Wildcard on SEND / Queues / Events-Store / RPC | invalid destination |
Internal error text never crosses the wire — every destination error surfaces as the single sanitized
invalid destination message.
Related
Was this page helpful?
Connectivity and security
Connecting a STOMP client to KubeMQ — the KUBEMQ_STOMP_URL convention, ports 61613/61614, the CONNECT handshake, heartbeats, and TLS on 61614.
Events
Fire-and-forget STOMP pub/sub over KubeMQ Events — the /topic/ destination prefix, fan-out delivery, native wildcards (* and >), and at-most-once semantics.