KubeMQ
ConnectorsSTOMPHow-to guides

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:

  1. The raw destination length is checked first — ≤ 512 bytes, on the raw string before the leading-slash strip.
  2. Strip exactly one leading / (both /queue/x and queue/x are accepted).
  3. Split on /. Any empty segment (from //, a trailing /, etc.) is rejected as invalid destination.
  4. The first segment selects the pattern via a case-sensitive map.
  5. The remaining segments are .-joined into the KubeMQ channel.
  6. 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)PatternKubeMQ channelCanonical egress (always primary)
/queue/orders/newQueuesorders.new/queue/orders/new
/topic/a/b/cEventsa.b.c/topic/a/b/c
/topic-store/auditEvents-Storeaudit/topic-store/audit
/command/execCommands (RPC)exec/command/exec
/query/lookupQueries (RPC)lookup/query/lookup
/reply/r1reply (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)AliasPattern
/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:

DefaultPatternBare sensor/temp resolves toChannel
events (config default)Eventssensor.temp
queuesQueuessensor.temp
storeEvents-Storesensor.temp
nonerejected (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/*/ca.*.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-typestomp.content-type
correlation-idstomp.correlation-id
reply-tostomp.reply-to
prioritystomp.priority
typestomp.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 bare content-type tag and a stomp.content-type tag, the stomp.* tag wins on egress, and exactly one content-type header 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 with stomp. or equals x-kubemq-metadata is silently stripped — the only way to set a stomp.* tag is through the corresponding standard header.
  • x-kubemq-metadata is egress-only. STOMP ingress never sets the KubeMQ Metadata field. On egress, if a native producer left a non-empty Metadata, it surfaces as a read-only x-kubemq-metadata header. 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 versionDrops on egress
1.0:, CR, or LF in a name; CR or LF in a value
1.1CR (1.1 has no escape for it)
1.2nothing — 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)

TriggerWire message
//, trailing /, /queue/ (empty segment)invalid destination
/queue (prefix, no channel)invalid destination
Bare destination with DefaultPattern=noneinvalid destination
Raw destination > 512 bytesinvalid destination
Wildcard on SEND / Queues / Events-Store / RPCinvalid destination

Internal error text never crosses the wire — every destination error surfaces as the single sanitized invalid destination message.

Was this page helpful?

On this page