weida

GitHub crates.io

Core NATS (client protocol, `nats-server` 2.14) — feature parity

Rendered from docs/libraries/nats.md at 5a20f15

Contents
  1. 1. What a row means
  2. 2. The twelve verbs
  3. 3. Subjects and the two wildcards
  4. 4. Queue groups
  5. 5. Request-reply
  6. 6. Authentication and transports
  7. 7. Bounds
  8. 8. JetStream, clusters, gateways and leaf nodes
  9. 9. Interop: not run
  10. 10. The definition of done
  11. 11. Sources

Status: complete against the six clauses of 0013 §4.7, read for Core NATS. This document is the sixth clause itself — the row-by-row table — and §10 gives the other five their verdicts.

The library is two crates: weida-nats-codec, the sans-I/O text-protocol codec whose [dependencies] is empty, and weida-nats, the client on top of it. Neither depends on weida; a caller who wants a NATS client and nothing else uses weida-nats alone (0013 §4.3, §4.4).

This is a client, and almost everything else NATS does is a server feature. Routes, gateways, leaf nodes, clusters and JetStream are things nats-server does with other servers and with storage (0014 §2), and a client reaches none of them through the client protocol. Every row below is therefore read as the client half. A queue group is the one concept that sounds like a server feature and is not: it is a set of subscriptions sharing one eligible delivery per publication, declared in SUB, so it belongs here.

1. What a row means

Three verdicts and no fourth:

  • present — implemented, with the module or test that carries it named. A present row may still say where the behaviour differs, and then the difference is the row.
  • refused with a reason — the configuration exists in the protocol and fails here, at configuration time, with a message naming why (0013 §4.4 item 4). Nothing is silently ignored.
  • absent with a reason — not implemented and not configurable, with what is missing named.

"Partial" is not a verdict. Where something is present in part, the row says which part is missing (0013 §4.7 clause 6).

What was measured, and what was not. Nothing here was measured against a running server. which nats-server is empty on this machine, and so are docker and podman, so crates/nats/weida-nats/tests/interop_nats_server.rs is eight #[ignore]d tests carrying the install command. And there is no substitute: unlike the AMQP half of this workstream, where fe2o3-amqp ships an acceptor and therefore a foreign server that runs anywhere, NATS has none — async-nats is a client, and two clients cannot talk to each other, because routing, queue-group selection and request-reply all live in the server. So absence means no interop, not weaker interop, and §9 says what the eight tests would establish rather than what they did.

Every claim below is therefore one of two kinds, and the rows say which: read from the protocol reference (docs/research/nats.md, which cites the client protocol reference and the NATS ADRs) and asserted against a scripted server that replays the reference's own byte sequences, or read from the code. No row claims a running nats-server accepted anything.

2. The twelve verbs

The client protocol reference lists twelve and there is no thirteenth. All twelve are encoded or decoded by weida-nats-codec's op.rs, each with the declared-count discipline that keeps a payload from ever being scanned for a terminator.

VerbDirectionVerdict
INFOserver → clientpresent — the first thing on the connection and again later; connection.rs merges an asynchronous one rather than replacing it, because a topology notice that omitted headers would otherwise revoke header support
CONNECTclient → serverpresent — every field of the reference as a field of ConnectionOptions, and confirmed with a PING: the reference gives CONNECT no acknowledgement, so writing CONNECT and PING together makes the handshake end in an answer whether the server accepted it or sent -ERR and closed
PUBclient → serverpresent — subject, optional reply subject, declared count
HPUBclient → serverpresent — the NATS/1.0 block, with the total checked against max_payload because headers count inside it
SUBclient → serverpresent — subject, optional queue group, client-chosen sid
UNSUBclient → serverpresent — with and without max_msgs, and the local count so a subscription that has already had that many ends at once
MSGserver → clientpresent — and a MSG carrying a sid this client could not have written is Error::Protocol rather than a message routed by guesswork: the sid is "generated by the client"
HMSGserver → clientpresent — both counts, and the header block parsed under a bounded entry count
PINGbothpresent — sent on an interval of ours and answered immediately when received
PONGbothpresent — the unanswered-PING count bounded, and at the bound the connection is State::Failed rather than pinged at again
+OKserver → clientpresent — decoded, and only ever seen with verbose: true, which this client does not set by default
-ERRserver → clientpresent — the reason passed through as octets, because an error message is remote text and a client that could not report a non-UTF-8 one would lose the only explanation it was given

