Rule Priority and Action Precedence Explained
How declarativeNetRequest decides which rule wins: priority first, then action order (allow, allowAllRequests, block, upgradeScheme, redirect), modifyHeaders stacking, frame-level allows, and multiple extensions.
Table of Contents
A request matches three of your rules — a block from a filter list, an allow from the user’s allowlist, and a modifyHeaders that strips a tracking header — and the outcome is not what you expected: the request is blocked despite the allowlist, or allowed but without the header change. DNR’s conflict resolution is fully specified and entirely predictable, but it is not “most specific wins” or “last added wins”. It is priority first, then a fixed order of actions, with special rules for header modification and frame-level allows. This guide walks through the algorithm with examples. It belongs to declarativeNetRequest rules.
The resolution algorithm
For each request, the browser finds every rule in your enabled rulesets, dynamic rules and session rules whose condition matches. It then selects the matching rule with the highest priority. If several share the highest priority, it breaks the tie by action type in this order: allow and allowAllRequests first, then block, then upgradeScheme, then redirect. That single winning rule decides whether the request is allowed, blocked, upgraded or redirected. modifyHeaders rules are handled separately: all matching modifyHeaders rules with a priority higher than the winning allow-type rule are applied, in priority order, and their header operations stack. Before any of this, an allowAllRequests rule matched by an ancestor frame acts as an allow for every request in that frame tree, with its own priority. Specificity, rule id and order of addition play no part.
Step-by-step: reason about your rules
1. Read the outcome from priority first
1const rules = [
2 { id: 1, priority: 1, action: { type: "block" }, condition: { requestDomains: ["ads.example"] } },
3 { id: 2, priority: 10, action: { type: "allow" }, condition: { requestDomains: ["ads.example"], initiatorDomains: ["partner.site"] } },
4];
5// Request to ads.example from partner.site → rule 2 (priority 10) wins → allowed
6// Request to ads.example from news.site → only rule 1 matches → blocked
Execution context: rule design. The higher-priority allow is a carve-out: it wins only where its narrower condition matches. That is the standard way to write exceptions in DNR. If both rules had priority 1, the allow would still win at the tie — but relying on the tie-break makes the exception fragile against a later block rule added at priority 2.
2. Know the tie-break order
1// Same priority, same request
2{ id: 3, priority: 5, action: { type: "redirect", redirect: { url: "https://x/stub.js" } }, condition: { urlFilter: "lib.js" } }
3{ id: 4, priority: 5, action: { type: "block" }, condition: { urlFilter: "lib.js" } }
4// Outcome: block (block beats redirect at equal priority)
Execution context: rule design. Many surprises come from expecting a redirect or upgrade to take effect when a block of equal priority also matches. If the redirect should win, give it a higher priority; do not rely on authoring order, which is irrelevant.
3. Understand how allows affect header rules
1[
2 { id: 5, priority: 2, action: { type: "allow" },
3 condition: { requestDomains: ["api.example"] } },
4 { id: 6, priority: 1, action: { type: "modifyHeaders",
5 requestHeaders: [{ header: "x-client", operation: "set", value: "readable" }] },
6 condition: { requestDomains: ["api.example"] } },
7 { id: 7, priority: 3, action: { type: "modifyHeaders",
8 responseHeaders: [{ header: "x-frame-options", operation: "remove" }] },
9 condition: { requestDomains: ["api.example"] } },
10]
11// Rule 5 allows the request. Rule 6 (priority 1 < 2) is suppressed by the allow.
12// Rule 7 (priority 3 > 2) still applies.
Execution context: rule design. An allow does more than prevent blocking — it also stops lower-priority header modifications. That is often intended (“allowlisted sites are left untouched”) and occasionally a bug (“why did my header rule stop working on allowlisted sites?”). Give header rules that must always apply a priority above your allowlist band.
4. Account for frame-level allows
1{ id: 8, priority: 100, action: { type: "allowAllRequests" },
2 condition: { requestDomains: ["news.example"], resourceTypes: ["main_frame"] } }
3// Every request made by a news.example page — including to ads.example — is allowed
4// unless a rule with priority > 100 matches it.
Execution context: rule design. When the main frame matches an allowAllRequests rule, every request in that tab’s frame tree is treated as matching an allow at priority 100. A block rule at priority 101 would still win, which is how you can keep blocking a known-malicious domain even on allowlisted sites. See allowlisting sites with allow and allowAllRequests rules.
5. Plan priority bands for the whole extension
1export const PRIORITY = Object.freeze({
2 listBlock: 1,
3 listException: 10, // @@ rules from filter lists
4 headerFix: 20,
5 userAllow: 50,
6 siteAllow: 100,
7 headerAlways: 150, // header rules that must apply even on allowlisted sites
8 security: 200, // malware / phishing blocks that override allowlists
9});
Execution context: a shared constants module used by the build and the worker. Bands with gaps leave room for new rule kinds without renumbering. Writing them down turns DNR’s priority system from a source of surprises into a design document.
6. Remember other extensions
When several extensions’ rules match the same request, each extension’s rules are resolved independently, and then the browser combines the outcomes: if any extension blocks, the request is blocked; otherwise if any redirects or upgrades, that applies (with a defined order among extensions); allows in one extension do not override blocks in another. Header modifications from different extensions are applied with installation order deciding conflicts on the same header. Your allowlist cannot unblock a request another blocker blocks.
7. Verify with testMatchOutcome
1// unpacked + declarativeNetRequestFeedback
2const { matchedRules } = await chrome.declarativeNetRequest.testMatchOutcome({
3 url: "https://ads.example/x.js", type: "script", initiator: "https://news.example", tabId: -1,
4});
5console.table(matchedRules);
Execution context: the service worker of an unpacked build. testMatchOutcome reports the rules that determine the outcome for a hypothetical request, which is the fastest way to check a priority scheme against tricky cases. Encode those cases as tests, as described in testing declarativeNetRequest rules offline.
Common mistakes
- Relying on rule order or specificity. Neither affects resolution; only priority and action type do.
- Exceptions at equal priority. They work by tie-break today and break when someone adds a higher block.
- Header rules below the allowlist. They silently stop applying on allowlisted sites.
- Assuming your allow beats another extension’s block. Extensions resolve independently; any block wins.
- Unplanned priorities. Ad hoc numbers lead to collisions; define bands.
Cross-browser variation
- Chrome / Edge: the algorithm as described, including
allowAllRequestsframe inheritance and cross-extension combination. - Firefox: implements the same priority and action ordering for DNR; its blocking
webRequestlisteners run separately and are not part of DNR resolution. - Safari: follows the DNR model with some unsupported actions or conditions; test priority interactions in Safari for portable rulesets.
Verification
- Write test requests that hit each band boundary and assert outcomes with
testMatchOutcome. - Add a header rule below and above the allowlist band and confirm it is suppressed and applied respectively on an allowlisted site.
- Install a second test extension with a block rule and confirm your allowlist does not override it.
FAQ
What is the maximum priority?
Priorities are positive integers; use a small, documented range rather than very large numbers.
Does a session rule beat a dynamic rule?
Only by priority. The rule’s source (static, dynamic, session) does not affect resolution.
Can a redirect ever win over a block?
Only with a higher priority.
Related
- Allowlisting sites with allow and allowAllRequests rules — the allowlist band in practice.
- Redirecting and rewriting headers with rules — the modifyHeaders details.
- Debugging rules that don’t match — when the outcome still surprises you.
- declarativeNetRequest rules — the parent topic.