Discovery
A v5 client does not hardcode relay limits. Its first call to a relay origin is the auth request, and the answer carries a discovery document that lists what the matched domain accepts and enforces.
GET /api/mte-relay?frameVersions=2
The relay negotiates the frame version, 2 in this release, before any Kyber
work, then issues a token and the document. A client that already holds a
token adds clientId=<token> to refresh it; a valid token keeps the same
client id, and an invalid or aged one gets a new id. Every refusal is listed
under Check order and refusals.
The response is JSON:
{
"clientId": "<54 character token>",
"relay": {
"discoverySchema": 1,
"buildVersion": "5.0.5",
"frameVersions": [2],
"features": ["http", "keepalive", "plain-headers", "browser-cookies", "pass-through", "relay-error-header"],
"mteProfile": "mte/...;kyber=1024;probe=...",
"kyberStrength": 1024,
"sequenceWindow": -63,
"timeWindow": 1000,
"maxFrameBytes": 65536,
"maxMessageBytes": 65536,
"maxMetadataBytes": { "MKE": 61439, "MTE": 7168 },
"maxPairsPerBatch": 100,
"maxPairsPerClient": 200,
"maxPairBytes": 1073741824,
"maxPairSeconds": 86400,
"maxRequestSeconds": 30,
"maxResponseSeconds": 0,
"idleTimeoutSeconds": 90,
"pingIntervalSeconds": 25,
"drainSeconds": 30,
"sessionTimeoutSeconds": 7200,
"tokenMaxAgeSeconds": 86400,
"transports": ["http"],
"stateMode": "shared",
"routeHeader": "X-MTE-Relay-Route",
"maxRequestBodyBytes": 0,
"forwardBrowserCookies": true
}
}
The values above are the defaults. The auth endpoint needs no credentials, so everything in the document is public. It never contains upstreams, secrets, custom headers, CORS settings, Redis settings, or host names.
Fields
| Field | Source | What it tells a client |
|---|---|---|
discoverySchema | fixed | Version of the document layout. 1 in this release. |
buildVersion | image version file | The relay build that answered, for support and logs. Omitted when the image has no version file, never 0.0.0. Clients key behavior on features, never on this string. |
frameVersions | fixed | Frame versions the relay accepts: [2]. |
features | fixed | Registry feature names this build offers. The HTTP relay offers http, keepalive, plain-headers, browser-cookies, pass-through, and relay-error-header. |
mteProfile | MTE library | The MTE library build: version, token size, DRBG, cipher, hash, verifiers, Kyber strength, and a probe digest. A client compares it with its own and refuses to pair on a settings mismatch, and the pairing request repeats it so the relay can refuse with 490 profile_mismatch. |
configVersion | control plane | The applied control plane configuration version. Absent on a relay configured only from its environment. |
kyberStrength | KYBER_STRENGTH | Kyber strength both ends must use. |
sequenceWindow, timeWindow | SEQUENCE_WINDOW, TIME_WINDOW | Pairing inputs. Both ends build their decoders with these values, so a mismatch means no pair at all. |
maxFrameBytes, maxMessageBytes | MAX_FRAME_BYTES | Largest frame payload. Clients size their DATA frames from it. |
maxMetadataBytes | MAX_METADATA_BYTES_MKE, MAX_METADATA_BYTES_MTE | Bound on the request metadata JSON per encode type. |
maxPairsPerBatch, maxPairsPerClient | MAX_PAIRS_PER_BATCH, MAX_PAIRS_PER_CLIENT | Pairing limits. |
maxPairBytes, maxPairSeconds | MAX_PAIR_BYTES, MAX_PAIR_SECONDS | Rekey thresholds for connection transports. |
maxRequestSeconds, maxResponseSeconds, idleTimeoutSeconds, pingIntervalSeconds | the timers, or the domain's own request_seconds, response_seconds, idle_seconds, ping_seconds | Deadlines the relay applies. |
drainSeconds | DRAIN_SECONDS | Drain window on connections. |
sessionTimeoutSeconds | SESSION_TIMEOUT_MINUTES times 60 | Idle timeout for pairs. Clients schedule keepalive from it. |
tokenMaxAgeSeconds | TOKEN_MAX_AGE_SECONDS | Token lifetime. |
transports | fixed | ["http"] for the HTTP relay. |
stateMode | fixed | shared, always: the relay keeps pair state in its state store rather than on a connection. SocketX sends owner. |
routeHeader | fixed | X-MTE-Relay-Route, the routing header clients send. |
maxRequestBodyBytes | MAX_REQUEST_BODY_MIB | Inclusive plaintext body cap in bytes. 0 means unlimited. Clients refuse a larger body before sending it. |
passThroughRoutes | the domain's pass_through_routes | Paths a client sends as plain requests. Omitted when empty. |
forwardPlainHeaders | the domain's forward_plain_headers | Plain headers the relay forwards upstream, lowercased. Omitted when empty. |
forwardBrowserCookies | the domain's forward_browser_cookies | Whether browser cookies reach the upstream. |
Values that come from live settings, such as maxRequestBodyBytes and the
domain fields, are read on every auth, so a control plane change shows up on
the next auth without a restart.
Check order and refusals
The relay checks a discovery request in a fixed order, the same order SocketX uses on its discovery endpoint, so both products refuse the same request with the same code and the same message:
- A CORS preflight (
OPTIONS) on aHostthatDOMAIN_MAPmatches answers204, with CORS headers only for an allowedOrigin, unless the domain setscors_preflight_passthrough. - Any method other than
GETorHEADanswers405. The method is checked before theHost, so aPOSTor anOPTIONSto an unknownHostgets405, not480. - The
Hostis resolved. A malformedHostanswers490 invalid_request, and aHostthat noDOMAIN_MAPentry matches answers480 no_domain_for_host, both as relay errors: an ERROR frame body and theX-MTE-Relay-Errorheader. Cloudflare, Application Gateway, and Front Door replace plain 4xx bodies with their own pages; an ERROR frame with a relay status survives them. - The per source rate limit. Discovery is limited to 600 requests per minute
per source with a burst of 600. Over the budget the relay answers
488 rate_limitedwithRetry-Afterand the messagetoo many requests; back off with jitter.RATE_LIMITS=falseturns every token bucket limiter off. Behind a load balancer setTRUSTED_PROXIES, or every client shares one budget. See Limits and scaling. - Version negotiation. The relay intersects
frameVersionswith the frame versions it accepts,[2]in this release, before any Kyber work. An empty intersection answers478 frame_version_unsupportedwith the messageserver accepts [2], client offered [..]; minimum SDK versions: android 5.0.0, ios 5.0.0, browser 5.0.0, go relay 5.0.0, where[..]is the list the client sent. A request with noframeVersionsat all getsno frameVersions offered (a pre version 2 SDK); server accepts [2]; minimum SDK versions: android 5.0.0, ios 5.0.0, browser 5.0.0, go relay 5.0.0, and theHEADprobe a version 4 SDK sends gets the same, as the status and the header only. AframeVersionsvalue that is not a comma separated list of integers from 1 to 255 answers490 invalid_request. - The token and the document. A valid
clientIdtoken keeps its client id. An invalid or aged token gets a new client id and raises theauth_invalid_tokensecurity event at severitylow; a valid or absent token raises nothing. The mint is logged at debug.
An Origin is never refused. A disallowed Origin gets the document without
CORS headers, so a browser cannot read it; curl and native clients are
unaffected. The origin rules are
the same on the relay and on SocketX.
Configuration version header
When a worker runs under the control plane, every relay response carries
X-MTE-Relay-Config-Version, the applied configuration version. The protocol
registry says a client that sees a higher value than the configVersion it
learned at auth should read discovery again. Browser engine
5.0.0 and the outbound relay client do not act on the header
yet. The browser engine keeps the bounds it adopted at its first auth until the
client is created again, for example on the next page load. It raises one
discovery-drift warning when a later auth reports a different
sequenceWindow, timeWindow, maxFrameBytes, or maxMetadataBytes. A
changed maxRequestBodyBytes raises no warning; the engine keeps enforcing the
value from its first auth.
The header is absent on a relay configured only from its environment.
What the SDKs do with it
The mte-relay-browser engine (5.0.0) reads discovery on the
first request to each relay origin and:
- throws
MteRelaySetupErrorwhen the relay does not accept frame version 2, when themteProfilesettings differ, or whenkyberStrengthdiffers from its own; - uses the relay's
sequenceWindowandtimeWindow, and warns once when you configured a differentsequenceWindow; - clamps its pool to
maxPairsPerClient; - schedules keepalive at one third of
sessionTimeoutSeconds, between 1 and 10 minutes, unless you setkeepAliveIntervalMs; - throws
MteRelayRequestTooLargeErrorfor a body larger thanmaxRequestBodyBytesbefore sending it; - sends requests that match
passThroughRoutesas plainfetchcalls; - warns once for each
unencryptedHeadersname the relay will not forward.
See the JavaScript client for the options involved.