3. Subjects and the two wildcards

subject.rs is a pure function, which is what lets the rules be asserted without a connection. What it is asserted against is the reference's grammar and nats-server's own documented checks — not against a running server (§1).

RuleVerdict
* matches exactly one tokenpresent
> matches one or more tokenspresent
> must be the final tokenpresent — orders.>.created is Error::InvalidSubject at the subscribe call rather than a pattern with a guessed meaning
A wildcard is a whole token or it is literal textpresent — orders.*x has a literal second token, matching nats-server's own literal-subject check, so orders.* is refused as a publish subject while orders.*x is not
The degenerate subjects "", .a, a., a..brefused with a reason, not normalised: no token may be empty. a..b and a.b are different subjects to a server that accepted them, so collapsing the dot would publish somewhere the caller did not name. nats-server answers -ERR 'Invalid Subject', so refusing locally reports the same fact one round trip earlier
Subject token character setpresent as the reference gives it — dot-separated tokens — and no stricter. The reference stops there, so this client does not invent an alphabet

4. Queue groups

BehaviourVerdict
SUB with a queue grouppresent — subscribe_with_queue_group, and the group name checked by the same rules as a subject token
One eligible delivery per publication to the grouppresent as the client half: this client declares the group and reads what arrives. Which member the server picks is the server's, and the protocol makes no promise about the distribution — so there is nothing here to implement and nothing to assert beyond the declaration
A queue group as a durable broker queueabsent, and absent from Core NATS: a queue group holds no messages, survives no disconnect and replays nothing. What a caller who wants that needs is JetStream, which is §8

5. Request-reply

There is no request verb. Request-reply is PUB with a reply subject nobody else subscribes to, and that is the whole of it (request.rs).

PieceVerdict
The inboxpresent — one wildcard subscription per connection, per-request reply subjects under it
The mandatory windowpresent, and mandatory on purpose: request takes a Duration and there is no form without one, because a request API that can hang is the failure the type exists to prevent
request_manypresent — a bounded collection inside one window, so a misbehaving responder cannot fill memory
The 503 no-responder answerpresent — negotiated in CONNECT as no_responders, and reported as Error::NoResponders, which is never collapsed into Error::RequestTimeout: 503 means "nobody was listening, now" and a timeout means "somebody may well have been"
The inbox tokenpresent, and not secret: built from the process id, a nanosecond clock reading and a process-local counter. An inbox name is a routing token; authorization is the server's, so guessing the subject buys exactly what a subject permission already allows. What correctness needs is that two requests never share a reply subject, and those three sources give that within a process, across processes and across a restart. A caller who needs an unguessable inbox sets inbox_prefix from a source it trusts

6. Authentication and transports

MechanismVerdict
No credentialspresent — and refused at connect time where INFO.auth_required is set, rather than sent and rejected
auth_tokenpresent — Credentials::Token
user and passpresent — Credentials::UserPassword, with require_tls beside it, because the password is in the clear otherwise
NKey (Ed25519 over INFO.nonce)present — Credentials::Nkey, and the signing is the caller's: options::Signer is a closure that receives the nonce unchanged and returns the signature and the public key. There is no cryptographic dependency in this crate, for the same reason the rustls::ClientConfig is the caller's — a messaging library that held a private key and chose an algorithm would be making the application's security decision. tests/handshake.rs pins that the nonce arrives byte for byte and the answer lands in CONNECT.sig
User JWT (.creds)present — Credentials::Jwt, the JWT as the credentials file carries it plus the same caller-supplied signer
Mutual TLS (the client certificate as the identity)present — the certificate is in the caller's rustls::ClientConfig, so this is a configuration of the transport rather than a CONNECT field
Exactly one credential form per connectionpresent by construction: Credentials is one enum, so a connection cannot carry two and let the server choose
TransportVerdict
TCP, port 4222present — transport.rs, the reference's "a client normally connects through a TCP/IP socket"
TLSpresent — completed before ordinary traffic where INFO.tls_required says so, which makes it the server's decision rather than the client's; --no-default-features turns the stack off for a build that has no business linking one
The trust anchorsdeliberately not in the crate — the caller's rustls::ClientConfig, because which certificates an application trusts is the application's decision and a library that chooses cannot be audited
WebSocketabsent — what is missing: the WebSocket handshake, its subprotocol and the framing over it. The server supports it and the client protocol inside it is identical, so this is an addition to transport.rs and nothing else; no caller has asked
UNIX-domain socketsabsent, and not reachable from here: the reference offers them only "when embedded in Go", so there is no wire form for a Rust client to speak

