Every name in bold below is written exactly as it appears in the extension, so you can also find it with Ctrl+F here or search for it in the interface.
All of these are a rule. Pick the rule type from the Rule type dropdown when you create it.
| I want to… | Use this |
|---|---|
| Make an endpoint return a 500, a 404, a 401 — an error I can't easily trigger for real | Rule type Mock the response (API), then set Status code. See Your first rule. |
| Return test data for an endpoint that doesn't exist yet | Rule type Mock the response (API), write the JSON in Response body. See Your first rule. |
| Start a mock from what the server really returned, instead of typing a fixture | The Mock button in the DevTools panel — see Building rules from real traffic. |
| Change one field of a real API response and leave the rest alone |
Rule type Modify the real response body, Mode =
Overwrite a JSON field, e.g. Field path user.status.
|
| Swap a piece of text inside a real response | Rule type Modify the real response body, Mode = Find and replace. |
| Add, override or remove a header | Rule type Modify headers — see the syntax and the Presets below. |
| Send an Authorization header the app doesn't send |
Rule type Modify headers, preset Auth headers. It writes
Authorization: Bearer {{token}}, so define a token
variable to go with it.
|
| Stop the browser serving from cache so my rule gets a chance to run | One-off: ↻ Reload without cache in the popup. For every reload: rule type Modify headers, preset Cache bypass. |
| Get past a CORS complaint while developing | Rule type Modify headers, preset Development CORS — but read Things that look like bugs but aren't first: it does not lift a real browser block. |
| Point a call at localhost or at another environment | Rule type Redirect the request, fill in Redirect to. |
Replace staging with local in every URL, keeping the rest |
Rule type Replace text in the URL (Find / Replace with). |
| Force a query parameter the interface doesn't expose (a debug flag, a page size) | Rule type Query parameter. |
| Make a call fail, to see what my error handling does | Rule type Block the request. |
| Test slow network, spinners and loading states | Set Latency (ms) in the rule's Network simulation block. It works on any rule type — including a rule that does nothing else. |
| See what my UI does if this endpoint returns a 500, but with the real payload and without writing a mock | Set Override the real status in the rule's Network simulation block. The request still goes out, the real body still comes back, and only the status code changes. See Overriding the status of a real response. |
| Send the backend a payload the interface would never send | Rule type Modify the request body. |
| Mock an endpoint and make it slow — or redirect a call and add a header to it | Two rules matching the same URL, one for each half: they all apply. See When several rules match the same request. |
| Affect a single GraphQL operation, not the whole endpoint |
Any rule type, plus a Body condition such as "operationName":"GetUser". The
DevTools panel fills this in for you.
|
| Limit a rule to one tab so it doesn't affect everything I have open | The 📌 button next to the rule in the popup — see extra conditions. |
| I want to… | Use this |
|---|---|
| Work out why my rule isn't doing anything | Why a rule might not be firing — the full checklist, in order. |
| Check my pattern matches a URL before saving the rule | Test URL (optional), right under the pattern in the rule form. See matching. |
| Know which of my rules wins when two of them match the same URL | When several rules match the same request — short answer: they all apply, and where they clash the one lower in the list wins. |
| See which requests a rule actually caught | Record this tab in the popup, then 📡 Captures in the options page. See Seeing what actually happened. |
| Switch a host between Local, Staging and Prod without editing every rule | Variables plus Environments. |
| Turn several rules on or off together | Put them in a Group (the Groups panel at the top of the options page) and toggle the group. Or tick several rules in the list and use the bar that appears. |
| Find one rule among many | The search box and the type filter next to it. The box says "name, URL or group", but it actually also looks inside the Description and the Tags. |
| Share my setup with a teammate, or move it to another machine | Export / Import in the options page. Read what "sanitize on export" covers before sending the file. |
| Hand someone a trace of what the page did | Export HAR in the DevTools panel — see Building rules from real traffic. |
| Copy a request into a terminal or the console | Copy as… in the DevTools panel (cURL or fetch()). |
| Reuse my rules in my Playwright or Cypress tests | Copy as… on the rule's own row (just that one), or Export → the mock buttons in the dialog (all of them, or the selected ones). Read the comments in the snippet before you commit it: they say what was redacted and what could not be translated. |
| Call the endpoint a rule intercepts, to compare against the real thing | The same Copy as… on the row, options cURL and fetch(). |
| Make one rule win over another that also matches | Move it with ↑ and ↓: the one lower in the list wins. See When several rules match the same request. |
| Keep the extension off everything but my own sites | Scope in the options page: everywhere, only localhost, or only the domains you list. |
| Turn everything off for a moment without deleting anything | The master toggle: On in the popup, Extension enabled in the options page. |
Press + New rule (in the popup or in the options page). The form opens on Mock the response (API), which is the most common case: answer a request from the browser instead of letting it reach the server. What the form shows up front is short — the rest is folded into four collapsible blocks, described below — and most of it is optional. Here is what a working JSON mock actually needs:
/api/users is enough to catch every URL with that
fragment. See Choosing which requests a rule matches.
200. The field is a plain number box with a list of
suggestions attached (200, 201, 204, 301, 400, 401, 403, 404, 429, 500, 502, 503…): click it to pick one,
or just type any code you want.
Content-Type: application/json already
written in the box, as a real value you can edit or delete: what is in the box is what gets saved. Delete
it and the mock comes back as plain text — a legitimate choice, and nothing writes the header back when
you save. Opening a rule that already exists never adds it, even when its headers are empty.
Everything else narrows the rule down or helps you find it later, and lives in four collapsible blocks: Organization (description, tags, group), Advanced matching (source, match type, method), Additional conditions (optional) (header, body and initiator conditions) and Network simulation (latency and status code, which apply to every rule type). A new rule opens with the four of them closed; editing an existing one opens each block that rule actually uses, so a folded block never hides something you set. Two things worth doing before you close the form:
Every rule has exactly one type, which decides what happens once it matches a request. The name in bold is the one in the Rule type dropdown, where the nine of them are grouped by what they do to the request — Mock, Modified passthrough, Redirect and Block, the split described in Mock vs. modify & passthrough. The shorter name in brackets is how the same type is labelled in the rule list, the type filter and the captures list. One rule does one of these things — but a request can be governed by several rules at once, so combining two types is a matter of writing two rules. See When several rules match the same request.
| Type | What it does | When to use it |
|---|---|---|
| Mock the response (API) [Response] |
The request never reaches the network. You fabricate the status, headers and body yourself. | The backend endpoint doesn't exist yet, or you need a specific error/edge case (500, empty list, malformed data) that's hard to trigger for real. |
| Modify the request body [Request body] |
The request still goes out for real, but with a different body than the page sent. | Testing how the backend reacts to a payload the UI wouldn't normally send. |
| Modify headers [Headers] |
Add, override or remove request and/or response headers. The request still goes out for real. One per
line: Key: Value overwrites or adds, +Key: Value appends to what is already
there, -Key removes. The Presets dropdown fills the boxes in for you with
three common sets — Cache bypass, Development CORS and
Auth headers — adding to whatever you already wrote and only replacing lines for the
same header.
|
Forcing a feature flag header, testing a missing/extra CORS header, faking an auth header locally. |
| Redirect the request [Redirect] |
Sends the request to a different URL entirely. | Pointing a call at a local server or a different environment without touching the app's config. |
| Block the request [Cancel] |
The request fails outright — fetch rejects, XHR fires an error event. |
Testing what your UI does when a call fails or a script fails to load. |
| Query parameter [Query param] |
Add or remove query string parameters. The request still goes out for real. One per line:
+key=value adds or overwrites, -key removes. |
Forcing a query param the UI doesn't expose (a debug flag, a page size). |
| Replace text in the URL [Replace] |
Replaces every occurrence of a piece of text in the URL, keeping the rest intact. The request
still goes out for real. The text you type in Find is matched literally, character for
character — it is not a regular expression, so a . only ever matches a
real dot.
|
Swapping staging for production in a URL without a full redirect rule. |
| No change (delay only) [No change] |
Does nothing to the request by itself: it goes out exactly as the page built it. It exists so that a rule can carry only a modifier — today, a latency — without also having to change something. | Slowing an endpoint down without altering it in any other way. |
| Modify the real response body [Response body] |
The request goes out completely unchanged; only the body that comes back is transformed before your
code sees it. Two modes: Find and replace (literal text, every occurrence) or
Overwrite a JSON field (a dot path such as user.status, leaving the rest
of the JSON alone). It has limits of its own — see
Things that look like bugs but aren't.
|
Flipping one field in a real API response (e.g. a status) without hand-writing a full mock. |
Delaying a request used to be a type of its own, and it no longer is. Latency (ms) lives in
the Network simulation block of the rule form and applies to every type: a mock
arrives late, a redirect leaves late, a blocked request fails late. Leave the field empty and the rule adds
no latency at all; 0 is not the same thing as empty — it is a real, if tiny, wait.
If all you want is to slow an endpoint down, use the No change (delay only) type and give it a latency. Rules that used to be of the old Delay type were converted to exactly that, keeping their milliseconds, the first time you ran this version.
There are two fields in the form that hold an HTTP status code, and they are not the same thing. It is the easiest confusion to fall into here, so it goes first:
| Field | What it is | Where it lives |
|---|---|---|
| Status code | The status of a response the extension builds. The request never goes out, so there is no real response anywhere. | Only on the Mock the response (API) type, next to the mock's body and headers. |
| Override the real status | Replaces the status of the response that did come back from the server. The real body, the real headers and everything else arrive untouched. | In the Network simulation block, on any rule type. |
The case it exists for: you want to see what your error handling does with a 500 from that
endpoint, but with the real data. With a mock you would have to hand-write the whole body — and then you
would no longer be testing against the real response, which is exactly what you wanted to keep.
200 to 599 is what's accepted. Outside that range the browser cannot build the
response at all, so an out-of-range value is ignored rather than breaking the request.
500 still carrying the original response's
OK contradicts itself the moment you log it; and an empty status text is what the browser
already hands you over HTTP/2 and HTTP/3, which have no reason phrase.
204, 205 or 304 the body is dropped, because
those statuses cannot carry one. That is exactly what the browser does with a real response of that kind.
And like any modifier, it combines: a single rule can override the status and transform the response body, and change a header on it, and arrive late.
This is the single most common point of confusion, so it's worth spelling out on its own:
Response
(or fakes the XHR properties) and hands it straight back. The real server never even sees the request.
A practical way to tell them apart while debugging: open DevTools' Network tab. A mocked request never shows up there at all (it never reached the network); a modified-and-passed-through one does, with the rewritten URL/body/headers. The captures list says the same thing in words, per request: "reached the network" or "never reached the network".
This is a property of the request, not of each rule taken alone: as soon as one matching rule mocks it, the request never leaves the browser, whatever the other matching rules do. A mock with a latency is a delayed answer that still never reaches the server — see When several rules match the same request.
Whatever its type, a rule always starts from one pattern: the Source dropdown decides whether that pattern is compared against the full URL or only the Host, and the Match type dropdown decides how. Both live inside the form's Advanced matching block, which is folded away until you open it:
| Match type | How the pattern is read |
|---|---|
| Contains (the default) | Matches if the pattern appears anywhere in the URL/host. /api/users catches every URL with that fragment in it. |
| Equals | Matches only if the URL/host is exactly the pattern, character for character. |
| Regex | The pattern is a JavaScript regular expression, tested against the URL/host. It doesn't have to match the whole thing unless you anchor it with ^/$. |
| Wildcard (* ?) |
A middle ground between "Contains" and "Regex": * stands for any run of characters
(including none) and ? for exactly one character. Everything else is literal, so you don't
have to escape anything — a . is a real dot, not "any character". Like Regex it isn't
anchored either: the pattern only has to occur somewhere in the URL/host.
|
https://*.example.com/api/v?/users in Wildcard mode matches
https://api.example.com/api/v2/users and https://staging.example.com/api/v3/users,
but not https://example.com/api/v10/users — ? is exactly one character.
Rather than guessing, paste a real URL into Test URL (optional), the box right under the pattern. It answers ✓ Matches or ✗ Doesn't match as you type, tells you what it compared against (the whole URL or just the host), and warns you if a Regex pattern doesn't compile. Nothing is saved and no request is made. It only checks the pattern, the match type, the source and the exclusions — not the method or the header/body/initiator conditions below.
/api/ except the health checkA pattern that is broad enough to be useful is usually broad enough to catch something you didn't want: the health check your dashboard polls every five seconds, the login call, the one request that already carries a real token. That's what Except URLs containing (one per line) is for — it sits just under the three dropdowns in Advanced matching. Write one pattern per line; if any of them appears in the request's URL, the rule leaves that request alone.
/api/ with /api/health and /api/auth/ as exclusions: every
API call is mocked except the health check and anything under auth.
Two things about this box are deliberate, and worth knowing before you fight it:
/api/health never appears in one. It also
means you can exclude a whole subdomain here (static.example.com) as easily as a path.
. is a dot and a * is an asterisk. Case matters, as everywhere else.
If you genuinely need a regular expression to describe what to leave out, the old escape hatch still
works — a negative lookahead in the pattern itself, ^(?!.*health).*api.* in Regex mode.
The Test URL box knows about exclusions: an excluded URL answers ✗ Excluded by “…” and names the line responsible, instead of claiming a match that would never happen.
On top of the pattern, a rule can carry extra conditions, under Additional conditions (optional) in the form — except Method, which sits with the other two dropdowns under Advanced matching. They are all optional, and all of them have to hold at once (AND, never OR):
Nothing stops two rules from matching the same request, and when that happens all of them apply — not just the first one. That is what lets you redirect a call and add a header to it, without trying to cram both into a single rule.
They are applied in list order, from top to bottom, and each rule only has a say in the part of the request its own type is about: a headers rule contributes header changes, a mock contributes the answer, and any of them can contribute a latency on top. When two rules speak about the same thing, the one lower in the list wins. So the order of the rule list is the priority — use the ↑ and ↓ buttons on a rule to move it above or below another one, and what your request ends up doing really does change.
| Two matching rules both… | What the request ends up doing |
|---|---|
| …mock the response | The lower one answers: its status, its headers, its body. The upper one is replaced outright, not blended into it. |
| …rewrite the URL (redirect, query parameter, replace text) |
Both, chained: each one rewrites the URL the previous one produced. "Redirect to localhost" plus "add
?debug=1" gives you a localhost URL with the parameter on it, not one or the other.
|
| …modify headers | Both sets of lines, run one after the other in list order — so for the same header name the later line wins, exactly as two lines inside a single rule would. |
| …modify the real response body | Both, in order: the lower rule transforms whatever the upper one produced. |
| …set a latency, or modify the request body | The lower one's value. A latency of 0 ms and an empty body are real answers, not "no opinion". |
/api/users with a 500 and carries a 2000 ms latency gives you a
500 that takes two seconds to arrive. Two separate rules — one mocking, one only adding the latency — give
you exactly the same thing, which is handy when you want to switch the slowness off on its own.
Two combinations are settled by a rule of their own rather than by list order:
To see which rules governed a given request rather than reasoning it out, record the tab and open the captures list: a request that matched three rules gets a ⚡ line for each of them, and the toolbar badge counts rules applied, not requests.
Define a name/value pair once (the "{{ }} Variables" button in the options page) and reuse it as
{{name}} anywhere you'd otherwise retype the same string: a rule's body, a header's value, a
redirect URL, a query param's value, the replacement text of a "Replace text in the URL" rule, and both the
replacement text and the new JSON field value of a "Modify the real response body" rule.
apiHost = api.staging.example.com, then use https://{{apiHost}}/v2/users
as a redirect target. Change the environment once, in one place, instead of hunting down every rule that
hardcoded the old host.
A name that doesn't match any defined variable is left exactly as written — {{typo}} shows up
literally in the request or response, rather than silently disappearing, so a mistake is obvious instead
of a mystery. Variables are not substituted into a rule's matching fields (the URL
pattern, or a header/body/initiator condition) — those compare against the real, live request, so a
variable there wouldn't mean anything.
An environment (the "🌐 Environments" button, next to Variables) is a named set of overrides on top of your variables — Local, Staging, Prod, whatever you need. It only has to override what actually changes between environments; anything it doesn't mention keeps the variable's own base value.
apiHost = local.test as the base value, a "Prod" environment can override just
apiHost = api.example.com and leave every other variable alone.
Pick the active environment from the star button next to it in the options page, or from the Environment dropdown in the popup (only shown once at least one environment exists). With no active environment, every variable simply uses its own base value — exactly like before environments existed.
First, a quick check that saves a lot of time: the extension's toolbar icon carries a badge with the number of rules applied on the current tab. If it's empty after reloading, nothing matched at all and the list below is where to look. If it's counting, the rule is firing and the surprise is in what it does, not in whether it runs.
Check these in order — they cover every reason a rule can silently fail to apply:
* or ? in a pattern still set to "Contains" or "Equals", where
they are just literal characters — those only mean anything in Wildcard mode.
send(body) — this only works for the "mock", "block",
"no change", "modify request body" and "modify headers" rule types on XHR (it works for every rule type on
fetch). A body condition also only ever matches plain text (JSON, form-encoded, plain
strings) — FormData/Blob/binary bodies are never read for it.
fetch or XMLHttpRequest. Net Tinker can only intercept
those two — it cannot affect <img>, <script src>, CSS, fonts, or
any other resource type when it comes to applying a rule (the DevTools capture panel can
still see all of those, it just can't act on them).
Guessing whether a rule ran is the slow way. There are three places that tell you, each answering a different question:
The captures list is off until you ask for it. Open the popup on the tab you want to watch and switch on Record this tab — only that tab's requests are captured. Then open the list with 📡 Captures in the options page (or View captures in the popup), which shows the Captured requests panel.
Each row tells you, in this order: the page, the status, the time, and then what Net Tinker did with it.
Unlike the DevTools panel, these captures are stored, so they're still there after you close the tab or the browser. The list keeps the 200 most recent and Clear empties it. Recording stops on its own if you close the tab being recorded.
Net Tinker works by patching fetch/XMLHttpRequest inside the page's own JavaScript.
That approach has a few unavoidable side effects:
interceptor.js for every request while
any rule is active on the page, because every request passes through Net Tinker's patched
fetch/XMLHttpRequest before reaching (or not reaching) the network. It doesn't
affect behavior, only attribution. If it bothers you, add interceptor.js to DevTools' own
Ignore List (Settings → Ignore List) — DevTools then skips those frames when computing the initiator.
responseType is
binary. The body can only be transformed when responseType is "",
"text" or "json"; with "blob", "arraybuffer" or
"document" there is no way to read it as text, so that particular request comes back
untouched. The rule itself is fine — it just can't act there.
user.status when user is a string). The body is
returned exactly as it arrived, rather than being half-rewritten or replaced by a guess. Use the
"Find and replace" mode for bodies that aren't JSON.
text/event-stream (server-sent events), multipart/x-mixed-replace — never
finishes. Those responses are handed back untouched, with the stream intact, and the rule simply doesn't
apply to that request; the captures list records it as "matched but not applied
— streaming response" so it isn't a silent no-op. The criterion is the media type, not the size: an
ordinary large response does finish, so it is transformed normally — it just arrives in one piece
instead of progressively. Rules of every other type leave the response body untouched and have neither
effect.
Open DevTools on any page and look for the "Net Tinker" panel (next to Elements, Console, Network, etc.). It lists every request the page makes while the panel is open — not just fetch/XHR, every resource type — so you can find the exact call you want to work with. The header shows Recording, and clicking it switches to Paused if you'd rather freeze the list; tick Preserve log to keep entries across navigations, and Clear empties it.
cURL
command or a fetch() snippet, for pasting into a terminal or the console.
.har
file, for handing to someone else or loading into another tool. The dialog lists the sensitive values it
found and offers Redact credentials (always on when the dialog opens) and
Include response bodies (always off, capped at 1 MB each, because bodies are what makes
the file heavy). Both boxes are reset every time it opens, so the safe option is never left off from a
previous export. A HAR carries real request headers, Authorization included, plus cookies —
treat the file like a credential.
Captures shown in this panel live only in memory while DevTools is open — closing DevTools clears them, and nothing is written to storage until you explicitly turn a captured request into a rule or export a HAR. That is the difference from the captures list in the options page, which is stored on purpose.
Export writes your rules and groups to a JSON file, together with your variables and environments. Exporting everything includes every variable you have; exporting a selection of rules only includes the variables those rules actually reference by name, and prunes each environment down to the same set. Which environment is currently active is a local preference and is never part of the file.
Because that file gets shared, the export goes through a dialog first. It lists what it found that looks
like a credential, and ticks Replace credentials with __REDACTED__ for you
when there is anything to hide. Sanitizing looks for names that suggest a credential — anything
containing auth, token, secret, password,
passwd, apikey, api_key, api-key,
credential, signature, cookie or session — and
replaces the matching value with __REDACTED__. It checks:
? would corrupt it.
{{token}}, {{apiKey}} and friends are the likeliest secret in
the whole file. An environment's overrides are judged by the name of the variable they point at, so a
"Prod" override of token is redacted exactly like the variable itself.
It deliberately does not touch:
value or data would not be caught, because the check is name-based, not
content-based.
"true"/"false", even under a sensitive-looking
name. A flag is not a secret, and redacting it would just produce a broken rule.
access-control-allow-credentials (a fixed CORS flag) and www-authenticate
(the challenge that makes a mocked 401 realistic).
Treat sanitize-on-export as a safety net against the most common accidental leak, not as a guarantee that an exported file contains no secrets — review what you're sharing before you send it.
On the way back in, Import lets you pick what to bring over and warns you when a rule already exists (same type, method and pattern), offering to import it as a copy, skip it or overwrite the existing one — with the same choice, separately, for variables whose value differs from yours.