Do not extract a handler from a god module yet.
You should pin every public route before that extract.
Record method, path, status, and body digest together.
Change only one function after the freeze turns green.
This walkthrough uses only the Python standard library.
No live network socket is required for the freeze.
No production traffic or customer data is assumed here.
Treat the sample module as a teaching fixture only.
Why a route table beats a gut refactor
Messy HTTP modules hide branching inside string path checks.
A small rename can shift a status code by accident.
A helper extract can alter JSON key order silently.
Callers then fail in ways that unit names never show.
A digest matrix catches those public byte-level shifts.
It does not prove business correctness by itself.
It only proves the public bytes did not move.
That is the only claim this method should make.
Model-assisted edits raise this same class of risk.
A model can reshape a handler and still look plausible.
Status codes and body bytes remain the ground truth.
Freeze both of them before any model sees the file.
The freeze contract
Store one JSON object for each exercised route.
Keep four required fields and nothing time-based.
-
methodstores the uppercase HTTP verb. -
pathstores the public path string. -
statusstores the integer status code. -
sha256stores the hex digest of body bytes.
Do not store timestamps inside the freeze rows.
Do not store wall-clock headers in this matrix.
Do not store randomized request identifiers either.
Those fields make an otherwise stable freeze flaky.
Optional fields still need the same determinism rule.
nbytes is safe because it derives from the body.
A request body fixture is safe if you control it.
Never add Date or X-Request-Id to the pin.
Step 1 — Isolate a pure dispatcher
Keep socket I/O out of the first freeze pass.
Drive a pure function instead of a real listener.
# dispatch.py — teaching fixture, not a framework
from __future__ import annotations
import json
from typing import Callable
Handler = Callable[[bytes], tuple[int, bytes]]
def _users_get(_body: bytes) -> tuple[int, bytes]:
payload = {"users": [{"id": 1, "name": "ada"}]}
return 200, json.dumps(payload, separators=(",", ":")).encode()
def _users_post(body: bytes) -> tuple[int, bytes]:
if not body:
err = {"error": "empty_body"}
return 400, json.dumps(err, separators=(",", ":")).encode()
try:
data = json.loads(body.decode())
except ValueError:
err = {"error": "invalid_json"}
return 400, json.dumps(err, separators=(",", ":")).encode()
name = data.get("name")
if not isinstance(name, str) or not name.strip():
err = {"error": "name_required"}
return 422, json.dumps(err, separators=(",", ":")).encode()
payload = {"id": 2, "name": name.strip()}
return 201, json.dumps(payload, separators=(",", ":")).encode()
def _item_get(_body: bytes) -> tuple[int, bytes]:
payload = {"id": 7, "ok": True}
return 200, json.dumps(payload, separators=(",", ":")).encode()
ROUTES: dict[tuple[str, str], Handler] = {
("GET", "/users"): _users_get,
("POST", "/users"): _users_post,
("GET", "/items/7"): _item_get,
}
def handle(method: str, path: str, body: bytes = b"") -> tuple[int, bytes]:
key = (method.upper(), path)
fn = ROUTES.get(key)
if fn is None:
err = {"error": "not_found"}
return 404, json.dumps(err, separators=(",", ":")).encode()
return fn(body)
Compact JSON separators are part of the public contract.
Key order is also part of the digest input.
Do not pretty-print responses on the freeze path.
Step 2 — Write the route digest file
# freeze_routes.py
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from dispatch import handle
CASES = [
("GET", "/users", b""),
("POST", "/users", b""),
("POST", "/users", b"{"),
("POST", "/users", b'{"name":""}'),
("POST", "/users", b'{"name":"lin"}'),
("GET", "/items/7", b""),
("GET", "/missing", b""),
("PUT", "/users", b""),
]
def row(method: str, path: str, body: bytes) -> dict[str, object]:
status, payload = handle(method, path, body)
digest = hashlib.sha256(payload).hexdigest()
return {
"method": method,
"path": path,
"body_b64": None if body == b"" else body.decode("utf-8", "replace"),
"status": status,
"sha256": digest,
"nbytes": len(payload),
}
def main() -> None:
table = [row(*case) for case in CASES]
Path("route_freeze.json").write_text(
json.dumps(table, indent=2, sort_keys=True) + "\n",
encoding="utf-8",
)
print(f"wrote {len(table)} rows")
if __name__ == "__main__":
main()
Execute the writer once against the current module.
python freeze_routes.py
python -c "import json; print(len(json.load(open('route_freeze.json'))))"
Commit route_freeze.json beside the dispatcher module today.
That file is now the public HTTP contract.
Empty output means CASES never reached handle.
Step 3 — Fail the job if any cell moves
# test_route_freeze.py
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from dispatch import handle
def test_route_matrix_matches_freeze() -> None:
expected = json.loads(Path("route_freeze.json").read_text())
assert expected, "freeze file must not be empty"
for item in expected:
method = item["method"]
path = item["path"]
raw = item["body_b64"]
body = b"" if raw is None else raw.encode()
status, payload = handle(method, path, body)
digest = hashlib.sha256(payload).hexdigest()
assert status == item["status"], (method, path, status, item["status"])
assert digest == item["sha256"], (method, path, digest)
assert len(payload) == item["nbytes"]
python -m pytest test_route_freeze.py -q
Do not edit dispatch.py before this test is green.
Green means the public bytes are pinned today.
Red means you still have no trustworthy baseline.
Step 4 — Make the smallest safe change
Extract one private helper and nothing else.
Do not split the route table in this commit.
Do not rename any public paths in this commit.
def _json(status: int, payload: dict) -> tuple[int, bytes]:
body = json.dumps(payload, separators=(",", ":")).encode()
return status, body
Wire _users_get through _json and stop there.
Leave POST handlers on the original inline path.
Leave the 404 branch on the original inline path.
Re-run the freeze test after that single wire-up.
python -m pytest test_route_freeze.py -q
If the digest moves, revert the helper immediately.
Pretty-print is the most common silent digest break.
Keep separators=(",", ":") byte-for-byte identical.
The default json.dumps call inserts ASCII spaces.
Common freeze misses
Read the assertion tuple when the test goes red.
It prints method, path, actual digest, and expected digest.
- Default JSON spacing changed the response body bytes.
- Dictionary key order changed during the helper extract.
- Unicode escaped where the baseline used raw UTF-8.
- A trailing newline appeared in an error body.
- The 404 payload changed while you touched GET.
Do not edit the freeze to silence a red test.
Expand CASES only when you intend a new contract.
A red digest is a public break until proven otherwise.
How to grow CASES without poisoning the pin
Do not invent paths you cannot currently serve.
Start from real traces or from the ROUTES keys.
Add one error body for each validation branch.
Add one unknown method and one unknown path.
Re-run the writer after you add a case.
That is a contract expansion, not a refactor.
Commit the new freeze rows in their own commit.
Then resume helper extracts on a later commit.
Cover empty body, truncated JSON, and missing fields.
Those three cases catch most POST helper mistakes.
A 201 case without a 422 case is an incomplete pin.
Unknown methods protect the not_found JSON shape.
Step 5 — Optional model pass after the freeze
A frozen matrix lets you review machine-proposed extracts.
Paste one handler, not the entire god module.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode offers free model access and a free server option.
Those two facts are the only product claims used here.
This article does not name models, quotas, or hardware.
It does not claim speed, quality, or permanence either.
A workable review loop uses six numbered checks.
- Keep
route_freeze.jsonunder version control. - Copy one handler plus the freeze rows it covers.
- Ask the model for a helper extract only.
- Apply the patch on a throwaway git branch.
- Run
python -m pytest test_route_freeze.py -q. - Keep the patch only if every digest matches.
The free server helps when a local GPU is absent.
The freeze file still executes on your own laptop.
Do not outsource the assertion step to the model.
The model does not own the public HTTP contract.
Bound the prompt so the patch cannot sprawl.
Extract _json(status, payload) from _users_get only.
Do not change ROUTES keys.
Do not change JSON separators.
Stop after one helper.
Reject any patch that touches extra route handlers.
Reject any patch that reformats JSON response bodies.
Reject any patch that injects logging timestamps.
A plausible helper that moves one digest is a failed patch.
Decision table
Use this table before you invite a model.
| Situation | Action |
| freeze file missing | Write the freeze. Do not edit handlers. |
| freeze test red | Restore the baseline. Do not extract. |
| one helper, tests green | A local extract is enough. |
| helper is obvious, time short | Stay local. Skip the model. |
| helper spans hidden branches | Expand CASES first. Then freeze. |
| you want a second opinion | The model may propose. Tests decide. |
| paths or status must change | Update the freeze in a separate commit. |
Never mix a behavior change with a helper extract.
Use two commits for those two kinds of work.
The first commit may update freeze rows with intent.
The second commit must keep every digest stable.
Git sequence
Record the freeze before any helper exists.
git add route_freeze.json dispatch.py test_route_freeze.py freeze_routes.py
git commit -m "pin route digest matrix before handler extract"
Extract the helper only after that commit exists.
git add dispatch.py
git commit -m "extract _json helper for GET /users only"
git show plus the freeze test is the review pack.
You do not need a long prose review to start.
If git diff lists two handlers, the extract is too large.
Split that diff before you argue about naming.
What this method does not prove
The body digest ignores response header maps completely.
It ignores cookie side effects and Set-Cookie churn.
It ignores database writes and log line contents.
Identical JSON can still hide a skipped side effect.
A 200 with identical JSON can still be wrong.
The handler can skip an auth check after extract.
The handler can skip a rate-limit branch as well.
Add those checks as separate tests in later commits.
SHA-256 here is not a security control boundary.
It is only a regression pin for public bytes.
Anyone with the freeze file can rebuild the table.
Rebuild ability is required for an honest contract.
Who should not use this approach
Skip this method if public routes are not stable.
Skip this method if bodies embed wall-clock timestamps.
Skip this method if JSON key order cannot be pinned.
Skip this method if the handler is not deterministic.
Also skip it for binary streaming response endpoints.
Chunk boundaries will churn the body digest often.
Use a header-only freeze for those streaming paths.
That workflow needs a different fixture and article.
Teams with a real HTTP contract suite can skip this.
OpenAPI plus golden responses already cover the same risk.
This method is for a god module with no tests.
Treat it as a bootstrap, not a final test strategy.
Recap
Pin status codes and body digests before any extract.
Extract one helper after the freeze test is green.
Keep JSON separators explicit in every response path.
Let the matrix, not plausibility, accept the patch.
If you use a free remote model, send one handler only.
Keep the freeze test running on your own machine.
The contract can stay local when the proposal does not.
Top comments (0)