7. Bounds

Every table whose size a peer can influence has a named bound, and each is marked the protocol's own or ours (INVARIANTS.md).

BoundDefaultWhose, and what it bounds
INFO.max_payloadthe server'sthe protocol's. Learned from INFO and enforced locally, before the wire: the server would answer -ERR 'Maximum Payload Violation' and close, and one comparison in the caller's own task keeps the connection. For a headered publish the number checked is the total
max_control_linethe server'sthe protocol's. Checked on every line this client writes
max_pending64 MiB, the server'sthe protocol's, and the server's to enforce: a client cannot set it. What a client controls is how fast it drains, which is subscription_queue below
max_pings_out2the protocol's number for the server's pings, ours for the client's — the same number the server documents. At the bound the connection is State::Failed
ping_interval2 minours, and the same number nats-server's own ping_interval defaults to
max_subscriptions4096ours, and it cannot be anyone else's: the sid is "generated by the client", so nothing but the client is in a position to stop the table from growing
max_pending_requests1024ours. Inbox entries, each a request that may never be answered; the protocol has no notion of the map at all
subscription_queue512ours. Messages waiting for one application reader; a full queue drops the copy with a tracing::warn! rather than blocking, because Core NATS is at-most-once, blocking would stall every other subscription on the connection, and the server's own answer to a client that will not read is to buffer max_pending and then disconnect it
outgoing_queue256ours. Operations queued for the driver
max_connect_urls256ours, for retention. The codec refuses an array above its cap — a JSON array announces no length, so the only place to stop is while reading it — but a caller who wants to keep 2 must not turn a cluster that grew to 300 into a dropped connection, so the decode cap is never lowered below the default and the retention bound truncates afterwards
max_header_entries128ours. Entries in one NATS/1.0 block, which the block's byte count bounds only weakly
handshake_timeout15 sours. The protocol gives no deadline, not even for the opening INFO
max_resolved_addresses8ours. What a hostname may expand to

8. JetStream, clusters, gateways and leaf nodes

All four are absent, as server features, and each row says what a client would need to reach it. This is the part of NATS that is largest by documentation and smallest by client protocol: none of it adds a verb.

FeatureVerdict
JetStreamabsent — it is a persistence layer over Core NATS, reached by publishing request messages to $JS.API.* subjects and reading JSON replies. What is missing: the $JS.API request/reply schemas (stream and consumer create, update, info, delete), the acknowledgement protocol on a pull consumer (+ACK, -NAK, +WPI, +TERM as payloads, not verbs), the Nats-Msg-Id de-duplication window, and the double-ack the documentation calls "exactly once". A client that wanted it would add a module on top of this crate and no new verb, because the transport is PUB, SUB and MSG throughout
Clustersabsent as a feature, present as the one thing a client sees: INFO.connect_urls is retained and readable, which is the cluster's whole client-visible surface. What is missing: anything that uses it, because redialling is the caller's — see the reconnect row below
Gateways, leaf nodesabsent — server-to-server topologies. There is no client-protocol surface at all: a client connected through a leaf node speaks the same twelve verbs and cannot tell
Reconnectabsent, deliberately — "client reconnection policy is a client-library policy, not a Core NATS wire guarantee". What is missing: a redial loop and automatic re-subscription. What is present is the material a policy needs: Connection::closed reports why it ended, the retained connect_urls, and Connection::lame_duck_notice for the server's own "I am going away" signal. Every subscription ends with the connection, which is the protocol's behaviour rather than a limitation — a core subscription has no stored session to resume

