Skip to main content
Version: 5.x

Errors and recovery

MTE Relay v5 answers relay faults with HTTP statuses 470 to 490, and keeps 500, 502, and 503 for true server faults, upstream failures, and state store outages. Each error carries a reason token, and each code has a registered client action. The v4 custom codes (559 to 570) are not used; the protocol registry keeps 559 to 569 retired so they are never reassigned.

Relay faults moved out of the 5xx range, where a CDN or proxy may replace the response body with its own page. Cloudflare, Application Gateway, and Front Door do the same to plain 4xx bodies such as a 403 or 429, which is why a discovery refusal is 480, 488, or 490 and never one of those: an ERROR frame with a relay status survives them. That is also why v5 repeats every error in a response header. SocketX 2.x answers with the same codes.

How an error arrives​

Every relay generated response carries the error twice:

  • The body is an ERROR frame: a two byte code followed by JSON with reason, an optional message, and an optional cause.
  • The X-MTE-Relay-Error header repeats the code and reason, for example X-MTE-Relay-Error: 470 pair_not_found, in case a proxy replaces the body. A response to HEAD carries no body, so the header is its only copy.

The HTTP status equals the code. Error responses also carry Cache-Control: no-store, no-transform, private, and Retry-After when the client should wait. If a failure happens after the relay has started a framed response, the response ends with an ERROR frame instead of a truncated body.

A v5 client treats any answer that is not a valid frame, whatever its status or content type, as a transport error and never as application data. A CDN error page, a WAF block page, or an ingress 404 lands there.

Codes​

The HTTP relay column says whether the MRS and MAR relay sends the code in this release. The others are registered for the connection transports and gateways that share the protocol.

CodeReasonsMeaningClient actionHTTP relay
470pair_not_foundThe relay has no state for this pair, while the client still has other pairs there.Replace the pair.yes
471decoder_restore_failedThe relay could not restore the pair's decoder state.Replace the pair.yes
472encoder_restore_failedThe relay could not restore the pair's encoder state.Replace the pair.yes
473decode_failedDecoding failed.Replace the pair.yes
474encode_failedEncoding failed.Replace the pair.yes
475invalid_client_idThe token failed its signature check, which happens when the domain secret changed or a replica has a different secret, or the relay holds no pairs for this client, for example after a restart without Redis.Full repair: authenticate again and pair again, at most once per 30 seconds.yes
475invalid_tokenAt pair or keepalive, the token is older than TOKEN_MAX_AGE_SECONDS.Authenticate again. The relay answers an aged token with a new client id, so the client drops its pairs and pairs again.yes
475unknown_keyA preshared key was never issued or was revoked.Stop and tell the operator.no
476pool_exhaustedNo capacity.Back off.no
477invalid_frame, eof_before_endThe frame broke a rule, was truncated, or the request deadline passed between frames. eof_before_end means the body ended without END.invalid_frame: surface, the pair stays usable. eof_before_end: replace the pair.yes
478frame_version_unsupportedThe client offered no frame version the relay accepts, sent a frame version 1 body, or sent HEAD /api/mte-relay, the v4 SDK's probe.Surface. The message names the accepted versions and the minimum SDK versions; the texts are on Discovery.yes
479replay_rejectedThe decoder rejected a replayed or out of order request.Surface, the pair stays usable, and log a security event.yes
480no_domain_for_hostNo DOMAIN_MAP entry matches the Host, at discovery or on any later request.Surface. The message names the host the relay saw.yes
481frame_too_large, body_too_large, message_too_largeA frame over MAX_FRAME_BYTES, a body over MAX_REQUEST_BODY_MIB or MAX_CONTROL_BODY_MIB, or a reassembled WebSocket message over its bound.Surface. On HTTP the pair stays usable.yes
482metadata_invalidThe request metadata JSON is malformed or over its bound.Surface, the pair stays usable.yes
483header_mismatchThe encrypted method, version, or flags disagree with the plaintext envelope.Surface, the pair stays usable, and log a security event.yes
484target_forbidden, port_out_of_range, authz_revokedConnection transports only.Reset the local socket.no
485upstream_capacityAn outbound domain ran out of pair capacity toward the next relay: for CLIENT_PAIR_ACQUIRE_WAIT_MS, more requests were in flight than CLIENT_MAX_PAIRS or than the next relay's MAX_PAIRS_PER_CLIENT, or the pool was still growing behind the next relay's pairing rate limit. See Limits and scaling.Back off, honoring Retry-After.yes
486pair_busyReserved in the registry. The HTTP relay does not send it in this release.Retry once, then replace the pair.no
487rekey_overdueConnection transports only.Reconnect with a new OPEN.no
488connection_limit, rate_limitedToo many requests. The HTTP relay sends rate_limited on discovery, pair, and keepalive, with the message too many requests; back off with jitter. Discovery allows 600 requests per minute per source with a burst of 600.Back off with jitter, honor Retry-After, and keep the pair.yes, rate_limited
489target_resetConnection transports only.Reset the local socket.no
490invalid_request, pair_exists, pair_limit, profile_mismatchValidation failed on a JSON endpoint or an outbound request, or the Host header does not parse (invalid_request, at discovery or on any later request).pair_limit: drop unused pairs through keepalive and retry once, then stop growing. profile_mismatch: fatal setup error. The rest are client bugs.yes
500internal, configurationA server fault, or a configuration problem such as a domain with no secret.Surface.yes
502upstream_connect_failed, upstream_response_failedupstream_connect_failed: the upstream could not be reached, or sent no response headers within HTTP_CLIENT_TIMEOUT_SECONDS. upstream_response_failed: the upstream ran the request and the response then failed.Surface. upstream_response_failed means the origin ran the request. upstream_connect_failed usually means it did not, but after a header timeout the origin may still have run it.yes
503state_store_unavailable, state_store_write_failedstate_store_unavailable: at pair or keepalive the state store could not be read or written, and nothing executed. A state read that fails on the data path is answered as a missing pair, 470. state_store_write_failed: the relay could not save the pair's state; for a streamed body the origin has already run the request.state_store_unavailable: retry after Retry-After, the pair stays usable. state_store_write_failed: replace the pair and do not retry.yes

