# Error Frames (/connectors/stomp/reference/error-frames)



This is the complete, verified vocabulary of `ERROR`-frame `message` strings the KubeMQ STOMP
connector emits — the trigger for each, the receipt rules, and the one failure mode that is **not**
an ERROR frame at all: the RPC `stomp-error` header.

## ERROR is terminal [#error-is-terminal]

<Callout type="warn">
  **Every protocol error produces an `ERROR` frame followed immediately by a socket close.** There
  is no recovery on the same connection — the client must reconnect. The `message` string is
  **sanitized**: internal error text, stack chains, and file paths never cross the wire.
</Callout>

The connector has three ERROR-emit paths, all of which close the connection:

| Function         | Phase                                    |
| ---------------- | ---------------------------------------- |
| `writeErrorSync` | handshake (before the write loop starts) |
| `sendError`      | post-CONNECT frame dispatch              |
| `sendErrorAsync` | connector shutdown broadcast             |

An ERROR frame carries `message:<sanitized text>` and, when a detail is provided, a
`content-type:text/plain` body with a short human-readable explanation.

## The ERROR-frame vocabulary (complete) [#the-error-frame-vocabulary-complete]

Every `message` string the connector can emit, grouped by phase. The **detail** column shows the
optional ERROR body where one is set.

### Handshake errors (before CONNECTED) [#handshake-errors-before-connected]

| `message`                  | Detail body                            | Trigger                                                                    |
| -------------------------- | -------------------------------------- | -------------------------------------------------------------------------- |
| `malformed frame`          | `first frame must be CONNECT or STOMP` | first frame is not `CONNECT` / `STOMP` within 30 s                         |
| `broker not ready`         | —                                      | the message broker not ready at CONNECT (gate before auth)                 |
| `connection limit reached` | —                                      | `MaxConnections` exceeded (counted at accept; ERROR deferred to handshake) |
| `authentication failed`    | —                                      | `passcode` JWT rejected (no detail — no leakage)                           |
| version-negotiation ERROR  | —                                      | no common `accept-version`; the ERROR carries `version:1.0,1.1,1.2`        |

### Frame-codec errors (read loop) [#frame-codec-errors-read-loop]

| `message`         | Detail body | Trigger                                                                |
| ----------------- | ----------- | ---------------------------------------------------------------------- |
| `frame too large` | —           | frame exceeds 64 headers / 8 KiB header block / `MaxBodySize`          |
| `malformed frame` | —           | unparseable frame; body on a non-`SEND` command; bad escaping; lone CR |

### Dispatch errors (post-CONNECT) [#dispatch-errors-post-connect]

| `message`                    | Detail body                             | Trigger                                                                        |
| ---------------------------- | --------------------------------------- | ------------------------------------------------------------------------------ |
| `transactions not supported` | —                                       | `BEGIN` / `COMMIT` / `ABORT`, or **any frame** carrying a `transaction` header |
| `malformed frame`            | `already connected`                     | a second `CONNECT` / `STOMP` after the handshake                               |
| `unknown command`            | — (offending command echoed, sanitized) | an unrecognized command                                                        |

### SEND errors [#send-errors]

| `message`             | Detail body                                  | Trigger                                                                                        |
| --------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `invalid destination` | `missing destination header`                 | SEND with no / empty `destination`                                                             |
| `invalid destination` | —                                            | bad destination grammar (empty segment, empty channel, no pattern, too long, wildcard on SEND) |
| `invalid destination` | `cannot SEND to reply destinations`          | SEND to a `/reply/` destination                                                                |
| `access denied`       | —                                            | Casbin `Enforce(write)` denied                                                                 |
| `broker not ready`    | —                                            | the message broker not ready at SEND time (SENDs do **not** buffer)                            |
| `frame too large`     | `too many headers or header value too large` | >32 custom tags or a tag value >4096 bytes                                                     |
| `message rejected`    | —                                            | the array publish call returned an error (sanitized; detail in server log)                     |

### SUBSCRIBE errors [#subscribe-errors]

| `message`                              | Detail body                  | Trigger                                                         |
| -------------------------------------- | ---------------------------- | --------------------------------------------------------------- |
| `selectors not supported`              | —                            | a `selector` header on SUBSCRIBE                                |
| `invalid destination`                  | `missing destination header` | SUBSCRIBE with no / empty `destination`                         |
| `invalid subscription`                 | `id header required`         | no `id` on 1.1 / 1.2                                            |
| `invalid subscription`                 | `unknown ack mode`           | `ack` is not `auto` / `client` / `client-individual`            |
| `invalid destination`                  | —                            | bad destination grammar                                         |
| `cannot subscribe to RPC destinations` | —                            | SUBSCRIBE to `/command/` or `/query/` (STOMP is requester-only) |
| `invalid subscription`                 | `duplicate subscription id`  | the `id` is already in use on this connection                   |
| `access denied`                        | —                            | Casbin `Enforce(read)` denied (queues / events / store)         |
| `invalid subscription`                 | `<replay error>`             | bad Events-Store `start-from` / `start-value` combination       |

### UNSUBSCRIBE / ACK / NACK errors [#unsubscribe--ack--nack-errors]

| `message`              | Detail body               | Trigger                                             |
| ---------------------- | ------------------------- | --------------------------------------------------- |
| `invalid subscription` | `missing id`              | UNSUBSCRIBE with no resolvable `id`                 |
| `invalid subscription` | `unknown subscription id` | UNSUBSCRIBE for an id this connection never created |