9. Interop: not run

crates/nats/weida-nats/tests/interop_nats_server.rs — 8 tests, all #[ignore]d, because nats-server is absent. The harness starts the server, waits for its own "Server is ready" line rather than a sleep, and stops it in Drop so a panicking test leaves nothing behind; the peer on the far side is async-nats 0.50, an independent client, so every case is this client against a foreign one through a real server rather than against itself.

What the eight would establish: our publish read by an async-nats subscriber and the reverse; both wildcards, where the point is that the server and this client's pure subject::matches agree; a queue group holding one subscriber of each client with twenty jobs, asserting the count rather than the split; request-reply in both directions; the 503 arriving immediately and distinguished from a timeout; and headers crossing the server and coming back.

What exists instead, and what it is worth: tests/handshake.rs (18), tests/subscriptions.rs (12) and tests/request_reply.rs (10) drive a scripted in-process server that replays the reference's own byte sequences. They are assertions about the bytes this client writes and reads, checked against the protocol reference — not evidence that a real server accepts them. cargo tree -p weida-nats --no-dev-dependencies reaches neither weida nor weida-protocol, and the async-nats peer resolves to the same rustls 0.23 already in this workspace rather than adding a second TLS stack.

10. The definition of done

0013 §4.7's six clauses, read for Core NATS, each with its verdict.

ClauseVerdict
1. Every verb of the client protocol reference implemented or absent with a reasonmet — §2: all twelve present
2. The transports the reference defines, with the rest named absentmet — §6: TCP and TLS present; WebSocket absent with what is missing named; UNIX-domain sockets have no wire form for a non-Go client
3. The authentication mechanisms a client can offermet — §6: all five credential forms present, exactly one per connection by construction, and no cryptographic dependency
4. Interop in both roles against an independent implementationNOT met, and stated rather than worked around — §9. nats-server is absent and there is no pure-Rust substitute, because async-nats is a client. This is the one clause this library cannot claim, and it stays unclaimed until the binary is installed and cargo test -p weida-nats --test interop_nats_server -- --ignored runs
5. The subject model: both wildcards, queue groups, request-reply over inboxesmet — §3, §4, §5
6. A feature-parity table, row by row, where no row says "partial" without saying what is missingmet — this document

What a caller who compares this library with a full-featured NATS client does not get, in one list: JetStream and its $JS.API schemas; a reconnect loop; the WebSocket transport; UNIX-domain sockets; a cryptographic implementation of NKey or JWT signing, which is a closure the caller supplies instead; and interop evidence, until a server is installed. Nothing else.

11. Sources

  • ../research/nats.md — §1-§4 (the text protocol, the twelve verbs, INFO/CONNECT, PING/PONG, PUB/SUB/UNSUB/MSG, HPUB/HMSG, queue groups and the inbox), §5 (delivery and the server's buffering), §11 (the limits), §12 (the authorization boundary), §13 (the ecosystem and async-nats), and the numbered sources behind the server-behaviour claims.
  • 0013 §4.1 (crate layout), §4.2 (the dependency direction), §4.3 (the codec stays sans-I/O), §4.4 (the shape of the library and the option rule), §4.7 (the six clauses).
  • 0014 §2 (client and not server; what is a server feature), §3 (the interop peers, and that nats-server under the supervisor is the only route to one here).
  • Code the verdicts were read from, under crates/nats/: weida-nats-codec/src/ (op.rs, headers.rs, info.rs, connect.rs, json.rs, limits.rs) and weida-nats/src/ (connection.rs, subscription.rs, request.rs, subject.rs, message.rs, options.rs, transport.rs, error.rs), plus weida-nats/tests/ (handshake.rs, subscriptions.rs, request_reply.rs, interop_nats_server.rs).