A receiver accepts an unknown reason under a known code and applies that code's action, so a newer relay does not break an older client.

Other statuses you can see​

Some answers come from plain HTTP handling rather than the relay error model:

  • 404 for a request to an inbound domain that is neither a frame (POST with Content-Type: application/octet-stream) nor a pass through route.
  • 405 for a wrong method on a relay endpoint, for example anything but GET or HEAD on /api/mte-relay. A HEAD there is how a v4 client starts, and the relay answers it 478 frame_version_unsupported without issuing a token. An OPTIONS request to a host in DOMAIN_MAP gets 204 instead, unless cors_preflight_passthrough is on. The method is checked before the Host, so a POST or an OPTIONS to a host that DOMAIN_MAP does not match gets 405, not 480.
  • 502 Upstream request failed as plain text when a pass through route cannot reach its upstream.

A successful framed response always travels as HTTP 200. The origin's real status travels in the RESPONSE frame, as a plain status field that the encrypted metadata repeats, so a load balancer log shows 200 for relayed requests and 470 to 503 for relay errors.

Outbound domain errors​

On an outbound domain the caller speaks plain HTTP, so errors arrive as JSON instead of an ERROR frame, with the same X-MTE-Relay-Error header. The exception is a host that matches no DOMAIN_MAP key or does not parse: the relay answers it before it knows the domain is outbound, so that 480 or 490 arrives as a binary ERROR frame. Read the header in that case.

{
"code": 502,
"reason": "upstream_connect_failed",
"message": "the next relay answered an error",
"cause": { "code": 470, "reason": "pair_not_found", "hop": 1 }
}

cause names the error the next relay returned and how many hops away it happened, with the same lowercase members as the top level. The outbound relay turns a next relay error into 502: upstream_response_failed when the next relay reports that its origin ran the request or answers 500, and upstream_connect_failed otherwise. Pair capacity trouble is 485 upstream_capacity with Retry-After. Before answering, the relay retries a request on a fresh pair, up to four times, when the next relay rejected it with a pair, token, or state store error before its origin ran. Other errors, such as 481, 482, 488, and 490, are answered at once.

Operations checklist​

  • Alert on 470 to 490 and on the X-MTE-Relay-Error header, not on 559 to 570.
  • Keep 500, 502, and 503 as your true fault signals.
  • Treat a sustained rise in 475 invalid_client_id across replicas as a sign that replicas do not share client_id_secret, or that pair state is not shared through Redis.
  • Log X-MTE-Relay-Request-Id. The relay sets it on every response and sends it upstream under the same name, and the browser engine exposes it as requestId on its errors, so one id ties a client error to relay and origin logs.