ProxyRules cookbook
Recipes for common proxyrules setups. Each recipe's
rules file and fixtures below are the real files in
examples/proxyrules-cookbook/,
and mockstack/tests/live/test_cookbook.py runs every curl command on this page
against a live mockstack, so the responses shown are the responses you get.
Running the recipes
The cookbook README has the one-time setup. In short: start the echo "real service" in one terminal, then, from a recipe's directory, fill in the rules file's placeholders and start mockstack:
uv run python ../upstream.py # echo upstream on 127.0.0.1:8081, in its own terminal
export FIXTURES_DIR="$PWD/fixtures" UPSTREAM_URL=http://127.0.0.1:8081
envsubst '${FIXTURES_DIR} ${UPSTREAM_URL}' < rules.yml > rules.local.yml
MOCKSTACK__STRATEGY=proxyrules MOCKSTACK__PROXYRULES_RULES_FILENAME=rules.local.yml uv run mockstack
Under each command, the comment lines show the status line, the X-Mockstack-Result
and X-Mockstack-Rule response headers and the body. curl -i prints a few more
headers (date, content-length, ...) that are left out here, and a ... stands for
the rest of a long body.
1. Serve fixtures to tagged test traffic
Put a narrow rule with a header predicate ahead of a broad passthrough for the same path prefix. Rules are tried in order and the first match wins, so tagged requests get the fixture, and every other request is reverse-proxied to the real service unchanged: untagged requests, and tagged requests to endpoints that have no fixture rule.
rules:
- name: projects-fixture
method: GET
pattern: ^/projects/api/v1/project/(?P<id>[a-z0-9-]+)$
headers:
x-test-run: ".+"
replacement: file://${FIXTURES_DIR}/projects/project.json.j2
- name: projects-passthrough
pattern: ^/projects/(.*)
replacement: ${UPSTREAM_URL}/\1
{"id": {{ id | tojson }}, "name": "Test project", "status": "ACTIVE", "test_run": {{ headers["x-test-run"] | tojson }}}
The fixture can use the named group id from pattern and the request headers
(names lower-cased); see Template context
for everything available.
curl -i -H "X-Test-Run: ci-42" http://127.0.0.1:8000/projects/api/v1/project/proj-123
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: projects-fixture
# {"id": "proj-123", "name": "Test project", "status": "ACTIVE", "test_run": "ci-42"}
curl -i http://127.0.0.1:8000/projects/api/v1/project/proj-123
# HTTP/1.1 200 OK
# x-mockstack-result: proxy
# x-mockstack-rule: projects-passthrough
# {"source":"upstream","path":"/api/v1/project/proj-123","method":"GET",...}
curl -i -H "X-Test-Run: ci-42" "http://127.0.0.1:8000/projects/api/v1/projects?page=2"
# HTTP/1.1 200 OK
# x-mockstack-result: proxy
# x-mockstack-rule: projects-passthrough
# {"source":"upstream","path":"/api/v1/projects","method":"GET","query":{"page":"2"},...}
2. Per-scenario fixture directories
A replacement containing {{ ... }} is rendered with Jinja, so a request header can
choose the fixture directory. Restrict the header predicate to the characters a
directory name needs: a value outside that class fails the predicate and falls through
to the passthrough. A value that passes the predicate but has no fixture directory is
not passed through: the rule still matched, so the request is answered 404 and
stamped error, and a typo'd scenario never reaches the real service.
rules:
- name: projects-scenario
method: GET
pattern: ^/projects/api/v1/project/(?P<id>[a-z0-9-]+)$
headers:
x-test-scenario: "[a-z0-9_-]+"
replacement: file://${FIXTURES_DIR}/{{ headers['x-test-scenario'] }}/projects/project.json.j2
- name: projects-passthrough
pattern: ^/projects/(.*)
replacement: ${UPSTREAM_URL}/\1
{"id": {{ id | tojson }}, "status": "ACTIVE", "scenario": "healthy"}
{"id": {{ id | tojson }}, "status": "ARCHIVED", "scenario": "archived"}
curl -i -H "X-Test-Scenario: healthy" http://127.0.0.1:8000/projects/api/v1/project/proj-123
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: projects-scenario
# {"id": "proj-123", "status": "ACTIVE", "scenario": "healthy"}
curl -i -H "X-Test-Scenario: archived" http://127.0.0.1:8000/projects/api/v1/project/proj-123
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: projects-scenario
# {"id": "proj-123", "status": "ARCHIVED", "scenario": "archived"}
curl -i -H "X-Test-Scenario: flaky" http://127.0.0.1:8000/projects/api/v1/project/proj-123
# HTTP/1.1 404 Not Found
# x-mockstack-result: error
# x-mockstack-rule: projects-scenario
# {"error":"Template file not found."}
curl -i -H "X-Test-Scenario: Healthy" http://127.0.0.1:8000/projects/api/v1/project/proj-123
# HTTP/1.1 200 OK
# x-mockstack-result: proxy
# x-mockstack-rule: projects-passthrough
# {"source":"upstream","path":"/api/v1/project/proj-123","method":"GET",...}
Warning
Never let an unconstrained request value build a fixture path or an upstream URL.
mockstack rejects a rendered path containing .., but a .* predicate would still
let the header pick any file within the fixture tree. See the warning under
Dynamic replacements.
3. Mock a single-endpoint API by request body
SQL gateways and RPC-style APIs send every request to one path and put the intent in
the body. A json: predicate maps a dotted path in the JSON body to a regex that must
match the whole value. Here (?is) makes the match case-insensitive and lets .
cross the line breaks of multi-line SQL; single-quoted YAML keeps the backslashes of
\b and \s as they are. The fixture receives the parsed body as request_json,
and the tojson filter writes it back as valid JSON.
rules:
- name: sales-facts-query
method: POST
pattern: ^/analytics/v1/sql$
json:
query: '(?is).*\bfrom\s+sales_facts\b.*'
replacement: file://${FIXTURES_DIR}/analytics/sales_facts.json.j2
- name: analytics-passthrough
pattern: ^/analytics/(.*)
replacement: ${UPSTREAM_URL}/\1
{"columns": ["region", "total"], "rows": [["north", 1250.0], ["south", 980.5]], "request": {{ request_json | tojson }}}
curl -i -H "Content-Type: application/json" \
-d '{"query": "SELECT region, SUM(amount) AS total FROM sales_facts GROUP BY region"}' \
http://127.0.0.1:8000/analytics/v1/sql
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: sales-facts-query
# {"columns": ["region", "total"], "rows": [["north", 1250.0], ["south", 980.5]], "request": {"query": "SELECT region, SUM(amount) AS total FROM sales_facts GROUP BY region"}}
curl -i -H "Content-Type: application/json" \
-d '{"query": "SELECT id FROM orders LIMIT 10"}' \
http://127.0.0.1:8000/analytics/v1/sql
# HTTP/1.1 200 OK
# x-mockstack-result: proxy
# x-mockstack-rule: analytics-passthrough
# {"source":"upstream","path":"/v1/sql","method":"POST",...}
4. Match JSON literals
A json: predicate is matched against the JSON text form of the value at its path.
Strings are matched as they are, without quotes. Every other value is re-serialised as
compact JSON, not taken from the request's wire text: true, false, null,
numbers as Python's json writes them, and objects and arrays with sorted keys and no
spaces. Quote predicate values in YAML ("true", "50") so they stay strings.
rules:
- name: express-orders
method: POST
pattern: ^/orders/api/v1/search$
json:
filter.express: "true"
replacement: file://${FIXTURES_DIR}/orders/search.json.j2
- name: unassigned-orders
method: POST
pattern: ^/orders/api/v1/search$
json:
filter.assignee: "null"
replacement: file://${FIXTURES_DIR}/orders/search.json.j2
- name: page-size-50
method: POST
pattern: ^/orders/api/v1/search$
json:
page.size: "50"
replacement: file://${FIXTURES_DIR}/orders/search.json.j2
- name: gold-customer
method: POST
pattern: ^/orders/api/v1/search$
json:
filter.customer: '\{"id":42,"tier":"gold"\}'
replacement: file://${FIXTURES_DIR}/orders/search.json.j2
- name: orders-passthrough
pattern: ^/orders/(.*)
replacement: ${UPSTREAM_URL}/\1
{"orders": [{"id": "ord-1001", "status": "OPEN"}], "total": 1}
All rules share one path, so X-Mockstack-Rule tells which predicate matched:
curl -i -H "Content-Type: application/json" -d '{"filter": {"express": true}}' http://127.0.0.1:8000/orders/api/v1/search
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: express-orders
# {"orders": [{"id": "ord-1001", "status": "OPEN"}], "total": 1}
curl -i -H "Content-Type: application/json" -d '{"filter": {"express": "true"}}' http://127.0.0.1:8000/orders/api/v1/search
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: express-orders
curl -i -H "Content-Type: application/json" -d '{"filter": {"assignee": null}}' http://127.0.0.1:8000/orders/api/v1/search
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: unassigned-orders
curl -i -H "Content-Type: application/json" -d '{"filter": {"status": "OPEN"}}' http://127.0.0.1:8000/orders/api/v1/search
# HTTP/1.1 200 OK
# x-mockstack-result: proxy
# x-mockstack-rule: orders-passthrough
curl -i -H "Content-Type: application/json" -d '{"page": {"number": 1, "size": 50}}' http://127.0.0.1:8000/orders/api/v1/search
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: page-size-50
curl -i -H "Content-Type: application/json" -d '{"page": {"number": 1, "size": 5e1}}' http://127.0.0.1:8000/orders/api/v1/search
# HTTP/1.1 200 OK
# x-mockstack-result: proxy
# x-mockstack-rule: orders-passthrough
curl -i -H "Content-Type: application/json" -d '{"filter": {"customer": {"tier": "gold", "id": 42}}}' http://127.0.0.1:8000/orders/api/v1/search
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: gold-customer
What these show:
- Booleans:
"true"matches the JSON booleantrue, and also the string"true", because strings are matched without their quotes. nullversus absent:"null"matches a presentnull. A path that is absent never matches, whatever the regex, so{"filter": {"status": "OPEN"}}falls through.- Numbers:
"50"matches50. Numbers are re-serialised, so5e1is matched as50.0and does not match. Use a regex such as"50(\\.0)?"to accept both forms. - Objects:
{"tier": "gold", "id": 42}is matched as{"id":42,"tier":"gold"}, whatever the key order and spacing of the request.
5. Query-parameter predicates
pattern is matched against the request path only; the query string never reaches it.
Match query parameters with query: predicates instead, each a regex that must match
the parameter's whole value. When a parameter is repeated, mockstack uses its last
value, both for predicates and for query in templates.
rules:
- name: archived-users
method: GET
pattern: ^/users/api/v1/users$
query:
status: archived
replacement: file://${FIXTURES_DIR}/users/archived.json.j2
- name: users-page
method: GET
pattern: ^/users/api/v1/users$
query:
page: "[0-9]+"
replacement: file://${FIXTURES_DIR}/users/page.json.j2
- name: users-passthrough
pattern: ^/users/(.*)
replacement: ${UPSTREAM_URL}/\1
{"page": {{ query.page | int }}, "per_page": 2, "users": ["user-1", "user-2"]}
{"status": {{ query.status | tojson }}, "users": ["user-9"]}
curl -i "http://127.0.0.1:8000/users/api/v1/users?page=2"
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: users-page
# {"page": 2, "per_page": 2, "users": ["user-1", "user-2"]}
curl -i "http://127.0.0.1:8000/users/api/v1/users?status=archived&page=2"
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: archived-users
# {"status": "archived", "users": ["user-9"]}
curl -i "http://127.0.0.1:8000/users/api/v1/users?status=active&status=archived"
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: archived-users
# {"status": "archived", "users": ["user-9"]}
curl -i "http://127.0.0.1:8000/users/api/v1/users?status=archived&status=active"
# HTTP/1.1 200 OK
# x-mockstack-result: proxy
# x-mockstack-rule: users-passthrough
# {"source":"upstream","path":"/api/v1/users","method":"GET",...}
The second request satisfies both fixture rules; the first rule in the file wins.
6. Asserting in a test suite
A passthrough rule is a safety net for real traffic, but in a test suite it can hide a
broken fixture rule: if a path or header changes, the request silently reaches the real
service and the test may still pass. Assert X-Mockstack-Result on every response
that should come from a fixture:
"""Fail a test when mockstack did not answer from a fixture.
Copy into your test suite and point ``MOCKSTACK_URL`` at the running mockstack.
"""
import os
from collections.abc import Iterator
import httpx
import pytest
MOCKSTACK_URL = os.environ.get("MOCKSTACK_URL", "http://127.0.0.1:8000")
def expect_fixture(response: httpx.Response, rule: str | None = None) -> httpx.Response:
"""Assert that ``response`` was rendered from a fixture (by ``rule``, if given)."""
result = response.headers.get("X-Mockstack-Result")
served_by = response.headers.get("X-Mockstack-Rule")
assert result == "template", (
f"{response.request.method} {response.request.url.path} got "
f"{response.status_code} with X-Mockstack-Result={result!r} "
f"(rule {served_by!r}), not a fixture"
)
if rule is not None:
assert served_by == rule, f"served by rule {served_by!r}, expected {rule!r}"
return response
@pytest.fixture
def mockstack() -> Iterator[httpx.Client]:
headers = {"X-Test-Run": "ci"}
with httpx.Client(base_url=MOCKSTACK_URL, headers=headers) as client:
yield client
def test_project_comes_from_fixture(mockstack: httpx.Client) -> None:
response = mockstack.get("/projects/api/v1/project/proj-123")
expect_fixture(response, rule="project-fixture")
assert response.json()["status"] == "ACTIVE"
Run it against mockstack started on this recipe's rules:
If the fixture rule stops matching, the request is proxied and expect_fixture fails
with a message like GET /projects/api/v1/project/proj-123 got 200 with
X-Mockstack-Result='proxy' (rule 'projects-passthrough'), not a fixture.
Reading error results
When a failure is mockstack's own, the response is stamped X-Mockstack-Result: error
(plus X-Mockstack-Rule for the rule that matched) with a JSON error message. The
details, such as the rendered path or the exception, are only in mockstack's log.
| Status | Body | Meaning |
|---|---|---|
| 404 | {"error":"Template file not found."} |
A rule matched, but the fixture file it rendered does not exist (or its path contains ..) |
| 500 | {"error":"An internal error occurred while rendering the template."} |
The fixture file failed to render |
| 500 | {"error":"mockstack: internal error"} |
Any other failure, such as a replacement that references an undefined value or does not produce an absolute URL |
| 502 | {"error":"mockstack: upstream request failed"} |
The upstream could not be reached, or broke the connection or protocol |
| 504 | {"error":"mockstack: upstream request timed out"} |
The upstream did not answer within proxyrules_reverse_proxy_timeout |
A request that matches no rule at all is a different case: it is answered 404 stamped
X-Mockstack-Result: missing, with no X-Mockstack-Rule.
The rest of this recipe's rules file has one rule for each error. Start mockstack with
MOCKSTACK__PROXYRULES_REVERSE_PROXY_TIMEOUT=1 so the slow upstream times out:
{"query": {{ request_json.query.strip() | tojson }}}
rules:
# The rule the test suite relies on: tagged requests get a fixture.
- name: project-fixture
method: GET
pattern: ^/projects/api/v1/project/(?P<id>[a-z0-9-]+)$
headers:
x-test-run: ".+"
replacement: file://${FIXTURES_DIR}/projects/project.json.j2
- name: projects-passthrough
pattern: ^/projects/(.*)
replacement: ${UPSTREAM_URL}/\1
# 404 error: there is no fixture file for most user ids.
- name: user-fixture
method: GET
pattern: ^/users/api/v1/users/(?P<user_id>[a-z0-9-]+)$
replacement: file://${FIXTURES_DIR}/users/{{ user_id }}.json.j2
# 500 error: the replacement uses tenant_id, but the group is named tenant.
- name: tenant-projects
method: GET
pattern: ^/tenants/(?P<tenant>[a-z0-9-]+)/projects$
replacement: file://${FIXTURES_DIR}/tenants/{{ tenant_id }}/projects.json.j2
# 500 error: the fixture reads a JSON body that a GET request does not have.
- name: order-echo
pattern: ^/orders/api/v1/echo$
replacement: file://${FIXTURES_DIR}/orders/echo.json.j2
# 500 error: the replacement is a path, not an absolute URL.
- name: accounts-relative
pattern: ^/accounts/(.*)
replacement: /api/\1
# 502 error: nothing listens on port 1.
- name: invoices-unreachable
pattern: ^/invoices/(.*)
replacement: http://127.0.0.1:1/\1
# 504 error: the upstream answers after proxyrules_reverse_proxy_timeout.
- name: reports-slow
pattern: ^/reports/(.*)
replacement: ${UPSTREAM_URL}/slow
{"id": {{ id | tojson }}, "name": "Test project", "status": "ACTIVE"}
curl -i http://127.0.0.1:8000/users/api/v1/users/user-2
# HTTP/1.1 404 Not Found
# x-mockstack-result: error
# x-mockstack-rule: user-fixture
# {"error":"Template file not found."}
curl -i http://127.0.0.1:8000/tenants/tenant-a/projects
# HTTP/1.1 500 Internal Server Error
# x-mockstack-result: error
# x-mockstack-rule: tenant-projects
# {"error":"mockstack: internal error"}
curl -i http://127.0.0.1:8000/orders/api/v1/echo
# HTTP/1.1 500 Internal Server Error
# x-mockstack-result: error
# x-mockstack-rule: order-echo
# {"error":"An internal error occurred while rendering the template."}
curl -i http://127.0.0.1:8000/accounts/v1/balance
# HTTP/1.1 500 Internal Server Error
# x-mockstack-result: error
# x-mockstack-rule: accounts-relative
# {"error":"mockstack: internal error"}
curl -i http://127.0.0.1:8000/invoices/api/v1/invoices
# HTTP/1.1 502 Bad Gateway
# x-mockstack-result: error
# x-mockstack-rule: invoices-unreachable
# {"error":"mockstack: upstream request failed"}
curl -i http://127.0.0.1:8000/reports/daily
# HTTP/1.1 504 Gateway Timeout
# x-mockstack-result: error
# x-mockstack-rule: reports-slow
# {"error":"mockstack: upstream request timed out"}
7. Redirect mode versus reverse proxy
By default a rule whose replacement is a URL reverse-proxies the request: mockstack
calls the upstream and returns its response, stamped proxy. With
proxyrules_redirect_via=http_307_temporary (or http_301_permanent) mockstack
instead answers with a redirect to the rewritten URL, stamped redirect, and the client
makes the upstream call itself. File fixture rules are served the same way in both
modes. Start mockstack with MOCKSTACK__PROXYRULES_REDIRECT_VIA=http_307_temporary:
rules:
- name: user-fixture
method: GET
pattern: ^/users/api/v1/users/user-1$
replacement: file://${FIXTURES_DIR}/users/user-1.json.j2
- name: users-redirect
pattern: ^/users/(.*)
replacement: ${UPSTREAM_URL}/\1
curl -i http://127.0.0.1:8000/users/api/v1/users/user-1
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: user-fixture
# {"id": "user-1", "name": "Test user"}
curl -i http://127.0.0.1:8000/users/api/v1/users/user-2
# HTTP/1.1 307 Temporary Redirect
# location: http://127.0.0.1:8081/api/v1/users/user-2
# x-mockstack-result: redirect
# x-mockstack-rule: users-redirect
curl -i -L http://127.0.0.1:8000/users/api/v1/users/user-2
# HTTP/1.1 307 Temporary Redirect
# location: http://127.0.0.1:8081/api/v1/users/user-2
# x-mockstack-result: redirect
# x-mockstack-rule: users-redirect
#
# HTTP/1.1 200 OK
# {"source":"upstream","path":"/api/v1/users/user-2","method":"GET",...}
curl -i "http://127.0.0.1:8000/users/api/v1/users?page=2"
# HTTP/1.1 307 Temporary Redirect
# location: http://127.0.0.1:8081/api/v1/users?page=2
# x-mockstack-result: redirect
# x-mockstack-rule: users-redirect
Things to know before choosing redirect mode:
- The final response comes straight from the upstream, so it carries no
X-Mockstack-*headers, and a test cannot tell from it that mockstack was involved. - The client must be able to reach the upstream itself, and must follow redirects.
- The original query string is kept: it is appended to the rewritten URL in
location(with&if the replacement already has a query of its own).
8. Fixture status codes and headers
A fixture is answered with HTTP 200 unless its rule sets status, which makes it easy to
test how a client handles a dependency that is down, throttled, or answers with a
non-200 success. response_headers adds headers such as Retry-After or Location; a
list value sends a header once per item, and a Content-Type replaces the type inferred
from the file suffix. The response is still stamped X-Mockstack-Result: template: the
status came from your fixture, so error keeps meaning that mockstack itself failed.
rules:
# 503 with Retry-After: the orders service is down for this scenario.
- 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_DIR}/errors/unavailable.json.j2
# 429 with a problem+json body, which replaces the type inferred from .json.j2.
- name: orders-rate-limited
method: GET
pattern: ^/orders/api/v1/orders/(?P<order_id>[a-z0-9-]+)$
headers:
x-test-scenario: throttled
status: 429
response_headers:
Content-Type: application/problem+json
Retry-After: "5"
replacement: file://${FIXTURES_DIR}/errors/rate-limited.json.j2
# 201 Created, with a Location header for the new order.
- name: order-created
method: POST
pattern: ^/orders/api/v1/orders$
status: 201
response_headers:
Location: /orders/api/v1/orders/ord-1001
replacement: file://${FIXTURES_DIR}/orders/created.json.j2
- name: orders-passthrough
pattern: ^/orders/(.*)
replacement: ${UPSTREAM_URL}/\1
{"error": "service unavailable", "order_id": {{ order_id | tojson }}}
{"type": "about:blank", "title": "Too Many Requests", "status": 429}
{"id": "ord-1001", "status": "OPEN", "customer": {{ request_json.customer | tojson }}}
curl -i -H "X-Test-Scenario: outage" http://127.0.0.1:8000/orders/api/v1/orders/ord-1001
# HTTP/1.1 503 Service Unavailable
# retry-after: 30
# x-mockstack-result: template
# x-mockstack-rule: orders-outage
# {"error": "service unavailable", "order_id": "ord-1001"}
curl -i -H "X-Test-Scenario: throttled" http://127.0.0.1:8000/orders/api/v1/orders/ord-1001
# HTTP/1.1 429 Too Many Requests
# content-type: application/problem+json
# retry-after: 5
# x-mockstack-result: template
# x-mockstack-rule: orders-rate-limited
# {"type": "about:blank", "title": "Too Many Requests", "status": 429}
curl -i -H "Content-Type: application/json" -d '{"customer": "cust-7"}' http://127.0.0.1:8000/orders/api/v1/orders
# HTTP/1.1 201 Created
# location: /orders/api/v1/orders/ord-1001
# x-mockstack-result: template
# x-mockstack-rule: order-created
# {"id": "ord-1001", "status": "OPEN", "customer": "cust-7"}
curl -i http://127.0.0.1:8000/orders/api/v1/orders/ord-1001
# HTTP/1.1 200 OK
# x-mockstack-result: proxy
# x-mockstack-rule: orders-passthrough
# {"source":"upstream","path":"/api/v1/orders/ord-1001","method":"GET",...}
Things to know:
- The status and headers apply only once the fixture has rendered. A missing fixture is
still a 404 stamped
error, without them. - A
204or304is sent without a body, but its fixture file must still exist. statusandresponse_headersonly work onfile:///fixture rules: mockstack refuses to start when a rule whosereplacementis a URL sets them.- Headers that mockstack manages cannot be set:
Content-Length, hop-by-hop headers such asConnectionandTransfer-Encoding,Date,Serverand theX-Mockstack-*result headers.
9. Record fixtures from a real service
Instead of writing fixtures by hand, let mockstack record them. In record mode, a fixture rule whose file does not exist yet sends the request on to the next matching URL rule, writes the response into the fixture file, and answers from that file. A scrubber can rewrite each body before it is written, here to mask e-mail addresses. Start mockstack from the recipe directory:
mkdir -p fixtures
PYTHONPATH=. MOCKSTACK__STRATEGY=proxyrules MOCKSTACK__PROXYRULES_RULES_FILENAME=rules.local.yml \
MOCKSTACK__PROXYRULES_RECORD_MODE=missing MOCKSTACK__PROXYRULES_RECORD_ROOT=fixtures \
MOCKSTACK__PROXYRULES_RECORD_SCRUBBER=scrubbers:mask_emails uv run mockstack
rules:
# Serves users/<user_id>.json.j2, which record mode writes on the first request.
- name: users-recorded
method: GET
pattern: ^/users/api/v1/users/(?P<user_id>[a-z0-9-]+)$
replacement: file://${FIXTURES_DIR}/users/{{ user_id }}.json.j2
# Where record mode sends a request whose fixture does not exist yet.
- name: users-passthrough
pattern: ^/users/(.*)
replacement: ${UPSTREAM_URL}/\1
"""Scrubbers for mockstack record mode: mask personal data before a fixture is written."""
import re
from pathlib import Path
from fastapi import Request
EMAIL = re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
def mask_emails(body: str, *, request: Request, rule_name: str | None, path: Path) -> str | None:
"""Replace every e-mail address in a recorded body with ``***@***``."""
return EMAIL.sub("***@***", body)
The first request is recorded from the echo upstream, with the address masked:
curl -i "http://127.0.0.1:8000/users/api/v1/users/user-7?contact=ada@example.com"
# HTTP/1.1 200 OK
# x-mockstack-result: record
# x-mockstack-rule: users-recorded
# {"source":"upstream","path":"/api/v1/users/user-7","method":"GET","query":{"contact":"***@***"},...}
fixtures/users/user-7.json.j2 now holds that body, after a {# mockstack:recorded #}
marker. The same request is then served from the file, without calling the upstream:
curl -i "http://127.0.0.1:8000/users/api/v1/users/user-7?contact=ada@example.com"
# HTTP/1.1 200 OK
# x-mockstack-result: template
# x-mockstack-rule: users-recorded
# {"source":"upstream","path":"/api/v1/users/user-7","method":"GET","query":{"contact":"***@***"},...}
Things to know:
- Until a fixture is recorded, its requests really go to the real service, including
POST,PUTandDELETE-- so record against a safe environment, never production. - Restart without the three
RECORDsettings to replay only.overwritemode re-records files carrying the marker and never touches hand-written fixtures. - A response is recorded only when its status matches the rule's
status(200 by default) and its body is text; otherwise it is returned stampedproxy. See Recording fixtures. - Recorded files contain whatever the real service returned. Review them before committing, and never run record mode on a shared or exposed instance.