Mocking and Fault Injection
Watching traffic answers what an app does. This page is about the other half: making the network answer something it would not have answered, so you can see what the app does about it. A backend that is not built yet, an endpoint that returns 500, a response that takes eight seconds, an API that has simply gone away.
Everything here works on any platform Chute runs on, and everything that touches HTTPS needs decryption enabled for that host first — an encrypted request Chute cannot read is one it cannot answer for.
What Chute can and cannot simulate
Chute intervenes at the connection and the HTTP message. It has no traffic shaper, so:
| Can | Return a canned body, return a chosen status code, add a fixed delay before a request or a response, refuse a connection outright, force HTTP/3 clients back to TCP, send a request to a different host than the one it asked for |
| Cannot | Limit bandwidth, drop or reorder packets, add jitter, degrade a connection partway through, or simulate a specific RTT at the transport level |
There is no [Throttle] section and no speed limit anywhere in the configuration. If you need a slow link rather than a slow response, that belongs to a network conditioner (Apple's Network Link Conditioner, or a router), not to Chute.
Pick a mechanism
| To simulate | Use | Where |
|---|---|---|
| A response body that does not exist yet | [Map Local] |
Mock Response |
| Exactly 503 | [URL Rewrite] … reject |
URL Rewrite |
| An empty 200, a blank image, an empty JSON object | reject-200, reject-img, reject-dict |
URL Rewrite |
| Any other status — 401, 429, 500 | An http-request script |
JS Scripting |
| Latency | An http-request or http-response script |
JS Scripting |
| An endpoint that is simply unreachable | A REJECT rule |
Built-in Policy |
| A client that will not fall back to TCP | block-quic |
Misc Options |
| A different backend behind the same URL | [Host], or [URL Rewrite] header mode |
Local DNS Mapping |
A canned response body
[Map Local] answers a matching request from a file or from inline base64, without asking the real server:
[Map Local]
^https://api\.example\.com/v1/profile.* data="/Users/me/mocks/profile.json"
^https://api\.example\.com/v1/flags.* base64="eyJiZXRhIjogdHJ1ZX0="
Three things decide whether this works:
- The regex must match the whole URL, not a part of it. End the pattern with
.*unless you want to match a URL with no query string at all. data=is read by the device running Chute. On a Mac that is convenient — edit the file, and the next request sees the change. On a phone or an Apple TV a path from your Mac means nothing; usebase64=there, or serve the file over HTTP and use URL Rewrite.- The status is always
200 OK.[Map Local]has no way to set one, and the connection is closed after the answer. For any other status, use a script — see below.
The body supports the {{ "{{url}}" }}, {{ "{{host}}" }}, {{ "{{path}}" }}, {{ "{{method}}" }} and {{ "{{ua}}" }} template variables, which is enough to make a mock that echoes what it was asked.
An error status
For 503, no script is needed — a reject-mode URL rewrite returns HTTP/1.1 503:
[URL Rewrite]
^https://api\.example\.com/v1/orders.* _ reject
The sibling modes cover the other shapes of "nothing useful": reject-200 (200 with an empty body), reject-img (a 1×1 GIF), reject-dict ({} as JSON, 200). All of them apply to HTTPS only when that host is being decrypted.
For any other status code, an http-request script short-circuits the request:
[Script]
Fail429 = type=http-request, script-path=/Users/me/mocks/fail429.js, pattern=^https://api\.example\.com/v1/orders
// fail429.js — answer without contacting the server
$done({
response: {
status: 429,
headers: {
"Content-Type": "application/json",
"Retry-After": "30"
},
body: JSON.stringify({ error: "rate_limited" })
}
})
A script's pattern is matched anywhere in the URL, unlike the rewrite families — a prefix such as ^https://api\.example\.com/v1/orders is enough, and no trailing .* is needed.
In the HTTP/1.1 path the status line is written with the reason phrase
OKwhatever the code is (HTTP/1.1 429 OK). Clients read the number, not the phrase, so this is cosmetic — but it is what you will see in a raw capture.
Latency
A script blocks the message until it calls $done(), so a timer is a delay:
[Script]
SlowAPI = type=http-response, script-path=/Users/me/mocks/slow.js, pattern=^https://api\.example\.com/v1/, timeout=15
// slow.js — hand the real response back 8 seconds late
setTimeout(function () {
$done({})
}, 8)
The budget is the script's own timeout: 5 seconds by default, and anything above 30 is clamped to 30. A script that has not called $done() when the timeout expires is treated as passthrough — the message goes on unmodified — so a delay longer than the timeout does not fail loudly, it just stops delaying. Set timeout above the delay you want, as in the example.
Use type=http-request to delay before the server is contacted (the app sees a slow round trip), and type=http-response to delay after (the server was fast, the app still waits).
An endpoint that is simply gone
A mock replaces a response; a REJECT rule denies the connection. It works at the connection level, so it covers any protocol rather than HTTP alone, and it does not need decryption:
[Rule]
DOMAIN-SUFFIX,api.example.com,REJECT
REJECT-DROP, REJECT-TINYGIF and REJECT-NO-DROP are accepted for compatibility and all behave as plain REJECT. For an HTTP request, show-error-page-for-reject = true replaces the bare refusal with a readable error page, which makes it obvious in a browser that the block was yours.
This is also how you check that a fallback path exists at all — reject the primary host and see whether the app reaches for the secondary one or just spins.
Forcing a client off HTTP/3
QUIC runs over UDP and Chute cannot decrypt it, so an app on HTTP/3 is invisible to every mechanism on this page. Rejecting its QUIC flows makes compatible clients retry over TCP, where all of it works:
[General]
block-quic = on
auto rejects QUIC only when the flow is headed for a proxy; on rejects it everywhere, including DIRECT. For traffic arriving through TUN, Chute answers a rejected QUIC flow with ICMP Port Unreachable so the client falls back immediately instead of waiting out a timeout.
Sending the request somewhere else
Two ways, at two layers:
[Host]
api.example.com = 10.0.0.5
A [Host] mapping answers the DNS lookup with an address of your choosing — a staging box, or an address that goes nowhere if what you want is a connection that times out rather than one that is refused. It applies to every protocol, and needs no decryption. Purge the DNS cache after changing it.
[URL Rewrite]
^https://api\.example\.com/v1/(.*) https://staging.example.com/v1/$1 header
Header mode rewrites the request in place and fixes up the Host header, so the client never learns it was redirected. This one is HTTP-level and does need decryption for HTTPS. Where the destination cannot be rewritten in place, Chute falls back to answering with a 307 to the new URL.
Confirming that it fired
A rule that never matches looks exactly like a rule that matched and did nothing — the failure mode of this whole page.
- Rewrites and mocks: the Web Console's Rules page lists every URL Rewrite, Header Rewrite, Body Rewrite and Map Local rule that has fired this run, with a count. Not in the list means it has never matched. The same data is
rewrite_hitsonGET /api/rules. - Per connection: open the connection in the console or Dashboard and read its Rewrites applied rows, which name the rule in its own words.
- Scripts do not appear in that table. A script's evidence is its own output:
console.loglines land in the log, readable on the console's Logs page or withGET /api/logs.
Cleaning up
Rules added from the console, from Dashboard or over POST /api/rewrites/:family live in the running kernel and are gone at the next restart — which makes them ideal for an experiment and a bad place to keep something you rely on. Rules in the configuration file survive restarts, which makes them a good place to keep a mock and a very bad thing to forget about: a [Map Local] line left in a configuration will still be answering requests weeks later, and it looks exactly like a broken server.