Native Protocol Mocks (gRPC, WebSocket, Kafka, RabbitMQ, NATS, SMTP)
Mockarty can serve gRPC and WebSocket mocks natively — real protocol traffic straight from the platform, with no generated server binaries to build or run. You upload an API contract (for gRPC), create mocks as usual, and point your client at Mockarty.
Native serving works alongside the classic paths (generated servers and resolve endpoints). Nothing changes for existing mocks.
In Mocks → Constructor, choose the protocol before writing the reply. The
reply field and its example change with the protocol: gRPC, MCP and GraphQL use
JSON-style replies, SOAP uses XML, and Kafka/RabbitMQ/NATS/Socket messages can
be plain text. The editor changes its highlighting and hint when you switch
protocols; it does not rewrite content you already entered. Review the reply
before saving or pressing Test.
Native gRPC
How it works
- You publish your
.proto(a file or a zip archive of files) as a gRPC contract in the Contract Registry. - Mockarty compiles the contract and knows every service, method and message type it declares.
- A native gRPC listener answers real gRPC calls for those methods, resolving each call through your gRPC mocks — including request conditions, JsonPath/Faker templating, scripted responses, delays and error codes.
- Server reflection is on: tools like
grpcurland Postman discover your services without needing the.protolocally.
Enable the listener
Set the port before starting Mockarty (the listener is off by default):
MOCKARTY_GRPC_NATIVE_PORT=5890 ./mockarty
Optional: MOCKARTY_GRPC_NATIVE_NAMESPACE sets the default namespace for calls that don’t specify one (falls back to the standard default namespace).
Publish a gRPC contract
Contracts → Registry → Publish, with spec type gRPC and your .proto content. Or via API:
curl -X POST http://localhost:5770/api/v1/contract/registry \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{
"serviceName": "greeter",
"specType": "grpc",
"specContent": "syntax = \"proto3\";\npackage demo;\nservice GreeterService {\n rpc SayHello(HelloRequest) returns (HelloReply);\n}\nmessage HelloRequest { string name = 1; }\nmessage HelloReply { string message = 1; }"
}'
Active gRPC contracts are picked up by the native listener automatically (within about a minute, or immediately after you open them via the services API below).
Inspect services and message skeletons
curl http://localhost:5770/api/v1/contract/registry/<contract-id>/grpc-services \
-H "Authorization: Bearer $TOKEN"
The response lists every service and method with its type (unary, server_stream, client_stream, bidi_stream) and ready-to-edit request/response JSON skeletons — copy the response skeleton into your mock’s payload and fill in the values. AI agents use the same data via the grpc_contract_services MCP tool.
Create the mock
A regular gRPC mock — service, method, and a response payload:
curl -X POST http://localhost:5770/api/v1/mocks \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{
"id": "greeter-hello",
"namespace": "sandbox",
"grpc": {"service": "demo.GreeterService", "method": "SayHello"},
"response": {"payload": {"message": "Hello, $.req.name!"}}
}'
$.req.* reads fields from the decoded gRPC request; all Faker functions work too.
Call it
grpcurl -plaintext -d '{"name":"Ada"}' localhost:5890 demo.GreeterService/SayHello
# {"message": "Hello, Ada!"}
grpcurl -plaintext localhost:5890 list # reflection: discover services
grpcurl -plaintext localhost:5890 describe demo.GreeterService
To scope a call to a namespace or a specific mock server name, send gRPC metadata:
| Metadata key | Meaning | Default |
|---|---|---|
x-mockarty-namespace |
namespace to resolve mocks in | the configured default |
x-mockarty-server |
mock server name filter | none |
With the multi-tenant switch on (MOCKARTY_NATIVE_BROKERS=1, see
One port, many namespaces) the
metadata is not needed: call <namespace>.mock.example.com:5771 over TLS and the
host selects the namespace — grpcurl -insecure team-a.mock.example.com:5771 demo.GreeterService/SayHello.
One call from the API tester
Tested a real gRPC upstream in the API tester and want Mockarty to serve that
answer as a mock? One request registers the contract and creates the mock:
curl -X POST http://localhost:5770/api/v1/api-tester/grpc/mock-with-contract \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{
"serverAddress": "payments.internal:9090",
"requestData": {"service": "payments.PaymentService", "method": "Charge", "body": "{\"amount\":100}"},
"responseData": {"body": "{\"status\":\"APPROVED\"}"}
}'
The proto schema is pulled via server reflection from serverAddress (or pass
protoContent with the .proto source instead). Registration is idempotent:
an identical spec reuses the existing contract ("contractReused": true),
a changed spec updates it in place with a new version — repeats never pile up
duplicate contracts. In the UI the same happens automatically when you click
Create Mock on a gRPC response. AI agents get this as the
grpc_mock_with_contract MCP tool.
Streaming
All four gRPC kinds are served:
- Server streaming — make the mock’s payload a JSON array; each element is sent as one message.
- Client streaming — the client sends a sequence; mock conditions match the last message; one response is returned.
- Bidirectional — every incoming message is matched independently and answered with the mock’s response (an array payload sends a burst of messages per request).
Errors
Give the mock a gRPC status code (1–16) and/or an error message — the native listener returns a real gRPC error:
{"response": {"statusCode": 7, "error": "permission denied for this user"}}
Rich error details. Attach the same standard google.rpc status details the generated server emits — errorInfo, retryInfo, badRequest, quotaFailure, preconditionFailure, help, debugInfo, localizedMessage, requestInfo, resourceInfo — and the native listener puts them on the wire so a real client reads them via status.Details():
{"response": {"statusCode": 8, "error": "quota exceeded",
"errorDetails": [
{"type": "errorInfo", "details": {"reason": "QUOTA", "domain": "shop"}},
{"type": "retryInfo", "details": {"retryDelay": "30s"}}
]}}
Calls to methods no contract declares, or with no matching mock, return clear Unimplemented / NotFound errors that say exactly what to fix.
Decode a captured payload
A call whose method no contract declares is still captured — the raw
protobuf bytes land on the Undefined Requests page as an opaque payload.
Open the request and decode it right there: pick a descriptor source —
published contracts (default), a live reflection server, or an uploaded
.proto (optionally saved to the contract registry for reuse) — and the
payload turns into readable JSON. Edit in Constructor then pre-fills the
mock with the decoded body.
The same decoding is available over the API (and to AI agents as the
grpc_decode MCP tool):
curl -X POST http://localhost:5770/api/v1/api-tester/grpc/decode \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d '{
"service": "orders.OrderService", "method": "PlaceOrder",
"direction": "request",
"payloadBase64": "CgVTS1UtNxAD",
"source": {"kind": "reflection", "serverAddress": "orders.internal:9090"}
}'
# {"service":"orders.OrderService","method":"PlaceOrder","direction":"request","json":"{\"sku\":\"SKU-7\",\"quantity\":3}"}
Omit source to decode against every loaded contract. With
{"kind":"proto","protoContentBase64":"...","publish":true} the uploaded
.proto is also registered as a gRPC contract and the response carries its
publishedContractId.
One-of, delay and templating
Everything the HTTP path supports works natively too: oneOf rotating/random responses (sequential order advances per call), delay (honoured before the reply, never outliving the caller), JsonPath/Faker templating ($.req.*, $.fake.*), condition matching and scripted responses.
Native WebSocket
Socket mocks can be exercised over a real WebSocket connection — no generated socket server needed.
Connect and exchange events
ws://localhost:5770/ws-mock/<serverName>?namespace=<namespace>
Each message you send is a JSON object with an event (or type) field; the rest is the payload:
{"event": "order.created", "id": "ord-77", "total": 129.90}
Mockarty matches your socket mocks by server name + event, applies conditions and templating ($.req.* sees the message you sent), and replies with the mock’s payload as one JSON message. Each connection is independent — open as many at once as you like, and every one gets its own matched, templated reply. Delays and error codes work the same as everywhere else; failures come back as a self-explanatory frame without closing the connection:
{"error": {"code": 5, "message": "no socket mock for orders/order.updated in namespace sandbox — create one with event \"order.updated\""}}
Quick test from a browser console:
const ws = new WebSocket("ws://localhost:5770/ws-mock/orders?namespace=sandbox");
ws.onmessage = (m) => console.log("reply:", m.data);
ws.onopen = () => ws.send(JSON.stringify({event: "order.created", id: "ord-77"}));
Streaming a burst of events
Give the mock an array payload and Mockarty pushes each element as its own
message, in order — one inbound event can stream a whole sequence back on that
connection alone ($.req.* is templated in every element):
{
"socket": {"serverName": "feed", "event": "subscribe"},
"response": {"payload": [{"seq": 1, "id": "$.req.id"}, {"seq": 2}, {"seq": 3}]}
}
Webhooks
A socket mock’s webhook callbacks fire after the reply is sent, exactly as they
do for HTTP and the other native protocols — so a socket event can trigger a
downstream call in addition to answering the client.
Native TCP / UDP sockets
Socket mocks are also served over raw TCP and UDP — the same mocks that
answer on WebSocket, with no generated server binary. Enable the listeners:
MOCKARTY_TCP_NATIVE_PORT=9400 MOCKARTY_UDP_NATIVE_PORT=9401 ./mockarty
Wire protocol: one JSON message per line (TCP) or per datagram (UDP),
with an event (or type) field; replies come back the same way. Conditions,
JsonPath/Faker templating, delays, error codes, webhook callbacks and
undefined-request capture all work exactly like the WebSocket path — and an
array payload streams one message per element:
# TCP: newline-delimited JSON
printf '{"event":"order.created","id":"ord-7"}\n' | nc localhost 9400
# {"status":"accepted","id":"ord-7"}
# UDP: one datagram in, one (or a burst) out
printf '{"event":"ping"}' | nc -u -w1 localhost 9401
Optional env: MOCKARTY_SOCKET_NATIVE_NAMESPACE, MOCKARTY_SOCKET_NATIVE_SERVER
(the default socket server name; a message can override it with its own
"serverName" field). Unmatched events return a self-explanatory
{"error":{...}} frame and are recorded as undefined requests.
See also
- Contract Testing — publishing and validating API contracts
- JsonPath Guide — templating with
$.req.* - Faker Functions Reference — dynamic values in payloads
Native Kafka (preview)
Standard Kafka clients can produce to Mockarty as if it were a broker — no
generated server, no real Kafka. Enable the broker:
MOCKARTY_GRPC_NATIVE_PORT=5890 MOCKARTY_KAFKA_NATIVE_PORT=9092 ./mockarty
Point a producer at Mockarty and produce to a topic that has a Kafka mock. The
message resolves through the same pipeline as your other Kafka mocks — request
conditions, JsonPath/Faker templating, scripted responses and webhook
callbacks all apply — and the mock’s response is delivered on its output topic,
where a normal Kafka consumer reads it back (produce → mock → consume, with
no real Kafka anywhere).
Check a mock from the constructor
Open the mock in the constructor and press Test. The message body is
pre-filled from the mock’s conditions, or from the $.req.* fields its reply
and message key reference, so one click sends a message the mock can answer.
The result is the broker’s reaction, not an HTTP status:
- Mock reacted — Mock <id> names the mock that matched;
- Reaction says where the reply goes — the mock’s output topic, or
<input topic>.mockwhen none is set — plus the message key, the delay and,
for a toxic mock, that the message was dropped or answered with an error; - Reply payload is the rendered message (JsonPath and Faker resolved).
No mock reacted means nothing in the namespace matched: check the topic and
server name first, then the conditions. The message is recorded under
Undefined requests so you can turn it into a mock from there. RabbitMQ (reply
to the output queue, else <routing key>.reply), NATS (reply subject) and SMTP
(mail accepted or rejected, with the SMTP code) show the same kind of verdict.
Through the native listener
When the native listener for the protocol is running on this instance, the
test panel shows Send through the native listener with the address it will
dial, checked by default. Press Test and Mockarty connects to its own
listener with a real client — a Kafka producer, an AMQP publisher, a NATS
requester, an SMTP sender, a raw TCP socket, a gRPC call — on the real port
(with the TLS tenant host when TLS is on), sends the message from the test
fields and shows what came back on the wire:
- Mock reacted — Mock <id> with reply arrived on the output topic,
the reply queue or the reply subject, the reply headers and payload, and the
round-trip time. The native brokers stamp the matched mock’s id on every
reply as theX-Mockarty-Mock-Idheader, which is where the id comes from; - Delivered, no reply — the listener took the message, nothing answered
within the timeout: no mock matched or the mock drops its reply (toxicity); - Rejected by the listener — the mock’s own error (a Kafka error code, an
AMQP channel error, an SMTP 5xx, a gRPC status); - Native listener is off — the switch is greyed out and names the setting
that enables it; the Test button then runs the resolver check described above.
Untick the switch to run the in-process resolver check instead (matching
only, no network). The same probe is available to agents: the MCP tool
test_mock with a mockId of a Kafka / RabbitMQ / NATS / SMTP / socket / gRPC
mock goes through the native listener and returns the verdict, the reply and
the matched mock id; the REST endpoint is
POST /api/v1/mocks/{id}/probe-native.
Give a consumer something to read
The flow above starts with a producer. When the service you are testing
consumes from a topic instead, there is nothing to produce first — so let
the mock fill the topic itself.
In the mock constructor pick the Kafka protocol and open Seeded stream
(for consumers). Add the records the topic should already hold — a value, and
a key if your consumer needs one — and choose how they are delivered:
- Once, in order — the records are read front to back and the topic then
goes quiet, like a topic a producer stopped writing to. - Repeat forever — the set is served again and again, for a consumer that
must keep receiving while you observe it.
Point your consumer at Mockarty and start it. It reads the records straight
away; nothing needs to produce anything. Values support the same JsonPath and
Faker expressions as any other response, and are rendered once when the topic
is filled, so re-reading an offset returns the record it returned before.
A real produced message always wins: if something does produce to the topic,
that message is what the consumer sees.
Ask the broker about your consumer groups
A standard Kafka admin client can list the consumer groups on Mockarty and
describe one of them, which is the simplest way for a test to assert that the
service under test really joined its group:
client := &kafka.Client{Addr: kafka.TCP("localhost:9092")}
groups, _ := client.ListGroups(ctx, &kafka.ListGroupsRequest{})
info, _ := client.DescribeGroups(ctx, &kafka.DescribeGroupsRequest{GroupIDs: []string{"orders-consumer"}})
A group that has never joined comes back as Dead with no members, so
“nobody connected” is an ordinary answer rather than an error you have to
special-case.
Create and delete topics
Most services run an admin client on start-up — “make sure my topics exist” —
before they produce anything. Mockarty answers that: a standard Kafka admin
client can create and delete topics.
client := &kafka.Client{Addr: kafka.TCP("localhost:9092")}
client.CreateTopics(ctx, &kafka.CreateTopicsRequest{
Topics: []kafka.TopicConfig{{Topic: "orders.new", NumPartitions: 1, ReplicationFactor: 1}},
})
A created topic becomes real straight away — it appears in the next metadata
response, accepts produce, and serves fetch — so a service that creates and
then immediately produces behaves exactly as it would against a real broker.
Creating a topic that already exists returns “already exists” rather than
failing, which is what an idempotent start-up expects.
Deleting removes the topic and any messages buffered on it. Topics that come
from a mock are the exception: the mock defines them, so they cannot be
deleted over the wire — change or remove the mock instead.
Mockarty serves one partition per topic and reports that back, whatever
partition count you request. A consumer’s fetch limit is respected: ask for a
small MaxBytes and the broker answers within it, across as many fetches as the
topic needs.
Consumer groups keep their committed offsets, so a consumer that stops and
starts again resumes where it left off instead of replaying the topic. Note that
every consumer in a group receives the whole stream — Mockarty does not split a
topic between the members of a group, so testing how two instances of your
service share a workload needs a real broker.
A mock can also inject typical broker errors by setting its response status
to a Kafka error code (for example 6 NOT_LEADER_FOR_PARTITION, 3
UNKNOWN_TOPIC_OR_PARTITION, 19 NOT_ENOUGH_REPLICAS) — the producer sees a real
broker error, which is invaluable for testing retry/failover logic.
Optional env: MOCKARTY_KAFKA_NATIVE_ADVERTISE_HOST (the host clients should
reach the broker at, when Mockarty is behind a proxy/NAT),
MOCKARTY_KAFKA_NATIVE_NAMESPACE, MOCKARTY_KAFKA_NATIVE_SERVER.
Native RabbitMQ
A standard AMQP 0-9-1 client can publish to Mockarty as if it were RabbitMQ — no
generated server, no real RabbitMQ. Enable the broker:
MOCKARTY_AMQP_NATIVE_PORT=5672 ./mockarty
Point an AMQP client at Mockarty (amqp://guest:guest@host:5672/), publish to a
routing key that matches a RabbitMQ mock (with the default exchange the routing
key is the queue name). The message resolves through the same pipeline as your
other RabbitMQ mocks — conditions, JsonPath/Faker templating, scripted responses
and webhook callbacks — and the mock’s reply is delivered on its output queue,
which a normal consumer reads back (basic.consume push or basic.get pull).
After basic.cancel, later replies stay on the queue for basic.get or the
next consumer. Replies waiting for a consumer whose connection closes return
to the queue.
Message headers. Headers you set on the published message (the AMQP
headers table) reach the mock: use them in the mock’s header conditions to
select a response, and reference them in the payload with $.header.<name>
(every value — string, boolean, number — is available as a string).
Dead-lettering. A queue declared with x-dead-letter-exchange (and
optionally x-dead-letter-routing-key) behaves like on RabbitMQ: a delivery your
consumer rejects or nacks without requeue is republished to the dead-letter
exchange and reaches the queues bound to it (queue.bind; direct, fanout and
topic exchanges, or the default exchange "" with the queue name as the key),
carrying x-first-death-queue / x-first-death-reason headers. With
x-message-ttl, a reply nobody consumed within the TTL is dead-lettered with
reason expired. A reject or nack with requeue puts the message back on its
queue. Without a dead-letter exchange a rejected message is dropped. The
dead-letter setup and bindings belong to the node the client declared them on.
Queue length, priorities and quorum queues. A queue declared with
x-max-length or x-max-length-bytes holds at most that much. When it is full
the oldest message is dropped (x-overflow: drop-head, the default) and, if the
queue has a dead-letter exchange, dead-lettered with reason maxlen;
x-overflow: reject-publish keeps the queue and drops the new message instead,
and reject-publish-dlx also dead-letters it. A queue declared with
x-max-priority serves higher-priority messages first (a mock sets its reply’s
priority in outputProps; a priority above the queue’s maximum counts as the
maximum). A queue declared with x-queue-type: quorum counts each return of a
message in the x-delivery-count header, and with x-delivery-limit it
dead-letters a message returned more times than the limit (reason
delivery_limit). x-delivery-count arrives as a number, as on RabbitMQ. With several nodes sharing their broker state, the length
limits and priorities apply cluster-wide.
Consumers, purge and delete. Several consumers of one queue receive its
messages in turn. A queue declared with x-single-active-consumer: true sends
everything to the earliest consumer, and to the next one once that consumer
cancels or disconnects. When a channel or connection closes, the messages it
had not acknowledged go back to their queue and arrive again marked
redelivered. A queue declared with x-consumer-timeout (milliseconds) closes
a channel that holds one of its deliveries unacknowledged for longer, with
error 406, and returns the delivery the same way. queue.purge drops the waiting messages and reports how many;
messages still awaiting an acknowledgement stay. queue.delete drops the
queue’s messages, its declare arguments and bindings, and cancels its
consumers (the client library closes their delivery channels); with
if-unused or if-empty it refuses a queue that has consumers or messages,
with error 406, as RabbitMQ does. A queue declared with x-expires is deleted
the same way once nobody has consumed, read or re-declared it for that long.
With several nodes sharing their broker state, purge and delete apply
cluster-wide, while the single active consumer and x-expires are judged by
each node for the consumers connected to it.
Stream queues. A queue declared with x-queue-type: stream is a log:
consuming removes nothing, and every consumer reads it from the point it asks
for with the x-stream-offset consume argument — first, last, next (the
default: only new messages), an offset number, a timestamp, or an interval such
as 1h (messages published in the last hour). Each delivery carries its
position in the x-stream-offset header. The log keeps messages within
x-max-length, x-max-length-bytes and x-max-age (for example 7D),
dropping the oldest first. As on RabbitMQ, a stream consumer must use manual
acknowledgement with a prefetch (basic.qos), and basic.get and
queue.purge are refused with error 406. The stream is held by the node that
received the messages.
What the broker does not do. x-dead-letter-strategy: at-least-once is
accepted, but a dead-lettered message that no queue is bound to receive is
dropped rather than kept until it can be routed. The node writes one warning
per queue naming it. If your test depends on it, it needs a real RabbitMQ.
Reply headers and message properties. The mock’s response headers arrive on
the delivery as the AMQP headers table, and a mock can set the standard AMQP
message properties on its reply through its outputProps block:
"rabbitmq": {
"queue": "rpc.in",
"outputQueue": "rpc.out",
"outputProps": {
"correlationId": "$.req.correlationId",
"replyTo": "rpc.out",
"contentType": "application/json",
"deliveryMode": 2
}
}
Values support the same JsonPath and Faker expressions as any response, which is
what makes the classic RPC over RabbitMQ shape work end to end: your client
publishes with a correlation id, the mock echoes it back on the reply, and the
client matches the answer to its request exactly as it would against a real
broker. correlationId, replyTo, contentType, messageId, type, appId,
deliveryMode and priority are all delivered.
Named exchanges. Publish to the default exchange ("", routing key = queue
name) or to a named direct exchange whose routing key equals the queue name.
A mock that declares an exchange is isolated to it — it answers a publish
to that exchange only, never another exchange that happens to share the routing
key. A mock with no exchange set stays usable from both the default exchange
and a direct named exchange. (Direct and default routing are supported; topic/fanout fan-out to
differently-named queues is not.)
Optional env: MOCKARTY_AMQP_NATIVE_NAMESPACE, MOCKARTY_AMQP_NATIVE_SERVER.
Native NATS
A standard NATS client can connect to Mockarty as if it were a NATS server — no
generated server, no real NATS. Two ways to reach it:
On the shared TLS port (no extra port at all — see
One port, many namespaces). The
client must handshake TLS first and name the protocol in the host:
tls://team-a.nats.mock.example.com:5771 # nats.TLSHandshakeFirst() / nats --tlsfirst
On its own port — for a classic client that waits for the plaintext INFO
line before upgrading:
MOCKARTY_NATS_NATIVE_PORT=4222 ./mockarty
Point any NATS client at nats://host:4222 and publish a message. It resolves
through the same pipeline as your other mocks — subject matching, message-data
and header conditions, JsonPath/Faker templating, scripted responses and
toxicity — and the mock’s reply is published back:
# request/reply — the client gets the mock reply on its inbox
nats request orders.new '{"id":7}'
# {"orderId":"7","status":"accepted"}
Subjects and wildcards. A mock’s subject is the routing key. It may use
the two NATS wildcards, matched the way a real server matches a subscription:
*matches exactly one token —orders.*answersorders.newbut notorders.eu.new.>matches one or more trailing tokens —orders.>answersorders.newandorders.eu.new.
Request/reply and pub/sub. For request/reply the reply is published to the
request’s reply subject (the client’s inbox) automatically. For pub/sub, set the
mock’s output subject and any subscriber on that subject receives the reply.
Resolution order for the reply target: the mock’s outputSubject, else its
replySubject, else the request’s reply-to, else <subject>.reply.
Message headers. Headers you set on the message (NATS HPUB/message
headers) reach the mock: use them in header conditions and reference them in
the payload with $.header.<name>. A mock reply that declares response headers
is delivered with a header block (HMSG), which a header-aware client reads back.
Queue groups. A mock can declare a queue group; it’s exposed to scripts
as mk.request.attrs.queueGroup so a scripted response can branch on it.
JetStream
Applications that use JetStream instead of core NATS work against the mock too —
you just have to say so, because a core request and a JetStream publish look
identical on the wire. Declare the stream on a NATS mock:
{
"protocol": "nats",
"nats": {
"subject": "orders.new",
"jetstream": {
"stream": "ORDERS",
"subjects": ["orders.>"],
"maxMsgs": 1000
}
},
"response": { "payload": { "status": "accepted" } }
}
One mock carrying a jetstream block switches JetStream on for its whole
namespace. stream is the stream name your client looks up; subjects are the
subjects it captures (the same * / > wildcards). Leave subjects out and the
stream captures the mock’s own subject. Optional: maxMsgs (how many messages
the stream keeps for replay), storage, retention, replicas, description,
domain — reported back to the client as the stream’s configuration.
Several mocks may name the same stream; their subjects are merged into it.
With that in place a JetStream client works as usual:
nats stream info ORDERS
nats publish orders.new '{"id":7}' # acknowledged: stream ORDERS, sequence 1
nats consumer add ORDERS workers --pull
nats consumer next ORDERS workers
What the mock serves:
- Publish — a publish to a stream subject is answered with a real
acknowledgement carrying the stream name and a sequence number that grows with
every message. - Streams — account info, create, update, info, list, names (including the
“which stream owns this subject?” lookup a client does before subscribing),
purge and delete. A client that provisions its own stream at start-up works;
re-creating an existing stream is idempotent. - Consumers — create, info, names, list, delete. Both push subscriptions
(the client gets messages on its delivery subject, with the message’s own
subject and JetStream metadata) and pull fetches. - Replay — a subscriber that attaches after the publish still receives the
messages the stream is holding, so tests don’t have to race the producer. - Durable push consumers outlive their client — while nobody is subscribed
to a durable consumer’s delivery subject, new messages wait for it; when the
client comes back and subscribes with the same durable name, it receives them
and nothing it already got. (A client that callsUnsubscribe()deletes the
consumer it created, as on a real server.) - Acknowledgements — delivered messages carry an ack subject, so
msg.Ack()andmsg.Metadata()behave normally on the client side.
Mock responses on a stream subject. The mock still runs for a JetStream
publish — its condition matching, templating and scripts all work. Its response
goes to the mock’s output subject (or replySubject), because the
publisher’s inbox already carries the publish acknowledgement. That makes the
“consume, transform, publish elsewhere” shape easy to mock: a message on
orders.new produces a mock response on orders.processed.
If JetStream is not declared, a client calling JetStream against the mock
gets an immediate, explicit “JetStream not enabled” error instead of waiting out
its timeout — and plain core NATS mocks keep behaving exactly as before.
Limits. The mock keeps stream messages in memory and does not persist
anything (unless the cluster shared scope described under the cluster note keeps
them in the database): restarting Mockarty empties the streams, and each stream holds a
rolling window (1000 messages by default, maxMsgs to change it) rather than an
unbounded history. Not implemented: redelivery of unacknowledged messages,
duplicate-message suppression, stream mirrors and sources, fetching a stored
message by sequence, Key/Value and Object stores. These are broker durability
features — the mock exists to let your application’s JetStream code run, not to
replace a NATS cluster.
Optional env: MOCKARTY_NATS_NATIVE_NAMESPACE, MOCKARTY_NATS_NATIVE_SERVER,
MOCKARTY_NATS_NATIVE_SERVER_ID (announced in the INFO greeting). Enable TLS
multi-tenancy (one port, namespace from the SNI host) with
MOCKARTY_NATS_NATIVE_TLS=1.
Connection ceiling. The listener refuses connections beyond
MOCKARTY_NATS_MAX_CONNECTIONS (CPU-scaled, default 64–512) and, per tenant,
beyond MOCKARTY_BROKER_NS_MAX_CONNECTIONS (default 100) — wherever the
connection arrived, its own port or the shared TLS port.
Native SMTP
A standard mail client can send mail to Mockarty as if it were an SMTP server —
no generated server, no real mail server. Two ways to reach it:
On the shared TLS port (no extra port at all — see
One port, many namespaces). The
client connects with implicit TLS (SMTPS) and names the protocol in the host:
team-a.smtp.mock.example.com:5771 # connect with TLS, then read the 220 greeting
On its own port — for a client that expects a plaintext greeting and
upgrades with STARTTLS:
MOCKARTY_SMTP_NATIVE_PORT=2525 ./mockarty
Point any SMTP client at host:2525 and send a message. It resolves through the
same pipeline as your other SMTP mocks — sender / recipient / subject / body /
header conditions, JsonPath/Faker templating, scripted responses and webhook
callbacks — and the mock’s response drives the SMTP reply: accept the mail
(250) or reject it with a 4xx/5xx (set the mock’s status code, or return
statusCode / statusMessage / acceptMail from the payload). A message that
matches no mock is accepted (250) like a normal relay and recorded as an
undefined request so you can build a mock from it.
What the mock serves. HELO / EHLO, AUTH, MAIL FROM, RCPT TO,
DATA, RSET, NOOP, VRFY, QUIT, and STARTTLS when TLS is enabled. The
EHLO reply advertises 8BITMIME, AUTH PLAIN LOGIN and the message size
limit. A message is delivered verbatim, so multi-part MIME with attachments,
folded headers and body lines starting with a dot all arrive intact, and every recipient of a multi-recipient
message reaches the mock in order. MAIL FROM:<> — the null return-path every
bounce and delivery-status notification carries — opens a normal transaction, so
bounce handling can be tested too.
Limits. One message is capped at 1 MB and one command line at 64 KB; a
larger message is refused with 552, an over-long command line with 500. A
transaction may carry up to 100 recipients; further RCPT TO commands answer
452. A session that goes quiet for 5 minutes is closed, as is one that fails
AUTH five times. Not implemented: mailbox verification (VRFY answers 252,
since a mock cannot know a mailbox); CRAM-MD5 and other challenge-response
authentication mechanisms; pipelining, CHUNKING/BDAT and delivery-status
extensions. Mail is resolved and reported, never stored or forwarded — unless the
mock is a proxy mock, which relays it to a real upstream MTA.
Authentication (AUTH PLAIN / AUTH LOGIN). The listener advertises both
mechanisms, so a service that logs into a real mail server can be pointed at the
mock without changing its configuration — and by default any credentials
are accepted. Nothing to set up: your application keeps sending the username
and password it always sends, and the mail goes through.
Declare an auth block on an SMTP mock when you want to test authentication
itself:
{
"smtp": {
"auth": { "username": "app", "password": "s3cret" }
},
"response": { "payload": { "statusCode": 250 } }
}
- username / password — the credentials the mock expects. An empty field
means “any”:{"username": "app"}letsappin with any password,
{"password": "s3cret"}lets any user in with that password. Anything the
policy does not accept is answered with535. - reject — every login is answered with
535. This is how you drive your
application’s authentication-failure branch without inventing a wrong
password. Combined with a username it refuses only that login and leaves
everyone else alone. - required — an unauthenticated
MAIL FROMis answered with530, which
proves your application really does authenticate.
These are TEST credentials stored as part of the mock — never put a real
production secret there. The policy applies to the whole namespace: if several
mocks declare one, they are evaluated in mock priority order and the first one
whose username matches the presented login decides. When a client has
authenticated, the login joins the data the mock is matched against: put a
condition on the path authUser in any of the SMTP condition groups to answer
differently per logged-in account, and use $.req.authUser in the response
payload or a script. It also appears on the request-log entry. An
unauthenticated session carries no authUser, so such a condition simply does
not match it.
AUTH with and without TLS. Both work. Over TLS the credentials travel
encrypted, which is what most mail clients insist on: Go’s net/smtp, for
example, refuses to send AUTH PLAIN over a plaintext connection unless the
server is localhost. On a local or CI setup — the normal place to run a mock —
plaintext AUTH therefore works as-is; when the mock runs on another host, use
the shared TLS port (implicit TLS) or turn on MOCKARTY_SMTP_NATIVE_TLS=1 and
let the client STARTTLS first. AUTH is advertised in all three cases, before
and after the handshake — except on an already-encrypted session, where
STARTTLS is not offered (there is nothing to upgrade to).
Connection ceiling. A native broker listener refuses connections beyond
MOCKARTY_*_MAX_CONNECTIONS (CPU-scaled, default 64–512) and, per tenant,
beyond MOCKARTY_BROKER_NS_MAX_CONNECTIONS (default 100). A tenant at its
ceiling is answered with 421 before the greeting, and other tenants on the
same listener are unaffected.
The per-tenant ceiling is visible on /metrics for every native listener
(protocol is kafka, amqp, nats or smtp):
mockarty_native_broker_connections{protocol,namespace} (open connections per
tenant; a tenant with none disappears from the series),
mockarty_native_broker_namespace_cap{protocol} (the ceiling in force) and
mockarty_native_broker_refused_total{protocol,namespace} (connections refused
at the ceiling). A listener refusing tenants no longer looks like an idle one.
Optional env: MOCKARTY_SMTP_NATIVE_NAMESPACE, MOCKARTY_SMTP_NATIVE_SERVER,
MOCKARTY_SMTP_NATIVE_HOSTNAME (announced in the greeting/EHLO). Enable TLS
multi-tenancy (one port, namespace from the SNI host) with
MOCKARTY_SMTP_NATIVE_TLS=1.
Status codes are SMTP codes. 250 accepts, 421/451 are temporary
failures, 550 rejects. A mock authored with an HTTP-style code below 211
is sent as 250 (accepted), so set a real SMTP code when you mean to reject.
The status text is one line: line breaks in it (easy to get when you template it
from $.req.body) become spaces, and a text longer than the 480-character SMTP
reply budget is truncated.
Toxicity (chaos) for message mocks
Kafka, RabbitMQ, SMTP and socket mocks can inject faults into a matched
mock’s own reply — the message-broker analogue of the recorder’s HTTP
toxics. Open the mock’s Toxicity (chaos) section in the constructor, or set
a toxics block on the protocol context:
{
"kafka": {
"topic": "orders.events",
"toxics": { "delayMs": 200, "delayJitter": 100, "errorRate": 0.1, "dropRate": 0.05 }
}
}
- delayMs / delayJitter — latency before the reply, plus a random
0..jitter. - errorRate (0–1) — fraction of matched calls answered with an injected
protocol error instead of the reply (Kafka error code, an AMQP channel error,
an SMTP4xx/5xx, or a socket error frame). Tune the wire error with
errorCode/errorMessage. - dropRate (0–1) — fraction silently dropped: no reply at all (Kafka/AMQP
still accept the produce/publish — a real broker does — but buffer nothing;
SMTP answers421), so you can test client timeouts and at-least-once handling.
Leave every field at 0 (or omit toxics) to disable. Drops win over errors —
there is no reply to inject an error into — and the delay applies before either.
Desktop: native brokers out of the box
The Desktop build needs no configuration at all for native protocols.
It starts with the unified port already on: Kafka, RabbitMQ and TCP-socket
mocks are served on the main Desktop port (the one the browser opens),
demultiplexed by first bytes, so there is no extra port to discover. The
protocols that physically need their own port (server-speaks-first: NATS,
SMTP; and UDP) listen on 127.0.0.1 only, derived from the HTTP port:
| Protocol | Where |
|---|---|
| Kafka / RabbitMQ / TCP socket | the main Desktop port (e.g. localhost:5780) |
| gRPC | the main Desktop port, plaintext HTTP/2 (grpcurl -plaintext localhost:5780 …) |
| MCP | main port + 1 |
| NATS | main port + 2 |
| SMTP | main port + 3 |
| UDP | main port + 4 |
The Desktop port is plaintext (no certificate to install), so NATS and SMTP
keep their own loopback ports there: the shared-port routing for them needs TLS
(see Why two protocols put their name in the host).
On a server with MOCKARTY_NATIVE_BROKERS=1 both are also served on the HTTPS
port.
Every native listener binds 127.0.0.1 — nothing is reachable from the
network. Mocks created without a namespace land in sandbox, which is also
the namespace every local broker resolves against, so a mock you build in the
constructor works with a real client immediately: point your Kafka consumer at
localhost:<main port> and it reads the seeded topic.
gRPC on Desktop. A gRPC mock is served once its service is described by a
.proto. In Mocks → Constructor, choose gRPC and press Upload .proto
next to the method picker: the file is checked, its services appear in the
picker, and choosing a method fills the service, method and a reply skeleton.
From that moment any gRPC client reaches the mock on the main port:
grpcurl -plaintext -d '{"name":"Ada"}' localhost:5780 demo.GreeterService/SayHello
A socket message sent over raw TCP or UDP names its mock server with a
"serverName" field, e.g. {"serverName":"orders","event":"order.created"}.
Every knob
(MOCKARTY_UNIFIED_PORT, MOCKARTY_NATS_NATIVE_PORT, …) can still be set
before launch to override the defaults.
Server note. On a server (non-Desktop) the unified port is opt-in via
MOCKARTY_UNIFIED_PORT=1(plaintext HTTP) orMOCKARTY_UNIFIED_TLS=1
(HTTPS + native brokers on the TLS port — see
One port, many namespaces),
and native listeners bind0.0.0.0unlessMOCKARTY_NATIVE_BIND_ADDR
narrows them. Mandatory broker authentication
(MOCKARTY_BROKER_SASL=required) is enforced on the TLS paths only — with
the plaintext unified port Kafka is not demuxed at all (the connection
panel in the mock constructor shows the reason).NATS and SMTP are never served by the PLAINTEXT unified port, whatever
else is on it. Both are server-speaks-first — their client says nothing until
the server greets it — so there is no first byte to recognise them by, and a
plaintext port carries no SNI to name them by either. On the TLS unified port
they ARE served, under a reserved host family
({namespace}.nats.<domain>,{namespace}.smtp.<domain>), because the
ClientHello’s SNI names the protocol before the server has to greet. Without
TLS, give them their own port (MOCKARTY_NATS_NATIVE_PORT,
MOCKARTY_SMTP_NATIVE_PORT).
One port, many namespaces (TLS multi-tenant)
Every namespace is its own tenant: the same topic, queue, subject, route or
gRPC method in team-a and team-b never meet, and a client selects its
tenant by the host it connects to — nothing else changes on the client.
One switch turns this on:
MOCKARTY_NATIVE_BROKERS=1 MOCKARTY_NATIVE_TLS_DOMAIN=mock.example.com ./mockarty
That single setting makes Mockarty the broker for every protocol at once:
| Protocol | Where a client connects | What selects the namespace |
|---|---|---|
| HTTP, GraphQL, SOAP, MCP, SSE, WebSocket | https://<namespace>.mock.example.com:5771/… |
the host — no /stubs/<namespace> prefix needed |
| gRPC | <namespace>.mock.example.com:5771 (TLS) |
the host — no metadata header needed |
| Kafka | <namespace>.mock.example.com:5771 (security.protocol=SSL) |
the host; the broker re-advertises it, so reconnects stay in the tenant |
| RabbitMQ | amqps://<namespace>.mock.example.com:5771/<vhost> |
the host; the vhost is yours |
| Raw TCP sockets | <namespace>.mock.example.com:5771 (TLS) |
the host |
| NATS | tls://<namespace>.nats.mock.example.com:5771 — TLS handshake first (nats --tlsfirst, nats.TLSHandshakeFirst()) |
the host. The nats label names the protocol (see below) |
| SMTP | <namespace>.smtp.mock.example.com:5771 over implicit TLS (SMTPS) |
the host. The smtp label names the protocol |
Port 5771 is the HTTPS port: HTTPS, gRPC, Kafka, AMQP, raw sockets, NATS and
SMTP all share it. Point DNS (or /etc/hosts in dev) for
*.mock.example.com, *.nats.mock.example.com and *.smtp.mock.example.com
at the Mockarty host. UDP has no TLS handshake, so UDP stays single-namespace.
Why two protocols put their name in the host
The listener tells protocols apart after the TLS handshake, from the first
bytes the client sends — that works for HTTP, gRPC, Kafka, RabbitMQ and raw
sockets, because all of them speak first. NATS and SMTP do not: a NATS server
sends INFO … and an SMTP server sends 220 … before the client writes
anything. There is nothing to inspect, and Mockarty does not guess from
silence.
What those clients do send before the server has to make up its mind is the
TLS handshake itself, and the host name in it (SNI) is exactly the hostname you
configured. So the protocol travels there:
team-a.mock.example.com → HTTP / gRPC / Kafka / RabbitMQ / raw sockets (as before)
team-a.nats.mock.example.com → NATS
team-a.smtp.mock.example.com → SMTP (implicit TLS)
Nothing changes in your client or in the payloads. Your NATS client keeps
its subjects, headers and JetStream streams; your mailer keeps its envelope and
message. Only the host you dial carries one extra label. A host whose second
label is not nats or smtp keeps its plain meaning, so the rule only applies
where you use it.
nats and smtp are therefore reserved as the second label of a tenant
host when the unified TLS port is on. If your tenant domain itself begins with
one of those words, do not name a tenant host that way — the host would be read
as a protocol.
Which clients can use port 5771
| Protocol | Client shape | Port 5771 (shared) | Dedicated port |
|---|---|---|---|
| NATS | TLS handshake first (nats://… + nats.TLSHandshakeFirst(), nats --tlsfirst, tls://) |
yes | 4222 |
| NATS | classic — plaintext INFO first, TLS upgrade afterwards |
no — it waits for a plaintext greeting a TLS port never sends, so it never starts a TLS handshake | 4222 |
| SMTP | implicit TLS / SMTPS (connect with TLS, then read the greeting) | yes | — |
| SMTP | STARTTLS (plaintext greeting, then upgrade) | no — the same reason | 2525 |
NATS and SMTP clients that cannot handshake TLS first are served by their
dedicated ports, which stay exactly as they are (TLS + SNI multi-tenancy
included). On the shared port an already-encrypted SMTP session is not
offered STARTTLS — there is nothing to upgrade to.
Even openssl s_client -connect host:5771 -servername team-a.nats.mock.example.com
works with no protocol knowledge at all: it receives the NATS INFO banner.
Separate ports for server-speaks-first clients
Keep the dedicated listeners when you need STARTTLS mail or a classic NATS
client. Both are TLS + SNI multi-tenant, so the tenant host still selects the
namespace:
MOCKARTY_NATS_NATIVE_TLS=1 MOCKARTY_SMTP_NATIVE_TLS=1 \
MOCKARTY_NATIVE_TLS_DOMAIN=mock.example.com ./mockarty
- NATS —
<namespace>.mock.example.com:4222, TLS handshake first. - SMTP —
<namespace>.mock.example.com:2525, thenSTARTTLS; the SNI host of
that handshake selects the tenant.
Switching a service to Mockarty changes only the broker host. Your vhost,
queues, topics, consumer groups, routes, services and message shapes stay
exactly as they are in production configuration — the host alone selects the
namespace. The plain admin host (mock.example.com, no tenant label) keeps
serving the admin API and the explicit /stubs/<namespace>/… routes as before.
The switch expands to the individual settings, so an operator who needs a
different layout overrides just one of them (a value you set is never touched):
HTTPS_ENABLED, MOCKARTY_UNIFIED_TLS (the shared HTTPS port),
MOCKARTY_NATIVE_TLS (TLS+SNI on the dedicated listeners),
MOCKARTY_NATS_NATIVE_PORT (4222), MOCKARTY_SMTP_NATIVE_PORT (2525) — set a
port to an empty value to leave that listener off, or bind Kafka, AMQP and raw
sockets to dedicated ports with MOCKARTY_KAFKA_NATIVE_PORT,
MOCKARTY_AMQP_NATIVE_PORT, MOCKARTY_TCP_NATIVE_PORT (TLS+SNI as well).
Mockarty generates a wildcard certificate for *.<domain> — plus
*.nats.<domain> and *.smtp.<domain>, which the two-label tenant hosts above
need — automatically, or reuses the HTTPS certificate you configured. Dev
clients can skip verification or trust the generated certificate; production
deployments can supply their own wildcard certificate via the standard HTTPS
certificate settings.
Without the switch nothing changes: a native listener you bind by hand stays
plaintext and serves its single configured namespace
(MOCKARTY_*_NATIVE_NAMESPACE), and HTTP keeps the /stubs/<namespace> routes.
Cluster note. AMQP reply queues, Kafka offsets and consumer groups, and
NATS queue groups and JetStream streams live on the node that accepted the
connection, unless the shared scope below moves them into the database. When running several Mockarty nodes behind a TCP load balancer,
keep those producers and consumers on the same node
(source-IP sticky balancing, or a dedicated service per node) so consumers
read the replies their producers triggered. Source-IP stickiness is only
enough while both sides are the same client: as soon as the producer and the
consumer are different machines, point both at one node.
Core NATS subscriptions are a narrower exception: a mock reply published on
one node reaches direct subscribers on peers when cluster messaging is
connected, unless the reply subject is captured by a local JetStream stream.
A core NATS queue group is served across nodes too, and only once: the
publishing node names a single node to serve the group, and a group with a
member on the publishing node is always served there. Joining and leaving are
announced across the cluster as they happen, so a member that has just joined
can only miss a message published in the same instant, and a member that left
stops being chosen at once. JetStream consumers are not covered and still need
the same node unless the shared scope below is on. A failed
cluster publish closes the publisher connection instead of claiming local
success. With the PostgreSQL notification transport, its 7.5 KiB envelope
limit also applies to these replies; oversized replies close that connection.With
CLUSTER_MODE=truethe node says this itself: the status read
GET /api/v1/native-listenersreturns astateblock with the caveat, and
the connection panel in the mock constructor shows it next to the address.
DeclareMOCKARTY_NATIVE_STATE_SCOPE=per-nodeonce you have accepted per-node
state, so the answer stops being an open question rather than a permanent
warning.
MOCKARTY_NATIVE_STATE_SCOPE=shared— cross-node AMQP, Kafka and NATS
JetStream state (PostgreSQL only). When the node runs on PostgreSQL, the
shared scope starts every AMQP, Kafka and NATS broker the node serves — a
dedicated listener (MOCKARTY_AMQP_NATIVE_PORT,MOCKARTY_KAFKA_NATIVE_PORT,
MOCKARTY_NATS_NATIVE_PORT) or the unified port — with its state in the
database; a node that runs only some of these protocols is not asked to open
the others:
- AMQP replies are stored durably, so a publish on node 1 reaches a consumer
on node 2 (a disconnect before delivery re-parks the reply; expired replies
are swept by the cluster leader).- Kafka topic logs live in the database: the reply a mock writes to its
output topic when a record is produced on node 1 is read by a consumer on
node 2, offsets form one sequence however many nodes the producers use, a
stream mock’s records are stored once, and a topic deleted through one node
is gone on every node. Each topic keeps its latest 1000 records; records
older than 24 hours are removed by the cluster leader. If the database
cannot store a reply, the producer gets a storage error instead of a
success nobody can read.- Kafka consumer-group offsets and coordination leases live in the database,
so a group position committed on node 1 is honoured by the same group on
node 2 — at the same record; a commit from a node that lost its
coordination lease is fenced with ILLEGAL_GENERATION and the client rejoins.- NATS JetStream streams, their messages and consumer positions live in the
database: sequence numbers are cluster-wide (publishes on two nodes never
get the same number), a stream a client creates on node 1 is known on
node 2, and a pull or push consumer on node 2 receives what was published
on node 1. A durable consumer is delivered by one node at a time: while
node 1 servesWORKER, bindingWORKERon node 2 is refused with
“consumer … is delivered by another node of this cluster”. Node 1 lets go of
WORKERwhen it stops, when it disappears (after 30 seconds), or when its
client has not usedWORKERthere for 30 seconds — the case of a client
that reconnected through the load balancer to node 2. The client’s next
lookup, subscribe or fetch on node 2 then takesWORKERover, and it
continues after the last delivered message — nothing is replayed, nothing is
skipped. Consumer names and lists on every node include the consumers other
nodes serve.
Stored messages older than 24 hours are removed by the cluster leader.A node that cannot keep the state in a database refuses the scope with the
piece named (postgresql: falseon a SQLite/desktop build) — the listeners
for AMQP, Kafka and NATS do not start rather than promise something they
cannot do. The status
readGET /api/v1/native-listenerssays what went shared: the state block’s
sharedlist names every enabled broker (AMQP, Kafka, NATS). Core NATS
messages (plain publish/subscribe without JetStream) are not stored at all;
their cross-node delivery and queue groups (above) work whenever cluster
messaging is connected and do not depend on this scope. Socket, gRPC and SMTP keep no
broker state and are unaffected by any scope.
Kubernetes (Helm chart)
The chart turns all of this on with one block — admin.nativeListeners in
values.yaml:
admin:
nativeListeners:
enabled: true
domain: mock.example.com # tenant host = {namespace}.mock.example.com
unifiedTLS: true # HTTPS port 5771 = HTTPS + gRPC + Kafka + AMQP + socket + NATS + SMTP
nats: { enabled: true, port: 4222 }
smtp: { enabled: true, port: 2525 }
passthroughIngress: # outside the cluster (see below)
enabled: true
host: "*.mock.example.com"
Inside the cluster a client dials the admin Service with the tenant host as
its TLS server name — mockarty.<namespace-of-the-release>.svc:5771 for
Kafka/AMQP/socket, {namespace}.nats…:5771 and {namespace}.smtp…:5771 for
NATS/SMTP, or :4222 / :2525 for the dedicated listeners — and lands in the
namespace the SNI names. Outside the cluster the passthrough Ingress hands the
raw TLS stream (server name and ALPN intact) to that same port; it needs
ingress-nginx started with --enable-ssl-passthrough and wildcard DNS for
*.mock.example.com, *.nats.mock.example.com and *.smtp.mock.example.com
pointing at the ingress. The certificate is the one Mockarty generates
(self-signed wildcard); clients that must verify get a CA-issued wildcard
through the usual HTTPS_CERT_FILE / HTTPS_KEY_FILE.
Request logs
Every resolved native-listener hit — gRPC, WebSocket, TCP/UDP socket, Kafka,
RabbitMQ or SMTP — is recorded in the mock’s Request Logs (mock page →
Request Logs), with the full message and the mock’s reply. From there you can
inspect real traffic and create new mocks from logged requests, exactly like
with HTTP mocks.
Every native listener records unmatched traffic as an undefined request —
whether the client spoke gRPC, WebSocket, Kafka, RabbitMQ or SMTP — so the
“create a mock from what actually hit us” flow works the same across all
protocols, not just HTTP.
Proxy mode (SMTP, Kafka, RabbitMQ, WebSocket)
A native SMTP / Kafka / RabbitMQ / WebSocket mock can forward to a real
upstream instead of answering itself — the async analogue of an HTTP proxy mock.
Set the mock’s proxy.target to the upstream address:
- SMTP —
"proxy": {"target": "smtp://mail.internal:25", "recordTraffic": true}
relays the mail to the real MTA and returns its verdict to the sender. - Kafka —
"proxy": {"target": "kafka://broker:9092", "recordTraffic": true}
produces the message to the real broker. - RabbitMQ —
"proxy": {"target": "amqp://guest:guest@rabbit:5672/", "recordTraffic": true}
publishes the message to the real broker. - WebSocket —
"proxy": {"target": "ws://upstream:8080/socket", "recordTraffic": true}
hands the whole connection to a bidirectional pipe against the upstream — the
client and the real server talk through Mockarty, which records what passes.
The upstream dial is SSRF-guarded (loopback / private / metadata addresses are
refused unless the deployment explicitly allows private targets), so a proxy
mock can’t be turned into a request-forgery primitive. With
recordTraffic: true every forwarded message is captured as an undefined
request (origin proxy) — one click from becoming its own mock.
Set proxy on the mock via the REST API or the create_mock MCP tool.