repomatic.pages_redirects module¶
A faithful Python replica of the engine Cloudflare Pages runs _redirects on.
Cloudflare’s documentation describes the file format; it does not describe the
accounting, and the accounting is where rules die. A site once lost the last 18
rules of its file for years this way, silently: wrangler pages deploy prints
nothing when the parser discards lines, and a dead redirect looks exactly like
a URL nobody visits. This module replicates the reference implementation so
lint-repo can audit a committed file the way production will read it, before
production reads it.
Transcribed on 2026-08-10 from the engine itself, not from the documentation, and compared again on 2026-10-08 with cloudflare/workers-sdk@6478d32, run on the same inputs:
Parsing:
packages/workers-shared/utils/configuration/parseRedirects.tsin cloudflare/workers-sdk, as bundled in wrangler 4.118 (the same code path Miniflare uses, and the same parser family the Pages asset server feeds on).Sorting into the two rule sets:
constructRedirectsinpackages/workers-shared/utils/configuration/constructConfiguration.ts.Matching:
packages/workers-shared/asset-worker/src/utils/rules-engine.ts, called bypackages/pages-shared/asset-server/handler.ts.
The five rules of the engine that the documentation does not state:
A static rule is only free while it appears before the first dynamic rule. The parser flips
canCreateStaticRuleto false permanently at the first source containing*or:placeholder. Every later rule, however static it looks, is charged against the dynamic budget.The dynamic budget is 100, and blowing it aborts the file. Rule 101 of that mixed stream does not get skipped: the parser breaks out of the loop, discarding every remaining line. Order is therefore not a style choice, it decides which rules exist.
Matching is anchored and literal about trailing slashes. A placeholder compiles to
[^/]+(at least one character, never a slash, never empty), a splat to.*(may be empty), and the whole source to^...$./a/:bdoes not match/x/y/and/a/*matches/a/with an empty splat.An exact rule is only probed first while it is free. The exact rules above the first dynamic one go to a map that the server reads before anything else. An exact rule below that point joins the dynamic rules, in file order: a pattern above it that matches the same path answers first, and the exact rule never fires.
A source that cannot compile is dropped at run time. A second
*, a*beside:splat, or a placeholder name used twice gives a regular expression with a duplicate group. The parser keeps such a rule, and the server discards it when the regular expression fails to build.
- class repomatic.pages_redirects.Rule(source, destination, status, line_number)[source]¶
Bases:
object- property compiles: bool¶
Whether the server can build the regular expression of the source.
It cannot when two groups take the same name: a second
*, a*beside:splat, or a placeholder name used twice. The parser keeps such a rule, and the server then drops it without a word.
- property unresolved: tuple[str, ...]¶
Destination tokens that no capture of the source replaces.
The server replaces
:namein the destination for each group the source captured, and:splatfor a*. Any other:namestays in the URL as literal text, and so does a*: the engine reports nothing, and the visitor lands on an address that holds a colon and a word.An exact source captures nothing, so its destination is literal text from end to end and this answers an empty tuple for it: a colon in the address of another site is ordinary there.
- class repomatic.pages_redirects.Invalid(message, line=None, line_number=None)[source]¶
Bases:
object
- class repomatic.pages_redirects.ParseResult(rules=<factory>, invalid=<factory>, aborted_at_line=None)[source]¶
Bases:
object
- repomatic.pages_redirects.parse_redirects(text)[source]¶
The exact algorithm of
parseRedirects, budget accounting included.- Return type:
- repomatic.pages_redirects.misordered_statics(rules)[source]¶
Exact-source rules charged against the dynamic budget by their position.
The engine’s static budget (2000) only covers exact rules appearing before the first dynamic source; every exact rule after that point burns a slot of the dynamic budget (100) instead. Such a file still works while the budget holds, so this is the early warning: each rule returned here brings the file one line closer to the silent abort
parse_redirects()reports asaborted_at_line. The fix is always the same reorder, all exact rules first, all pattern rules second.Caution
The reorder changes what the site answers for each rule that
shadowed_statics()returns: such a rule is dead where it sits, and it starts to fire once it moves above the patterns. Every other rule answers the same before and after.
- repomatic.pages_redirects.rule_pattern(source: str) Pattern[str][source]¶
Compile a rule source exactly the way
generateRuleRegExpdoes.Memoized:
apply_rule()recompiles the same dynamic rules once per probed path otherwise.
- repomatic.pages_redirects.apply_rule(rule, path)[source]¶
Return the destination for
path, or None if the rule does not match.A rule whose source cannot compile matches nothing: the server catches the error and drops the rule. See
Rule.compiles.
- repomatic.pages_redirects.sample_path(source)[source]¶
A concrete request path a rule source would have matched.
An exact source is already one, and is returned untouched, which is the case that matters: a dropped exact rule names the very URL that stops working. A pattern has no single answer, so each
:namestands in for itself and each*for one segment, yielding an illustration rather than a promise about live traffic.- Parameters:
source (
str) – Rule source, exact or patterned.- Return type:
- Returns:
A path that
rule_pattern()would match.
- repomatic.pages_redirects.discarded_rules(text, parsed)[source]¶
The rules the engine abandoned, recovered from the tail it never read.
parse_redirects()reports that it stopped and drops everything below, because that is what production does. Naming what was lost needs the tail parsed on its own, which is what this does, with the line numbers shifted back to where they sit in the real file.Caution
The tail is parsed with fresh budgets, so one long enough to exhaust them again reports only its first batch. Reading this as an illustration of what broke rather than an exhaustive inventory is the intent either way: the fix is the same reorder however many rules are below the line.
- Parameters:
text (
str) – The full_redirectssource.parsed (
ParseResult) – Whatparse_redirects()made of it.
- Return type:
- Returns:
The abandoned rules, empty when the parser read the whole file.
- repomatic.pages_redirects.evaluate(rules, path)[source]¶
First-match evaluation over the kept rules, the way the asset server runs it.
The server reads a map of exact sources first, then walks the other rules in file order and takes the first match. The map holds only the exact rules above the first dynamic one. So one pass in file order gives the same answer: every rule of the map sits above every rule of the walk.
Warning
An exact rule does not win over a pattern “wherever it sits”. Below the first dynamic rule it is one more rule of the walk, and a pattern above it that matches the same path answers first.
shadowed_statics()lists the exact rules that never fire for that reason.
- repomatic.pages_redirects.shadowed_statics(rules)[source]¶
Exact rules that never fire, each with the pattern that answers for it.
An exact rule below the first dynamic one is probed in file order with the patterns (see
evaluate()). When a pattern above it matches its source, that pattern answers every request the exact rule was written for. Nothing reports it: the request still redirects, to the destination of the pattern.