<Callout type="info">
  ACK / NACK **never** close on an unknown, expired, or foreign token — the late ack is silently
  ignored and the RECEIPT (if requested) is still sent.
</Callout>

### RPC SEND errors (the only RPC failures that close the connection) [#rpc-send-errors-the-only-rpc-failures-that-close-the-connection]

| `message`                        | Detail body | Trigger                                                                         |
| -------------------------------- | ----------- | ------------------------------------------------------------------------------- |
| `reply-to subscription required` | —           | the `reply-to` does not name an active `/reply/` sub on the **same** connection |
| `too many pending requests`      | —           | `RpcMaxPending` (default 1024) exceeded                                         |

Everything **else** about an RPC failure (timeout, logical error, dropped reply) is **not** an
ERROR frame — see [RPC `stomp-error`](#rpc-stomp-error-a-failure-that-is-not-an-error-frame).

### Shutdown [#shutdown]

| `message`              | Detail body | Trigger                                                                    |
| ---------------------- | ----------- | -------------------------------------------------------------------------- |
| `server shutting down` | —           | connector `Close()` broadcasts to every live connection, then drains ≤10 s |

## Receipt semantics [#receipt-semantics]

The `receipt` header requests a `RECEIPT` confirmation frame. The rules:

* A `receipt` header is honored on **every processed client frame** (SEND, SUBSCRIBE,
  UNSUBSCRIBE, ACK, NACK, DISCONNECT).
* `RECEIPT` is enqueued **after** the frame's processing completes:
  * **SEND** (events / store / queues): after the array call returns **successfully**.
  * **RPC SEND** (`/command` / `/query`): after **dispatch acceptance** — before the reply MESSAGE
    arrives.
* **A RECEIPT means "KubeMQ accepted the frame", NOT "a consumer received the message."** Do not
  treat a SEND RECEIPT as a delivery confirmation.

<Callout type="warn">
  **Handlers that close the connection on an ERROR path NEVER send a RECEIPT.** The STOMP spec
  permits an `ERROR` in lieu of a `RECEIPT`. So a SEND to a bad destination that carried a `receipt`
  header returns an `ERROR` and **no** RECEIPT — never block forever waiting for one.
</Callout>

A `DISCONNECT` with a `receipt` header flushes the `RECEIPT` to the wire **before** the socket
closes (deterministic graceful shutdown). This is the clean way to confirm a graceful close.

## RPC `stomp-error`: a failure that is NOT an ERROR frame [#rpc-stomp-error-a-failure-that-is-not-an-error-frame]

<Callout type="warn">
  **RPC failures arrive as a MESSAGE with a `stomp-error` header, NOT an ERROR frame.** A timeout, a
  logical error, or a dropped reply is delivered as **data** on your `/reply/` subscription, and the
  connection **stays open**. Only `reply-to` violations and pending-cap overflow (above) close the
  connection.
</Callout>

The reply MESSAGE has **three shapes**, only two of which are empty-bodied:

| Shape                             | Condition                            | `stomp-error` value                                 | Body                                                |
| --------------------------------- | ------------------------------------ | --------------------------------------------------- | --------------------------------------------------- |
| **(a) Logical error**             | `resp.Error != "" && !resp.Executed` | `<sanitized resp.Error>`                            | **carries the responder's body + tags** — NOT empty |
| **(b) Transport error / timeout** | `err != nil`                         | `timeout` / `request cancelled` / `<sanitized err>` | empty                                               |
| **(c) Nil response**              | `resp == nil`                        | `no response`                                       | empty                                               |

<Callout type="warn">
  **Detect RPC failure by the presence of the `stomp-error` header, NOT by an empty body.** A
  logical error (shape a) returns a fully-populated body and tags alongside `stomp-error`. Code that
  keys on "empty body" will misclassify logical errors.
</Callout>

Go context errors map to stable strings:

| Go error contains           | `stomp-error` value      |
| --------------------------- | ------------------------ |
| `context deadline exceeded` | `timeout`                |
| `context canceled`          | `request cancelled`      |
| (anything else)             | the sanitized error text |

A reply MESSAGE is best-effort — if the connection's output queue is full it is dropped (metric
`rpc_response` / `dropped`), again **without** closing the connection. See
[Commands](/connectors/stomp/how-to/commands) and
[Queries](/connectors/stomp/how-to/queries).

## Error-detail sanitization [#error-detail-sanitization]

Every destination-class trigger is mapped to one of three sanitized strings before it reaches the
wire:

| Trigger class                                                                | Wire `message`        |
| ---------------------------------------------------------------------------- | --------------------- |
| frame-too-large violations                                                   | `frame too large`     |
| empty segment / empty channel / no pattern / too long / wildcard not allowed | `invalid destination` |
| anything else                                                                | `malformed frame`     |

Header values and command names that are echoed in an ERROR (for example, the offending command in
`unknown command`) are sanitized first.

## Related [#related]

<Cards>
  <Card title="Capabilities" href="/connectors/stomp/reference/capabilities" description="Supported commands, ack modes, limits, and the ten-gotchas summary." />

  <Card title="Destination Grammar" href="/connectors/stomp/reference/destination-grammar" description="The destination grammar and the error mapping in context." />

  <Card title="Commands" href="/connectors/stomp/how-to/commands" description="The RPC flow and the stomp-error failure mode in practice." />

  <Card title="ACK Modes & Receipts" href="/connectors/stomp/how-to/ack-modes-and-receipts" description="Receipts and acknowledgements in depth." />
</Cards>
