On 3 November 2026 we released a new version of this site to Azure Static Web Apps. The candidate had passed every check in our release gate, rebuilt from a clean clone. The deployment client refused it fourteen seconds after it started, before it uploaded a single file. Nothing reached production, and nothing needed rolling back.
The gate was not wrong about anything it checked. A green gate proves that a release agrees with the gate's model of the platform; whether the platform accepts the release is decided by the platform's own validator, and the first time a release meets that validator is the deploy. The useful part of the refusal is the question it forced: what did our gate believe about the platform, and where did that belief come from?
This analysis is about one release of this site. The facts come from our own operations record of that day, and Microsoft's documentation was checked on 7 November 2026. Resource names, identities and internal addresses are left out; nothing below depends on them.
What the gate proved
The gate rebuilds the site from a clean clone, composes the artefact that will be deployed, runs the test suite and the accessibility checks, compares the composed files byte for byte with a second build, and checks the size of the routing configuration against the limit Microsoft documents, "Max file size is 20 KB" (Configure Azure Static Web Apps). Every check passed, and each was right about the thing it checked.
None of them compared one routing rule with another. Each rule was checked on its own.
What the deployment client checked
A deploy with the Static Web Apps CLI hands the built site and its configuration to a deployment client (swa deploy). That client validated staticwebapp.config.json before uploading anything, and refused it:
Encountered an issue while validating staticwebapp.config.json: A rule was already processed with a duplicate route /…. Therefore, this rule will not be evaluated. Please remove the duplicate rule.
The message is quoted as it arrived, with the internal path removed. The configuration held six pairs of rules whose routes differed only by a trailing slash: each retired address had a redirect written as /old-page/ and a second one written as /old-page.
Because the validation runs before the upload, the refusal changed nothing. We fetched every file from both production hosts afterwards and found the previous build, byte for byte.
Where the second rule came from
An earlier change had modelled the service as matching a route exactly. On that model, a request for /old-page would miss a rule written as /old-page/, so the change gave every retired address a second rule without the slash, to catch both spellings.
Its test resolved routes with Python's fnmatch, which compares a path with a pattern as the model said the service would, and the test agreed with the change. It could not have done otherwise: the test and the code were written from the same assumption, so the test checked the code against the model and never the model against the service. A test written from the same model as the code it tests cannot find the model's error; it can only confirm that the code follows it.
What Microsoft documents, and what we had to observe
Microsoft's configuration page documents how rules are chosen: "Rules are evaluated in the order as they appear in the routes array", "Rule evaluation stops at the first match", and "Each request can match at most one rule". It also documents that a rule for a folder path "won't match requests for the file" inside it, such as /profile/index.html (Configure Azure Static Web Apps, above).
It does not say whether a rule's trailing slash matters when a request is matched, or whether matching distinguishes upper from lower case. So we asked the live service, the same evening, with anonymous requests:
- every redirect rule that was neither a wildcard nor a file answered its own target whether the request carried the trailing slash or not: 134 requests, none differing;
- a request without the slash was answered by the rule written with it, even where a later wildcard rule could have matched;
- changing the case of a letter in the path returned 404;
- a folder rule did not answer requests for its
index.html, as documented.
Our reading: the service matches a rule's path with or without its trailing slash, and the deployment client treats the two spellings as one route, which is why it reported a duplicate. The second rules were never needed, and the client was right to refuse them. These are observations of the service on one day, not documented behaviour, and we record them that way.
What we changed
The corrected release went out the same evening, after four changes:
- One rule per address. The six slashless rules were removed, so each retired address keeps the one rule written as its page was published.
- A test that matches as the service does. Routes are now resolved without regard to a trailing slash and with regard to case, as observed.
- A gate step for the rule the client enforces. The gate refuses two rules for one route. Run on the refused configuration, it names exactly the six pairs.
- A check after every deploy. The post-deploy replay requests both spellings of every rule that is neither a wildcard nor a file, so each release proves the matching again instead of assuming it.
The third change is the one that moves the failure. The duplicate is now found on the developer's machine, minutes after it is written, instead of at the deploy.
What only a deploy can tell you
Some verdicts cannot be obtained before a deploy. We tried to get the deployment client to validate a configuration without deploying it, and could not: it checks its deployment credentials before it reads the configuration. What else the client refuses, beyond the rule its message named, is therefore established only by deploying.
That makes the place where the refusal happens part of the design. A validator that runs before the upload turns a refused release into an event with no consequence, and that is how this one ended. Our reading: when a check exists only on the platform's side, the first deploy of a new configuration is its last test, and it should run when a refusal costs nothing, with the previous artefact one command away.
What we recommend, in order
These are our recommendations from one release, not Microsoft's.
- List what the platform validates at deploy time that your gate does not, and move each documented rule into the gate: a size limit, a schema, a uniqueness rule.
- Establish the platform's behaviour before you model it. Read the documentation first. Where it is silent, ask the live service with requests that change nothing, and write the tests from what it answered.
- Test rules against each other, not only each rule against its target: order, overlap and duplicates are where routing configurations fail.
- Re-prove the behaviour after every deploy, with the requests that established it.
- Deploy so that a refusal changes nothing, and keep the previous artefact ready to deploy again.
- Record the refusal verbatim. The client's own words named the rule we had not modelled.
What this does not settle
- Which checks the deployment client applies. We did not find the duplicate-route check described on Microsoft's configuration page; we know it from its message.
- Whether the matching we observed holds for every kind of rule. We probed redirect rules on one day; rewrites, wildcards and headers were not probed this way.
- Whether the observed matching will change. It is not documented, so the post-deploy replay is how we would notice.
Where this fits
- The green test that measured nothing describes tests that passed because they pointed at nothing. This one passed because it pointed at our own model.
- A report can look complete and still be impossible to verify separates what was re-asserted from what was verified. A gate can only re-assert what it was built to believe.
- What a slot swap promises, and what it abandons is another place where the platform's guarantee and a release's assumptions part ways.
Sources, checked on 7 November 2026
- Azure Static Web Apps: Configure Azure Static Web Apps, swa deploy
- Python: fnmatch, Unix filename pattern matching
- Our operations record of 3 November 2026, for the release, the refusal, the requests to production and the correction
Support independent engineering knowledge
Knowledge, Analysis, the Field Guide and the tools are free to read and use, and they stay that way. If this article saved you time, you can support the research and writing behind it. Support is voluntary and unlocks nothing.
Buy me a coffee (opens in a new tab)João Lívio's page on buymeacoffee.com