HTTP¶
Full HTTP mock server. Matching on method + path (exact/wildcard/named params/regex), optional query params, header match (with re: / * pattern support), JSON/multipart/XML body field match, authentication, and state condition.
protocols:
http:
enabled: true
port: 8080
max_body_bytes: 10485760 # optional: limit request body size (bytes); 0 = unlimited (default)
mocks:
- id: create-user
request:
method: POST
path: /users
headers:
Authorization: "Bearer *"
response:
status: 201
body: '{"id":"{{uuid}}"}'
headers:
Content-Type: application/json
delay: 20ms
Query parameter matching¶
- id: admin-users
request:
method: GET
path: /users
query:
role: admin # exact match
page: "*" # any value (wildcard)
order_id: 're:^ORD-\d+$' # regex match ("re:" prefix)
debug: "!present" # asserts the param must be ABSENT
response:
status: 200
body: '[{"id":1,"role":"admin"}]'
When a query param is repeated (e.g. ?tag=a&tag=b), a match succeeds if
any occurrence satisfies the configured value/pattern.
JSON body field matching¶
Use dot-notation paths to match fields anywhere in a JSON body:
- id: gbp-payment
request:
method: POST
path: /payments
body_json:
currency: GBP # exact
"user.tier": premium # nested: {"user":{"tier":"premium"}}
"items.0.sku": "*" # any SKU (wildcard)
response:
status: 200
body: '{"ok":true}'
Form body field matching¶
For classic HTML <form method="post"> submissions (e.g. a hosted
payment/checkout page posting back card details), body_form matches
fields in an application/x-www-form-urlencoded body. Unlike
body_json/body_multipart/body_xml, values also support the "re:"
regex prefix — useful for matching a card number pattern rather than an
exact value:
- id: checkout-declined
request:
method: POST
path: /checkout/{id}
body_form:
card_number: "re:.*0000$" # any card number ending in 0000
response:
status: 200
body: {file: ./mocks/declined.html}
- id: checkout-approved
request:
method: POST
path: /checkout/{id}
response:
status: 200
body: '{{.request.body.card_number}}' # last-4 echoed back via templating, no extra config needed
A repeated field name (e.g. multiple same-named checkboxes) matches if
any submitted value satisfies the configured value/pattern, mirroring
query-param matching. The request Content-Type must declare
application/x-www-form-urlencoded (a charset parameter is fine) for
body_form to apply at all — this avoids spurious matches on non-form
bodies.
See the Nets/Nexi preset for a complete,
runnable worked example: a hosted checkout page (GET /checkout/{id})
with a real HTML form, posted back to body_form-matched
approved/declined responses — no HTML authoring required, just
mockly preset use nets.
Multipart and XML body field matching¶
For multipart/form-data requests (e.g. file uploads), body_multipart
matches individual form fields by name. Text fields are matched by value;
file parts are matched via <field>.filename/<field>.content_type:
- id: avatar-upload
request:
method: POST
path: /profile/avatar
body_multipart:
"name": "Alice" # text field, exact match
"avatar.filename": "*" # asserts a file was uploaded
"avatar.content_type": "image/png"
response:
status: 200
body: '{"ok":true}'
For XML bodies (SOAP, legacy enterprise APIs), body_xml matches
element text and attributes using the same dot-notation style as
body_json; a leading @ segment matches an attribute instead of
descending into a child element:
- id: soap-login
request:
method: POST
path: /soap
body_xml:
"user.role": admin # <user><role>admin</role></user>
"user.@id": "*" # <user id="...">
response:
status: 200
body: '<ok/>'
Response sequences¶
Return a different response on each successive call. Useful for simulating transient errors or pagination.
- id: flaky-service
request:
method: GET
path: /data
sequence:
- status: 503
body: '{"error":"unavailable"}'
- status: 503
body: '{"error":"unavailable"}'
- status: 200
body: '{"data":"ok"}'
sequence_exhausted: hold_last # hold_last (default) | loop | not_found
response:
status: 200
body: '{"data":"ok"}'
sequence_exhausted |
Behaviour after all entries are consumed |
|---|---|
hold_last |
Keep returning the last entry (default) |
loop |
Restart from the first entry |
not_found |
Return 404 |
Streaming responses (SSE / chunked)¶
Set response.stream.events to turn a mock into a sequence of flushed
events instead of one static body — for Server-Sent Events
(text/event-stream) or plain chunked/flushed streaming. Each event's
data is rendered as a template (same {{ }} syntax as body), and its
optional delay is slept before that event is written and flushed.
- id: chat-stream
request:
method: POST
path: /v1/chat/completions
response:
status: 200
headers:
Content-Type: text/event-stream
stream:
events:
- data: '{"chunk":"Hello"}'
delay: 100ms
- data: '{"chunk":" world"}'
delay: 100ms
- data: "[DONE]"
When Content-Type contains text/event-stream, each event is framed as
an SSE message: optional event:/id: lines, one data: line per line
of data (multi-line payloads are supported per the SSE spec), then a
blank line. Without that content type, data is written and flushed
as-is — no SSE framing — for plain chunked streaming.
stream is not combined with sequence, scenario patches, or fault
injection: if any of those fire for a given call, that call falls back to
a normal static response instead of streaming.
By default the response ends once all configured events have been sent.
Set stream.keep_open: true to leave the connection open indefinitely
afterwards instead, so it can receive further ad-hoc events pushed through
the stream management API — see that
section for examples and the cross-protocol behavior this also applies to
for gRPC streaming and GraphQL subscriptions.
- id: live-ticker
request:
method: GET
path: /v1/ticker
response:
status: 200
headers:
Content-Type: text/event-stream
stream:
keep_open: true
events:
- data: '{"price":100}'
Per-mock fault injection¶
Every HTTP mock can have its own fault: block — independently of protocol-level faults. This is useful for targeted latency tests or intermittent failures on one endpoint without affecting the rest of the protocol server.
- id: slow-search
request:
method: GET
path: /search
fault:
delay: 2s # add latency
error_rate: 0.5 # only apply 50% of the time (0 = always)
response:
status: 200
body: '[]'
Instead of (or in addition to) a fixed delay, use delay_range for a
uniform-random jittered delay per request — more realistic than one constant
value for testing timeout/retry/loading-state handling:
Use rate_limit to simulate a throttled API: once more than
requests_per_second requests are observed in the trailing one-second
window, over_limit_status (default 429) is returned instead of the normal
response. Each mock with a rate_limit fault tracks its own independent
window.
fault:
rate_limit:
requests_per_second: 5
over_limit_status: 429 # default 429
body: '{"error":"rate limit exceeded"}'
Near-miss diagnostics for unmatched requests¶
By default, a request that matches no mock returns a plain
{"error":"no mock matched"} (404) — this never changes for normal
client traffic. To debug why a request didn't hit the mock you
expected, opt in with ?debug=true or an X-Mockly-Debug: true header:
{
"error": "no mock matched",
"near_misses": [
{ "mock_id": "get-order", "reason": "query \"order_id\" value(s) [bad-id] did not match \"re:^ORD-\\d+$\"" }
]
}
Up to 3 of the closest candidates are reported (mocks that passed the
most checks — method, path, headers, query, body, body_json,
body_form, body_multipart, body_xml, state, auth — before failing), ranked
closest-first. Near-miss info is also
included in the mock's request log entry whenever diagnostics were
requested, so it shows up in the management UI's live log stream too.
Authentication matching¶
Use the auth: block to require valid credentials before a mock is considered a match. When auth fails the mock is skipped — add a fallback mock (without auth) to return your preferred 401 response.
Header values everywhere also support re: regex patterns and * wildcard, making it easy to match any bearer token without an auth: block if you prefer.
Bearer token
- id: secure-endpoint
request:
method: GET
path: /api/data
auth:
type: bearer
token: "my-secret-token" # exact | "re:^ey[A-Za-z0-9]" regex | "*" (any token)
response:
status: 200
body: '{"data":"sensitive"}'
- id: secure-endpoint-unauth # fallback for missing/wrong token
request:
method: GET
path: /api/data
response:
status: 401
headers:
WWW-Authenticate: 'Bearer realm="api"'
body: '{"error":"unauthorized"}'
Basic auth
- id: admin-page
request:
method: GET
path: /admin
auth:
type: basic
username: admin
password: s3cret # Mockly decodes the Base64 header internally
response:
status: 200
API key (header or query parameter)
- id: weather
request:
method: GET
path: /weather
auth:
type: api_key
header: X-API-Key # or: query: api_key
value: "key-abc123" # exact | "re:…" | "*"
response:
status: 200
NTLM (Windows-integrated auth)
Mockly drives the full 3-step NTLM challenge/response handshake automatically. Any well-formed token sequence is accepted — no credential validation.
- id: windows-endpoint
request:
method: GET
path: /api/windows
auth:
type: ntlm
response:
status: 200
body: '{"authenticated":true}'
The handshake sequence:
1. No Authorization header → 401 Unauthorized + WWW-Authenticate: NTLM
2. Authorization: NTLM <type-1 Negotiate> → 401 Unauthorized + WWW-Authenticate: NTLM <type-2 Challenge>
3. Authorization: NTLM <type-3 Authenticate> → mock's configured response
See configs/presets/ntlm.yaml for a complete working example.
Digest
Requires an Authorization: Digest … header to be present (no nonce validation):
HTML page mocking (body: {file: ...} / {dir: ...})¶
A response body can be a plain string (as above), or a structured
reference to a file or a whole folder on disk — handy for serving real
HTML/CSS/JS/images without hand-escaping markup into a YAML string.
File content is read fresh on every request (no caching), so editing the
file on disk takes effect immediately, no restart required. Templates
({{ }}) still render normally inside a file's content, except for
binary content-types (images, fonts, pdf, wasm, audio/video), which are
served as-is.
- id: checkout-page
request:
method: GET
path: /checkout
response:
status: 200
body:
file: ./pages/checkout.html # resolved relative to this config file's directory
The Content-Type header is inferred from the file's extension unless
you set an explicit headers: {Content-Type: ...} (which always wins).
Serving a whole folder (dir)
body: {dir: ...} expands, at config-load time, into one generated mock
per file found recursively underneath that folder (dotfiles/dot-folders
are skipped). Each file's path relative to the folder is joined with the
parent mock's own request.path using URL (not OS) path semantics:
Given ./site/docs/index.html and ./site/docs/guide/intro.html, this
registers GET /docs/index.html, GET /docs/guide/intro.html, and
(the standard "directory index" convention) GET /docs itself — any
index.html found at any depth is additionally registered at its parent
directory's own path. A body.dir mock must have a static, literal
request.path (no path_regex, wildcards, {name} captures, or re:
prefixes) and can't be combined with sequence. Loading fails fast if
two mocks (hand-written or generated) end up with the same method+path.
Scaffolding a page without editing YAML (mockly add page)
# Serve a single HTML file:
mockly add page --path /checkout --file ./pages/checkout.html
# Expand a whole folder of static assets into one mock per file:
mockly add page --path /docs --dir ./site/docs
Both forms resolve paths relative to the CLI's own working directory, then
read each file's content immediately and send it as a literal response
body — the management API never resolves a file/folder path supplied by a
caller, by design (it's a network-reachable control plane, so letting it
touch the server's filesystem on request would be a path-traversal risk).
Binary assets (images, fonts, PDFs, …) transfer byte-for-byte intact, not
just text. This means mocks added via mockly add page are point-in-time
snapshots: unlike body.file/body.dir configured directly in
mockly.yaml (which always re-read the file fresh from disk on every
request), editing the source file after running add page has no effect
until you re-run the same command. Re-running it (e.g. after editing the
folder's contents) replaces the previously-added mock(s) instead of
duplicating them, so refreshing is just re-running the same command.
See also¶
- Configuration — matching, templated responses, state conditions, and scenarios shared by every protocol
- Fault Injection — this protocol's fault fields:
status,body,delay,delay_range,error_rate,rate_limit - Management API — CRUD for HTTP mocks