Declarative Net Request Rules
Master chrome.declarativeNetRequest static, dynamic, and session rulesets in MV3 — rule schema, modifyHeaders, redirect, block actions, quota limits, and getMatchedRules debugging.
Master chrome.declarativeNetRequest static, dynamic, and session rulesets in MV3 — rule schema, modifyHeaders, redirect, block actions, quota limits, and getMatchedRules debugging.
In MV2, extensions could intercept every network request in JavaScript and mutate it synchronously — a model that allowed ad blockers and privacy tools to work but also let extensions stall page loads indefinitely and inspect sensitive request bodies. Manifest V3 removes that capability entirely. Any extension still relying on chrome.webRequest with the blocking extraInfoSpec will find those listeners silently ignored. The replacement is chrome.declarativeNetRequest (DNR): a declarative rule engine built into the browser process that evaluates rules without invoking extension JavaScript at all. This guide is part of Core APIs & Cross-Browser Data Management.
The first thing that trips developers: DNR rules do not fire simply because you defined them. A rule silently does nothing if your extension lacks a matching host permission, if a higher-priority allow or allowAllRequests rule is already in effect, or if the resourceTypes condition doesn’t align with what the browser classifies the request as. Understanding the rule-matching pipeline is what separates rules that work from rules that silently fail.
Before writing your first rule, verify these are already in place:
declarativeNetRequest permission in manifest.json — required for all rule evaluation. Add declarativeNetRequestFeedback in development builds to unlock getMatchedRules and onRuleMatchedDebug.host_permissions covering every domain your rules target — rules for URLs outside your host permissions are silently skipped, not rejected.allowAllRequests > allow > block > upgradeScheme > redirect > modifyHeaders. A single high-priority allow rule can defeat every block rule below it.Static rulesets are declared in the declarative_net_request manifest key. The browser loads and compiles them before any extension JavaScript runs. For user-configurable rules, add the permissions array entry; the rule_resources key registers static JSON files.
1{
2 "manifest_version": 3,
3 "name": "DNR Demo",
4 "permissions": [
5 "declarativeNetRequest",
6 "declarativeNetRequestFeedback" // dev builds only — omit in production
7 ],
8 "host_permissions": ["<all_urls>"],
9 "declarative_net_request": {
10 "rule_resources": [
11 {
12 "id": "baseline_rules",
13 "enabled": true,
14 "path": "rules/baseline.json"
15 }
16 ]
17 }
18}
Execution context: Parsed by the browser host at install and update time, before the background service worker initialises. Chrome and Edge accept the full schema; Firefox accepts declarative_net_request from Manifest V3 but enforces its own static-rule quota. Safari requires NSExtensionPointIdentifier set to com.apple.Safari.web-content-filter for content-blocking rules.
Every rule is a JSON object with four required top-level keys. The condition and action objects carry the real logic. Keep IDs as stable positive integers — changing a static rule’s ID in a bundled update removes the old rule and registers a new one, which matters for users who have exceptions built on top.
1// rules/baseline.json
2[
3 {
4 "id": 1,
5 "priority": 10,
6 "condition": {
7 "urlFilter": "||tracker.example.com^", // || = domain anchor, ^ = separator wildcard
8 "resourceTypes": ["script", "image", "xmlhttprequest"]
9 },
10 "action": {
11 "type": "block"
12 }
13 },
14 {
15 "id": 2,
16 "priority": 5,
17 "condition": {
18 "urlFilter": "||cdn.example.com^",
19 "resourceTypes": ["script"],
20 "initiatorDomains": ["trusted.app"] // only fires when initiated from this domain
21 },
22 "action": {
23 "type": "allow" // exempts requests matching this condition
24 }
25 }
26]
Execution context: Static rules are evaluated entirely in the browser’s networking layer — no extension JavaScript executes during matching. initiatorDomains and requestDomains are Chrome 101+ / Firefox 128+. Omitting resourceTypes matches all resource types, which is almost never what you want for precise blocking.
Header modification is the most nuanced action type. You can operate on both request and response headers. Each header entry specifies a header name, an operation (append, set, or remove), and for append/set a value. The rule needs the declarativeNetRequest permission; modifying response headers for cross-origin requests may also require appropriate CORS headers to already be present.
1{
2 "id": 10,
3 "priority": 20,
4 "condition": {
5 "urlFilter": "||api.internal.example.com^",
6 "resourceTypes": ["xmlhttprequest", "fetch"]
7 },
8 "action": {
9 "type": "modifyHeaders",
10 "requestHeaders": [
11 { "header": "X-Extension-Token", "operation": "set", "value": "v3-auth" },
12 { "header": "X-Debug-Source", "operation": "append", "value": "mv3-ext" }
13 ],
14 "responseHeaders": [
15 { "header": "Access-Control-Allow-Origin", "operation": "set", "value": "*" }
16 ]
17 }
18}
Execution context: Header modification runs in the browser’s networking thread before JavaScript in the page or worker sees the request or response. Firefox 128+ supports modifyHeaders for static rules; dynamic modifyHeaders requires Firefox 128+ as well. Safari 18+ supports a subset of response header modification.
The redirect action routes a matching request to a different URL. It supports four sub-forms: url (absolute URL), extensionPath (a path inside your extension package), regexSubstitution (capture-group rewrite paired with a regexFilter condition), and transform (selective URL component replacement).
1{
2 "id": 20,
3 "priority": 15,
4 "condition": {
5 "urlFilter": "||ads.example.com/banner*",
6 "resourceTypes": ["image", "sub_frame"]
7 },
8 "action": {
9 "type": "redirect",
10 "redirect": {
11 "extensionPath": "/assets/placeholder.png"
12 }
13 }
14}
Execution context: extensionPath redirects are the safest form — the target is always your own bundled asset. Redirecting to external url requires that the target URL’s origin is covered by host_permissions. Regex substitution is Chrome/Edge only; Firefox ignores regexSubstitution. The redirect action counts as an “unsafe” rule against the 5,000 unsafe-rule quota within dynamic rules.
Static rulesets are fast but immutable between extension updates. For user-configurable filters — blocked domains, custom headers, allow-list entries — use chrome.declarativeNetRequest.updateDynamicRules. Dynamic rules persist across browser restarts but are scoped to the current extension version.
1// background service worker — top-level or inside an event handler
2async function applyUserBlocklist(domains: string[]): Promise<void> {
3 const existing = await chrome.declarativeNetRequest.getDynamicRules();
4 const obsoleteIds = existing.map(r => r.id);
5
6 const newRules: chrome.declarativeNetRequest.Rule[] = domains.map((domain, i) => ({
7 id: 1000 + i, // start IDs in a separate namespace from static rules
8 priority: 50,
9 condition: {
10 urlFilter: `||${domain}^`,
11 resourceTypes: ['main_frame', 'sub_frame', 'script', 'image', 'xmlhttprequest']
12 },
13 action: { type: 'block' }
14 }));
15
16 await chrome.declarativeNetRequest.updateDynamicRules({
17 removeRuleIds: obsoleteIds,
18 addRules: newRules
19 });
20}
Execution context: Runs in the service worker. getDynamicRules() returns the current live set — always call it before adding to avoid duplicate-ID errors. Chrome allows up to 30,000 dynamic rules total, with 5,000 of those being “unsafe” types (redirect, modifyHeaders). Firefox caps dynamic rules at 5,000. If you need to store the user’s original blocklist, persist it to Chrome Storage API & Sync — dynamic rules are lost if the extension is disabled and re-enabled.
Session rules behave like dynamic rules but are cleared when the browser session ends (or the extension reloads). They are ideal for ephemeral debugging state or per-session user preferences that don’t need to survive a restart.
1// Inject a session-scoped debug header — gone on next browser restart
2await chrome.declarativeNetRequest.updateSessionRules({
3 removeRuleIds: [9001],
4 addRules: [{
5 id: 9001,
6 priority: 100,
7 condition: {
8 urlFilter: '||staging.internal.example.com^',
9 resourceTypes: ['xmlhttprequest']
10 },
11 action: {
12 type: 'modifyHeaders',
13 requestHeaders: [
14 { header: 'X-Debug-Session', operation: 'set', value: 'true' }
15 ]
16 }
17 }]
18});
Execution context: Session rules are stored in memory, not persisted to disk. They require the same declarativeNetRequest permission as dynamic rules. getSessionRules() returns the current active set. The session-rule quota is 5,000 rules (Chrome 116+). Firefox does not yet support session rules.
When a rule silently fails to match, the first tool to reach for is chrome.declarativeNetRequest.getMatchedRules. It returns records of which rules fired against which URLs in the last five minutes. Add declarativeNetRequestFeedback to your development manifest; omit it from production builds. The full debugging workflow — priority conflicts, urlFilter syntax errors, missing host permissions — is covered in debugging rules that don’t match.
1// Inspect recent rule matches (requires declarativeNetRequestFeedback permission)
2const result = await chrome.declarativeNetRequest.getMatchedRules({ tabId: -1 });
3for (const match of result.rulesMatchedInfo) {
4 console.log(
5 `Rule ${match.rule.ruleId} (ruleset: ${match.rule.rulesetId})`,
6 `matched ${match.request.url} at ${new Date(match.timeStamp).toISOString()}`
7 );
8}
Execution context: Available in service worker and extension pages. Requires declarativeNetRequestFeedback permission and the extension must be in developer mode (unpacked) or be in a testing profile. Pass tabId to scope results to one tab, or omit for all tabs. Chrome only; Firefox and Safari do not implement getMatchedRules.
redirect, modifyHeaders).regexFilter.chrome.debugger or a native messaging host.Chrome ships the most complete DNR implementation. Firefox 128+ added the core static and dynamic rule types but caps static rules at 5,000 per extension and lacks onRuleMatchedDebug and getMatchedRules. Use browser.declarativeNetRequest (with the browser.* namespace) in Firefox; the API surface is otherwise identical for basic rule types. Safari 18+ added DNR support but uses the WebKit engine’s own quota enforcement, does not expose onRuleMatchedDebug, and requires the extension be packaged as a Safari Web Extension. Regex support in Firefox is more restrictive — test any regexFilter patterns explicitly in Gecko before shipping.
For the migration path from webRequest-based extensions, see migrating webRequest to declarativeNetRequest step by step.
| Scope | Limit |
|---|---|
| Static rules (Chrome) | 30,000 |
| Static rules (Firefox) | 5,000 |
| Dynamic rules total | 30,000 |
| Dynamic rules — unsafe (redirect/modifyHeaders) | 5,000 |
| Session rules | 5,000 |
| Regex rules across all scopes | 1,000 |
| URL filter max length | 4,096 characters |
| Priority range | 1 – 2,147,483,647 |
Diagnose why a declarativeNetRequest rule silently fails — priority conflicts, urlFilter syntax, resourceTypes, host permissions, allow vs allowAllRequests, and the rules tester.
A practical step-by-step guide to replacing MV2 blocking webRequest listeners with MV3 declarativeNetRequest rules — audit, mapping, dynamic injection, and validation.