ProxyRules Strategy
The ProxyRulesStrategy routes each request through an ordered list of rules. A rule
can serve a Jinja fixture file, reverse-proxy the request to a real service, or
redirect the client there, so a single mockstack instance can mix mock responses with
real service calls.
See also the ProxyRules cookbook: tested, copy-paste recipes for the most common setups.
Overview
This strategy:
- Reads its rules from a YAML file; rules are tried in order and the first match wins
- Matches requests on a path regex, the HTTP method, and optional
headers,query,bodyandjsonpredicates - Serves
file:///Jinja templates as fixtures, with request data in the template context, and the rule's ownstatusandresponse_headers - Renders a
replacementthat contains Jinja delimiters as a template of its own (dynamic replacements), e.g. to pick a fixture directory from a header - Reverse-proxies to the rewritten URL by default, or answers with an HTTP redirect
- Stamps every response, including its own errors, with
X-Mockstack-ResultandX-Mockstack-Ruleheaders - Validates and compiles every rule at startup, so a broken rules file fails fast
- Can simulate resource creation for requests that match no rule
- Can record real upstream responses into the fixture files its rules serve (record mode)
- Provides OpenTelemetry integration for observability
Configuration
The strategy requires the following configuration:
settings = Settings(
strategy="proxyrules",
proxyrules_rules_filename="/path/to/rules.yaml",
proxyrules_redirect_via="reverse_proxy", # or "http_307_temporary" or "http_301_permanent"
proxyrules_reverse_proxy_timeout=10.0,
proxyrules_simulate_create_on_missing=False,
proxyrules_verify_ssl_certificates=True,
)
Every setting, with its environment variable, is described in Configuration.
Rules file
Rules are defined in a YAML file with the following structure:
rules:
- name: user-service
pattern: ^/api/v1/users/(.*)
replacement: http://user-service/api/v1/users/\1
method: GET # optional, if not specified matches all methods
Rule properties
name: Optional identifier for the rule, used in logs, telemetry and theX-Mockstack-Ruleresponse header.pattern: Regular expression matched against the request path withre.match, so it is anchored at the start of the path. The query string is not part of the path; match it withquery.replacement: Where the request goes. A URL is reverse-proxied or redirected to; afile:///path serves a file template. A plain replacement is applied withre.sub, so backreferences such as\1work. A replacement containing{{/{%is a dynamic replacement instead.method: Optional HTTP method to match, case-insensitive (if not specified, matches all methods,HEADandOPTIONSincluded).headers: Optional mapping of header name -> regex. Every listed header must be present and its whole value must match the regex (re.fullmatch). Names are case-insensitive. Use".*"to require presence only. A repeated header is matched on its first value.query: Optional mapping of query parameter -> regex; same regex semantics; parameter names are case-sensitive. A repeated query parameter (?tag=a&tag=b) is matched on its last value.body: Optional regex searched (re.search) in the decoded request body.json: Optional mapping of dotted JSON path -> regex, fully matched against the JSON text form of the value at that path (query,filter.client.id,items.0.name). Strings are matched raw, without quotes. Every other value is re-serialised as a compact JSON literal rather than taken from the request's wire text:true,false,null, numbers normalised (1e2is matched as100.0), and objects and arrays with sorted keys and no spaces ({"a":1,"b":2}), keeping non-ASCII characters as they are. A path that is absent never matches, while a presentnullmatches as the textnull. A rule withbody/jsonnever matches a request without a body.status: Optional HTTP status for the response of a file template rule, an integer from 200 to 599 (default 200). See Status codes and headers.response_headers: Optional mapping of header name -> value added to the response of a file template rule. A list value sends the header once per item.
Predicate values must be strings. YAML reads unquoted 2, true or 2024 as
numbers and booleans; mockstack converts them back with str(), which turns true
into True, so quote them: x-api-version: "2", active: "true". A predicate with
no value at all (x-foo:) is rejected.
Regexes in YAML
Write regexes as plain or single-quoted YAML scalars, where a backslash is an
ordinary character: replacement: https://projects.example/\1 or
query: '(?is).*from\s+sales_facts.*'. In a double-quoted scalar a backslash
starts a YAML escape sequence, so "\1" and "\s" do not parse and must be
written "\\1" and "\\s".
Matching order
All predicates on a rule are ANDed. Rules are evaluated in order and the first match wins, so put narrow, predicate-bearing rules before broad passthroughs:
rules:
- name: project-eval # only stamped eval traffic
method: GET
pattern: ^/projects/api/v1/project/(?P<id>[^/]+)$
headers:
x-request-eval-scenario: ".*"
replacement: file:///fixtures/projects/project.json.j2
- name: projects-passthrough # everything else reaches the real service
pattern: ^/projects/(.*)
replacement: https://projects.example/\1
Body predicates make it possible to mock endpoints where every request shares one path and the intent lives in the payload, such as SQL gateways:
- name: analytics-sales-eval
method: POST
pattern: ^/analytics/v1/sql$
headers:
x-request-eval-scenario: ".*"
json:
query: '(?is).*FROM\s+sales_facts.*'
replacement: file:///fixtures/analytics/sales_facts.json.j2
The body is read once per request and cached by Starlette, so predicates do not interfere with reverse proxying or resource-creation simulation.
Load-time validation
The rules file is loaded, and every rule compiled, when the strategy is constructed at
startup. mockstack refuses to start when the file cannot be read, is not UTF-8, does not
parse as YAML or has no top-level rules list. It also refuses, naming the offending
rule by position and name (e.g. rules.yml: rule #2 ('projects-fixture'): 'pattern' is
required), when:
- a rule has a key mockstack does not know, such as a misspelt
header(the error suggests the closest key); - a rule is not a mapping, has no
patternorreplacement, has apattern,replacementormethodthat is not a string, or hasheaders,queryorjsonthat is not a mapping; - a regex (
patternor any predicate) is invalid, or too large or deeply nested to compile; - a predicate has no value, or has a list or mapping where a single regex belongs;
- a Jinja
replacementhas a syntax error; - a
replacementmixes Jinja delimiters with a regex backreference (\1,\g<id>) in its literal text; statusis not an integer from 200 to 599;- a
response_headersentry has an invalid name, no value, or a value that is not a string or number, contains a control character, starts or ends with whitespace or is not Latin-1, or names a header mockstack manages; statusorresponse_headersis set on a rule whosereplacementis a plain URL rather than afile:///fixture;- a named group in
patternshadows a reserved template variable (path,method,query,headers,request_json,groups).
A broken rule therefore fails when mockstack starts, not on the first request that happens to reach it.
Example rules
Here are some example rules:
rules:
# Redirect all GET requests to /api/v1/users/* to the user service
- name: user-service-get
pattern: ^/api/v1/users/(.*)
replacement: http://user-service/api/v1/users/\1
method: GET
# Proxy all POST requests to /api/v1/orders to the order service
- name: order-service-post
pattern: ^/api/v1/orders
replacement: http://order-service/api/v1/orders
method: POST
# Redirect all requests to /api/v1/products to the product service
- name: product-service
pattern: ^/api/v1/products/(.*)
replacement: http://product-service/api/v1/products/\1
File templates
A replacement starting with file:/// serves a Jinja2 template instead of proxying.
The path is an absolute filesystem path; templates_dir is not consulted.
file:// followed directly by an absolute path (i.e. file:///abs/path, three
slashes total) is the canonical form; when building the path from a variable that
already starts with / (e.g. ${FIXTURES_DIR}), write file://${FIXTURES_DIR}/...
(two slashes) so the interpolated value supplies the third -- file:///${FIXTURES_DIR}/...
would otherwise double up into a four-slash, //-rooted path.
rules:
- name: project-fixture
method: GET
pattern: ^/projects/api/v1/project/(?P<id>[^/]+)$
replacement: file:///fixtures/projects/project.json.j2
The response content type comes from the file suffix, ignoring a trailing .j2
(project.json.j2 -> application/json). A successfully rendered template returns
HTTP 200, or the rule's status, stamped X-Mockstack-Result: template; see
Error handling for the failure cases. Use the tojson filter to
write request values into JSON fixtures, e.g. {"id": {{ id | tojson }}}. Fixture files
and the rules file are read as UTF-8, matching how filefixtures reads its templates.
Status codes and headers
status and response_headers set the status and extra headers a fixture is served
with, for example to simulate a dependency that is down:
rules:
- name: orders-outage
method: GET
pattern: ^/orders/api/v1/orders/(?P<order_id>[a-z0-9-]+)$
headers:
x-test-scenario: outage
status: 503
response_headers:
Retry-After: "30"
replacement: file:///fixtures/errors/unavailable.json.j2
- The response is still stamped
X-Mockstack-Result: template, so a test can tell a fixture's 503 from a 5xxerrorthat mockstack itself returned. - The status and headers apply only once the fixture has rendered. A missing fixture or a render failure is answered as described in Error handling, without them.
- A
204or304is sent without a body; the fixture file must still exist. - Header values are sent as written. A list value sends the header once per item (for
example several
Set-Cookieheaders), and aContent-Typereplaces the type inferred from the file suffix. As with predicates, quote values that YAML would read as numbers or booleans. - Headers that mockstack manages cannot be set:
Content-Length, the hop-by-hop headers (Connection,Keep-Alive,Proxy-Authenticate,Proxy-Authorization,TE,Trailer,Transfer-Encoding,Upgrade),Date,Server, and theX-Mockstack-ResultandX-Mockstack-Ruleresult headers. - Only fixture rules can set them. A rule whose plain
replacementis a URL is rejected at startup; a dynamic replacement is only known per request, so one that renders a URL is answered with a 500 stampederror.
The cookbook recipe
Fixture status codes and headers
shows a 503, a 429 with application/problem+json, and a 201 with Location.
Template context
| Variable | Contents |
|---|---|
path, method |
Request path and method |
query |
Query parameters as a dict (a repeated parameter keeps its last value) |
headers |
Request headers as a dict, names lower-cased (a repeated header keeps its first value) |
request_json |
Parsed JSON body, or None when the body is empty or not JSON |
groups |
Tuple of the positional groups from pattern (None for a group that did not participate) |
<named group> |
Each named group from pattern, e.g. id for (?P<id>[^/]+) |
id, <segment> |
Identifiers inferred from the path, as in the filefixtures strategy |
When names collide, the reserved names (path, method, query, headers,
request_json, groups) always win, and named groups override identifiers
inferred from the path. A named group that would shadow a reserved name, such as
(?P<headers>...), is rejected when the rules are loaded. A named group that did
not participate in the match (an unmatched optional group) is absent from the
context rather than None.
Dynamic replacements
A replacement containing {{ ... }} or {% ... %} is rendered as a Jinja2
template in its own right, with the same context as file templates, including every
named group from pattern and the groups tuple of positional groups. This
lets one rule fan out to per-scenario fixture directories:
- name: project-eval
method: GET
pattern: ^/projects/api/v1/project/(?P<id>[^/]+)$
headers:
x-request-eval-scenario: "[a-z0-9_-]+"
replacement: file:///fixtures/{{ headers['x-request-eval-scenario'] }}/projects/project.{{ id }}.json.j2
The replacement is compiled once, when the rules are loaded, so a Jinja syntax
error stops mockstack from starting. In this mode regex backreferences (\1,
\g<id>) are not expanded -- the replacement string is rendered directly and
re.sub never runs, so use {{ groups[0] }} (positional) or the named group
({{ id }}) instead. A replacement that mixes Jinja delimiters with a backreference
is rejected at load; only its literal text is checked, so the same characters inside a
{{ ... }} expression or a {# ... #} comment are allowed. The URL fragment is not
carried into the rendered result either; only plain re.sub mode appends it
(URL-encoded) to the path it rewrites.
A replacement with no Jinja delimiters keeps the plain re.sub behaviour, and
backreferences work as before.
Template replacements are rendered with strict undefined: referencing a header,
query parameter, JSON key or group that is not there is an error, not an empty
string. The request is answered with a 500 stamped X-Mockstack-Result: error and
the rule's X-Mockstack-Rule, and the error is logged, instead of silently
rendering a path such as file:///fixtures//projects/project.json.j2. Give
optional values an explicit fallback, e.g.
{{ headers.get('x-request-eval-scenario', 'default') }}.
Warning
A rendered file:// path or proxied URL must never be built from an
unconstrained request value. mockstack rejects any rendered template path
containing .., but a predicate like x-request-eval-scenario: ".*" still lets
a header select an arbitrary sibling fixture within the intended directory tree
(and a rendered replacement that proxies, https://{{ ... }}, lets request data
choose the upstream entirely -- mockstack is already an open reverse proxy; only
expose it on trusted networks). A rendered request value must also never be the
leading element of a file:// path -- e.g.
file://{{ headers['x-root'] }}/f.json lets a header select an arbitrary
absolute path (it need only start with /), which the .. guard cannot catch
since no .. segment is ever involved. Restrict predicates feeding a rendered
path or URL to the character classes you actually expect, e.g. [a-z0-9_-]+.
Recording fixtures
Record mode fills in fixtures from the real service. When a fixture rule matches and its
file does not exist yet, mockstack sends the request to the next matching rule whose
replacement is a URL, writes the response body to the fixture file, and answers by
rendering that file, stamped X-Mockstack-Result: record. Later requests are served from
the file, stamped template, without calling the service. Existing rules files work
unchanged: the passthrough rule that follows a fixture rule is where it records from.
Later rules whose replacement is a plain file:/// path are skipped when looking for it.
rules:
- name: users-fixture
method: GET
pattern: ^/users/api/v1/users/(?P<user_id>[a-z0-9-]+)$
headers:
x-test-scenario: "[a-z0-9_-]+"
replacement: file:///srv/fixtures/{{ headers['x-test-scenario'] }}/users/{{ user_id }}.json.j2
- name: users-passthrough
pattern: ^/users/(.*)
replacement: https://users.example/\1
Start mockstack with MOCKSTACK__PROXYRULES_RECORD_MODE=missing and
MOCKSTACK__PROXYRULES_RECORD_ROOT=/srv/fixtures, run the traffic you want fixtures for,
then review and commit the recorded files and run without record mode.
proxyrules_record_mode |
Behaviour |
|---|---|
off (default) |
Fixtures are only served |
missing |
A fixture file that does not exist yet is recorded |
overwrite |
Files recorded before are recorded again, e.g. to refresh stale fixtures; hand-written fixtures are never overwritten |
A response is recorded only when:
- a later matching rule has a URL
replacement; - the request is not a
HEADorOPTIONS; - the upstream's status equals the fixture rule's
status(200 by default), so the fixture replays with the status it was recorded with; - the body is UTF-8 text;
- the body is not still compressed with a coding mockstack does not decode (for example
brwithout the optionalbrotlipackage);gzipanddeflatebodies are recorded decoded.
Otherwise the upstream's response is returned stamped proxy with the URL rule's name,
and the reason is logged -- at INFO for a HEAD/OPTIONS request or a scrubber that
returns None, since both are expected; every other reason is logged at WARNING. With
no later URL rule, a missing fixture is
the usual 404 error. An upstream that fails is the usual 502 or 504 error, and a
fixture file that cannot be written is a 500 error; nothing is written in either case.
A later rule's replacement is still rendered even when it is a
dynamic replacement, to learn whether it gives a URL at all --
so a later template replacement that fails (for example one that references a missing
header) turns that record attempt into a 500, the same as it would for a plain proxied
request to that rule.
Recorded files:
- start with
{# mockstack:recorded #}, which renders to nothing and is howoverwritetells them from hand-written fixtures; - contain the body exactly: any Jinja syntax or carriage return in it is printed by an expression, so it is never evaluated and replays byte for byte;
- are written to a temporary file and renamed into place, so concurrent requests never read a partial fixture;
- must resolve, symlinks followed, inside
proxyrules_record_root. A fixture path outside it is served as usual and not recorded, with a warning the first time for each rule.
While a fixture has not been recorded yet:
- requests really reach the upstream, including non-idempotent methods such as
POST,PUTandDELETE-- recording a create endpoint creates real resources there; - a response that cannot be recorded (a status mismatch, a still-encoded or non-UTF-8
body, or a scrubber that returns
None) is sent to the upstream again on every request, not only the first; - there is no locking between requests: concurrent first requests for the same fixture each reach the upstream, and the last write to complete wins. A reader never sees a partial file, but overlapping requests can each record a different upstream response;
HEADandOPTIONSrequests for a fixture that may still be recorded are proxied rather than served from a fixture file; inoverwritemode this also applies to a fixture that was already recorded.
Only the response body is recorded. The rule's status and response_headers apply when
it is replayed, and the content type follows the file suffix as for any fixture.
Scrubbing recorded bodies
proxyrules_record_scrubber names a module:function that is called with every body
before it is written. It returns the text to write, or None to skip recording that
response, which is then returned stamped proxy:
def mask_emails(body: str, *, request: Request, rule_name: str | None, path: Path) -> str | None:
return EMAIL.sub("***@***", body)
The reference is imported and checked when the settings are loaded, so a typo or a
target that is not callable stops mockstack from starting; the module must be
importable, e.g. with PYTHONPATH=.. A scrubber that raises, or returns something
other than a string or None, answers that request with a 500 error and nothing is
written. rule_name is the matched fixture rule's name (None when the rule has
none), and path is the resolved fixture file path that will be written.
The scrubber must be a regular synchronous function, not async def: it is called
directly, not awaited, so an async def scrubber returns a coroutine object rather
than a string, and the request gets a 500 the same as any other non-str, non-None
return value. See the cookbook's
Record fixtures from a real service.
Warning
Record mode writes files whose paths can be chosen by request data and whose content
comes from the upstream. Enable it only for a recording session on a trusted
network, never on a shared or exposed instance, and review recorded fixtures before
committing them: they contain whatever the real service returned. A test harness
that asserts X-Mockstack-Result: template fails on record, so a gating run that
accidentally records is caught.
Redirection methods
proxyrules_redirect_via selects what a rule whose replacement is a URL does.
Rules that serve a file:/// template are unaffected.
-
Reverse proxy (
reverse_proxy, the default; stampedproxy)- Server forwards the request to the target service
- Client is unaware of the redirection
- Useful when you need to work with clients that do not handle HTTP redirects gracefully.
- Every routed method is forwarded,
HEADandOPTIONSincluded, together with the query string, headers and body; theHostheader is set to the target's host. The upstream's status, headers and body are returned. - Request and response bodies are fully buffered. Hop-by-hop headers
(
Connection,Transfer-Encoding,Upgrade, ..., plus every header named in aConnectionvalue) are stripped in both directions, andContent-Lengthis recomputed for the buffered body, so chunked clients and chunked upstreams both work.1xx,204and304responses carry noContent-Length. A response toHEADkeeps the upstream'sContent-Length, unless itsContent-Encodingnames a coding that mockstack decodes (below): that length is the encoded one, so it is dropped. - Compressed upstream bodies are decoded while they are read:
gzipanddeflatealways,brandzstdonly when the optionalbrotliandzstandardpackages are installed. The decoded codings are removed fromContent-Encoding, which becomesidentityonly when no coding remains. A coding that was not decoded (e.g.compress) stays in the header, and the body is forwarded still encoded with it. - The upstream's
DateandServerheaders are dropped, so the response carries a singledateandserverheader: the mockstack server's own. Other repeated response headers, such as severalSet-Cookieheaders, are passed through one by one rather than merged. - An upstream that cannot be reached, or does not answer within
proxyrules_reverse_proxy_timeout, is answered by mockstack with a 502 or 504. A rewritten URL that cannot be sent at all, such as one that is not an absolute URL, is a rules-file mistake and is answered with a 500 (see Error handling). HTTPS certificates are verified unlessproxyrules_verify_ssl_certificatesis disabled.
-
HTTP Temporary Redirect (
http_307_temporary, 307; stampedredirect)- Client makes a new request to the target URL
- Preserves the original HTTP method
-
HTTP Permanent Redirect (
http_301_permanent, 301; stampedredirect)- Client makes a new request to the target URL
- Browsers may cache the redirect
In both redirect modes the target URL is built from the request path, and the
original query string is then appended to it in the Location header: after ?, or
after & when the target already has a query of its own, and before any #fragment
of the target. The client's follow-up request goes straight to the target, so its
response carries no X-Mockstack-* headers.
Result headers
Every response the proxyrules strategy returns -- including its own error
responses -- carries:
| Header | Value |
|---|---|
X-Mockstack-Result |
template, proxy, redirect, create, missing, record or error |
X-Mockstack-Rule |
The matched rule's name (or its pattern when unnamed); absent when no rule matched |
X-Mockstack-Result |
Status | Meaning |
|---|---|---|
template |
200, or the rule's status |
A file:/// fixture was rendered |
record |
The rule's status (200 by default) |
Record mode wrote the upstream response to the fixture file and served it from that file |
proxy |
Upstream's | The request was reverse-proxied to the rewritten URL |
redirect |
301 / 307 | An HTTP redirect to the rewritten URL |
create |
201 | No rule matched; resource creation was simulated |
missing |
404 | No rule matched |
error |
404 | A rule matched but its fixture file does not exist (or its path contains ..) |
error |
500 | A fixture failed to render, a template replacement failed, the rewritten URL is not a usable absolute URL, or another internal failure |
error |
502 | The upstream request failed |
error |
504 | The upstream request timed out |
A 404 or 5xx stamped error came from mockstack, not from a fixture or the upstream.
Test harnesses should assert on these headers to turn a silently proxied request into
a failure (see the cookbook's
Asserting in a test suite).
An unstamped 5xx response means the ASGI layer itself failed (outside the strategy),
not that proxyrules returned it.
Error handling
When no matching rule is found, and resource creation simulation is disabled or the
request does not look like a resource creation (see
Resource creation simulation), the strategy returns a
404 NOT FOUND response stamped X-Mockstack-Result: missing, with the configured
missing_resource_fields as its body.
Every other failure is answered by the strategy itself, stamped
X-Mockstack-Result: error and, once a rule has matched, X-Mockstack-Rule,
instead of surfacing as a bare, unstamped 500 from Starlette's
ServerErrorMiddleware:
| Failure | Status | Body |
|---|---|---|
The rendered fixture path does not exist, or contains .. |
404 | {"error": "Template file not found."} |
| The fixture fails to render | 500 | {"error": "An internal error occurred while rendering the template."} |
| The upstream is unreachable, resets the connection or breaks the protocol | 502 | {"error": "mockstack: upstream request failed"} |
The upstream does not answer within proxyrules_reverse_proxy_timeout |
504 | {"error": "mockstack: upstream request timed out"} |
The rewritten URL cannot be sent at all: not an absolute URL (e.g. /api/v1/users), or an invalid port |
500 | {"error": "mockstack: internal error"} |
| Anything else, e.g. a template replacement referencing a missing value, or a malformed JSON body on the resource-creation path | 500 | {"error": "mockstack: internal error"} |
The rendered fixture path is never echoed back in a response body; it is only logged. Upstream failures are logged at WARNING; missing or rejected fixture paths, and rewritten URLs that cannot be sent, at ERROR; template render failures and unexpected internal errors at ERROR with a traceback. Log lines name the matched rule. If the client disconnects before its request body has been read, no response is sent.
A broken rules file fails at startup rather than per request; see Load-time validation.
Resource creation simulation
When proxyrules_simulate_create_on_missing is enabled, a request that matches no
rule and looks like a resource creation -- a POST whose path does not look like a
search or a command (such as .../search or .../run), or a request whose path ends
in /create -- is answered by simulating the creation instead of a 404:
- Returning a 201 CREATED status code, stamped
X-Mockstack-Result: create - Echoing back the request body with metadata fields injected into it, when the
request is JSON (a JSON
Content-Type, or a path ending in.json); any other request gets an empty body
The default metadata fields are controlled via the configuration file and at the time of writing are as follows:
{
"id": "{{ uuid4() }}",
"createdAt": "{{ utcnow().isoformat() }}",
"updatedAt": "{{ utcnow().isoformat() }}",
"createdBy": "{{ request.headers.get('X-User-Id', uuid4()) }}",
"status": {
"code": "OK",
"error_code": null
}
}
OpenTelemetry integration
With tracing on (see OpenTelemetry settings,
which need the opentelemetry extra), the strategy adds the following attributes to the
request's span when a rule matches. The shared rule attributes (rule_name, rule_method, rule_pattern,
rule_replacement) always describe the rule named in the response's
X-Mockstack-Rule header, never some other rule that was merely consulted along the
way (e.g. a record-mode upstream rule whose response ends up recorded and replayed
from the fixture instead):
mockstack.proxyrules.rule_name: The name of the matched rule (if specified)mockstack.proxyrules.rule_method: The HTTP method the rule matches (if specified)mockstack.proxyrules.rule_pattern: The pattern used to match the requestmockstack.proxyrules.rule_replacement: The replacement as written in the rules filemockstack.proxyrules.rewritten_url: The final URL after applying the rule (proxy and redirect results)mockstack.proxyrules.result_type: The result type stamped on the response (proxy,redirect,template,error, or in record moderecord), set on every proxy, redirect, template and error resultmockstack.proxyrules.template_path: The rendered fixture path (template results)
An error response that names a rule (X-Mockstack-Result: error with an
X-Mockstack-Rule header -- e.g. a missing or unrenderable fixture, an unreachable
upstream, or a record-mode write failure) carries that rule's shared attributes and
result_type error, overwriting any result type an earlier, now-superseded call had
set for the same response (e.g. a reverse proxy attempt that set proxy before it
failed).
In record mode (proxyrules_record_mode), attempting to record from the next matching
URL rule also sets:
mockstack.proxyrules.upstream_rule_name: The name (or pattern, when unnamed) of the rule the request was proxied to while attempting to recordmockstack.proxyrules.rewritten_url: The upstream URL the request was proxied to while attempting to record, whether or not the response ended up recordedmockstack.proxyrules.recorded_path: The fixture path written, when the response was recorded (result_typeisrecord; the shared rule attributes describe the fixture rule)mockstack.proxyrules.not_recorded_reason: Why the response was not recorded, when it was not (result_typeisproxy; the shared rule attributes describe the upstream rule, since that is whatX-Mockstack-Rulenames)
Testing evaluation suites
A single mockstack instance can sit in front of a real service and serve fixtures
only to evaluation traffic: stamp eval requests with a header (e.g.
X-Request-Eval-Scenario: healthy), put a narrow, header-gated rule ahead of a
broad passthrough for the same path prefix, and point the fixture rule's
replacement at a file:/// template under a per-scenario directory. Every
other request -- unstamped, or one whose header value fails the predicate --
fails open and is reverse-proxied to the real service untouched. A stamped
request naming a scenario with no fixture on disk still matches the eval rule
and does not fall through to the passthrough: it gets a 404 stamped
X-Mockstack-Result: error with body {"error": "Template file not found."}, so
evaluators see a missing fixture as an error, not a passthrough.
Have the eval harness assert X-Mockstack-Result (and, ideally,
X-Mockstack-Rule) on every response instead of only checking the body: a rule
that stops matching -- a path change upstream, a typo'd header -- then fails the
run instead of silently exercising the real service with fixture inputs.
See
examples/proxyrules-eval-isolation
for a complete worked example, including a live test that runs its rules file
and fixtures for real, and the
ProxyRules cookbook for smaller recipes of the
same building blocks.