Declarative Net Request API: Complete Guide
Master Chrome's Declarative Net Request API — rule formats, static and dynamic rules, header modification, redirects, migration from webRequest, and real-world examples.
Table of Contents
Declarative Net Request API
The complete guide to Chrome's network interception API — rules, priorities, debugging, and migrating from webRequest.
The declarativeNetRequest API (DNR) is how Manifest V3 extensions control network traffic. It replaces the webRequest blocking API with a rule-based system where you declare what to block, redirect, or modify — and Chrome's network stack executes it. You never see the actual requests.
This architectural shift is controversial. The webRequest API let extensions inspect and modify every byte of every request with arbitrary JavaScript logic. DNR limits you to a predefined set of rule types and conditions. But it is significantly faster (rules execute in C++ inside the browser, not JavaScript in a service worker), more private (extensions never see request content), and more reliable (rules persist even when the service worker is inactive).
Whether you are building an ad blocker, privacy tool, API proxy, or development utility, DNR is the API you need to master. This guide covers everything from basic rules to complex real-world patterns.
webRequest vs declarativeNetRequest#
The fundamental difference: webRequest is imperative (you write code that runs per request), declarativeNetRequest is declarative (you write rules that the browser evaluates).
| Feature | Aspect | webRequest (MV2) | declarativeNetRequest (MV3) |
|---|---|---|---|
| Execution model | JavaScript callback per request | Pre-compiled rules in browser engine | |
| Performance impact | Service worker wakeup per request | Near-zero — native C++ matching | |
| Privacy | Extension sees full request data | Extension never sees request content | |
| Rule limit | Unlimited (code-based) | Static: 300K, Dynamic: 30K, Session: 5K | |
| Persistence | Requires service worker running | Rules active even when SW inactive | |
| Request body access | Yes — can read POST bodies | No — cannot inspect request bodies | |
| Async decision | Yes — can defer blocking decision | No — rules are evaluated synchronously | |
| Header reading | Can read all headers | Can modify but not read headers |
300,000
Static Rule Limit
Per extension, across all static rulesets
30,000
Dynamic Rule Limit
Rules added/removed at runtime via API
5,000
Session Rule Limit
Temporary rules cleared when browser restarts
1,000
Regex Rule Limit
Rules using regexFilter (computationally expensive)
Rule Types#
DNR supports five action types. Each serves a distinct network interception use case.
Rule Format Deep Dive#
Every DNR rule is a JSON object with a condition (when to match), an action (what to do), and a priority (how to resolve conflicts).
// Basic block rule — block requests to a tracking domain
const blockTracker: chrome.declarativeNetRequest.Rule = {
id: 1,
priority: 1,
action: { type: "block" },
condition: {
urlFilter: "||tracker.example.com^",
resourceTypes: ["script", "image", "xmlhttprequest"],
},
};
// Redirect rule — send API requests through a proxy
const redirectApi: chrome.declarativeNetRequest.Rule = {
id: 2,
priority: 2,
action: {
type: "redirect",
redirect: {
transform: {
scheme: "https",
host: "proxy.myextension.com",
// Original path is preserved
},
},
},
condition: {
urlFilter: "||api.example.com/v2/*",
resourceTypes: ["xmlhttprequest"],
},
};
// Header modification — add CORS headers to API responses
const addCorsHeaders: chrome.declarativeNetRequest.Rule = {
id: 3,
priority: 1,
action: {
type: "modifyHeaders",
responseHeaders: [
{ header: "access-control-allow-origin", operation: "set", value: "*" },
{ header: "access-control-allow-methods", operation: "set", value: "GET, POST, PUT, DELETE" },
{ header: "access-control-allow-headers", operation: "set", value: "Content-Type, Authorization" },
],
},
condition: {
urlFilter: "||api.example.com/*",
resourceTypes: ["xmlhttprequest"],
},
};
// Allow exception — don't block the tracking domain on your own site
const allowException: chrome.declarativeNetRequest.Rule = {
id: 4,
priority: 2, // Higher priority than the block rule
action: { type: "allow" },
condition: {
urlFilter: "||tracker.example.com^",
initiatorDomains: ["myextension.com"],
resourceTypes: ["script", "image", "xmlhttprequest"],
},
};URL Filter Syntax#
The urlFilter field uses a custom pattern syntax, not regex (unless you use regexFilter):
|| — Match any scheme + domain start (anchors to domain boundary)
| — Anchor to URL start or end
* — Wildcard (matches any characters)
^ — Separator character (anything except alphanumeric, -, ., %)
Examples:
"||example.com^" → Matches https://example.com/anything
"||example.com/api/*" → Matches https://example.com/api/users
"*://*/ads/*" → Matches any URL with /ads/ in the path
"|https://exact.com/page|" → Matches only that exact URLFor complex patterns that urlFilter cannot express, use regexFilter:
const regexRule: chrome.declarativeNetRequest.Rule = {
id: 10,
priority: 1,
action: { type: "block" },
condition: {
regexFilter: "^https://example\\.com/user/\\d+/tracking",
resourceTypes: ["xmlhttprequest"],
},
};Regex rules are limited to 1,000 per extension because they are significantly more expensive to evaluate than pattern rules.
Static vs Dynamic vs Session Rules#
DNR rules live in three buckets, each with different lifecycle characteristics.
Static rules are declared in JSON files referenced from your manifest. They load when the extension installs and cannot be modified at runtime. Use them for your baseline rule set — the rules that are always active.
// manifest.json
{
"declarative_net_request": {
"rule_resources": [
{
"id": "baseline_rules",
"enabled": true,
"path": "rules/baseline.json"
},
{
"id": "privacy_rules",
"enabled": false,
"path": "rules/privacy.json"
}
]
}
}Static rulesets can be enabled/disabled at runtime, which is useful for optional features:
// Enable the privacy ruleset when user toggles the feature
await chrome.declarativeNetRequest.updateEnabledRulesets({
enableRulesetIds: ["privacy_rules"],
});Dynamic rules are added and removed via the API. They persist across browser restarts and service worker terminations. Use them for user-configurable rules — custom block lists, site-specific settings, or rules that change based on user preferences.
// Add a user-defined block rule
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [
{
id: 1001,
priority: 1,
action: { type: "block" },
condition: {
initiatorDomains: ["distracting-site.com"],
resourceTypes: ["main_frame"],
},
},
],
});
// Remove it later
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [1001],
});Session rules live only in memory and are cleared when the browser restarts. Use them for temporary rules — development overrides, time-limited blocks, or rules tied to a specific browsing session.
// Block a site for the current focus session
await chrome.declarativeNetRequest.updateSessionRules({
addRules: [
{
id: 5001,
priority: 3,
action: {
type: "redirect",
redirect: { extensionPath: "/blocked.html" },
},
condition: {
urlFilter: "||social-media.com^",
resourceTypes: ["main_frame"],
},
},
],
});Migration from webRequest#
If you are converting a MV2 extension that uses webRequest, here is the systematic approach.
Audit webRequest Listeners
List every webRequest listener in your extension. Categorize each by what it does: block requests, redirect, modify headers, or inspect content. Content inspection has no DNR equivalent — you'll need a different approach.
Convert Block Logic to Rules
Transform your blocking logic into DNR rules. If you block based on URL patterns, this is straightforward. If you block based on runtime conditions (time of day, user state), use dynamic rules that you update when conditions change.
Convert Redirects to Transform Rules
DNR redirect rules support URL transforms (change scheme, host, port, path, query, fragment individually) and static redirects. Map each redirect pattern to the appropriate DNR format.
Convert Header Modifications
Map setRequestHeader/setResponseHeader calls to modifyHeaders rules. Remember: DNR can set, append, or remove headers but cannot read existing header values.
Handle Unsupported Patterns
For logic that DNR cannot express (request body inspection, async decisions, content-based filtering), consider alternatives: content scripts for page modification, the fetch API in the service worker for API proxying, or accepting reduced functionality.
Test with Real Traffic
Use chrome.declarativeNetRequest.testMatchOutcome() and the onRuleMatchedDebug event to verify your rules match the same requests your webRequest listeners caught. Test edge cases: redirects, POST requests, cross-origin frames.
Real-World Examples#
Ad Blocking#
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||doubleclick.net^",
"resourceTypes": ["script", "image", "sub_frame", "xmlhttprequest"]
}
},
{
"id": 2,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||googlesyndication.com^",
"resourceTypes": ["script", "sub_frame"]
}
},
{
"id": 3,
"priority": 2,
"action": { "type": "allow" },
"condition": {
"urlFilter": "||googlesyndication.com^",
"initiatorDomains": ["allowlisted-site.com"],
"resourceTypes": ["script", "sub_frame"]
}
}
]CORS Proxy for Development#
[
{
"id": 100,
"priority": 1,
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{ "header": "access-control-allow-origin", "operation": "set", "value": "*" },
{ "header": "access-control-allow-methods", "operation": "set", "value": "GET, POST, PUT, DELETE, OPTIONS" },
{ "header": "access-control-allow-headers", "operation": "set", "value": "*" }
]
},
"condition": {
"urlFilter": "||localhost:3000/*",
"resourceTypes": ["xmlhttprequest"]
}
}
]Privacy Protection (Remove Tracking Headers)#
[
{
"id": 200,
"priority": 1,
"action": {
"type": "modifyHeaders",
"requestHeaders": [
{ "header": "referer", "operation": "remove" },
{ "header": "x-client-data", "operation": "remove" }
]
},
"condition": {
"urlFilter": "*",
"excludedInitiatorDomains": ["myextension.com"],
"resourceTypes": ["main_frame", "sub_frame", "xmlhttprequest"]
}
}
]1. What is the maximum number of static DNR rules per extension?
2. Which DNR action type can override block rules from OTHER extensions?
3. Can DNR rules inspect or modify request/response bodies?
The declarativeNetRequest API has a learning curve, but it is the foundation of network interception in modern Chrome extensions. For more on building MV3 extensions, see the complete Manifest V3 guide and background service workers deep dive.
Continue reading
Related articles
Debugging Chrome Extensions Like a Pro
Master Chrome extension debugging with DevTools. Debug service workers, content scripts, popups, storage, and network requests with practical techniques and code examples.
The Complete Guide to Manifest V3 in 2026
Everything you need to know about Manifest V3 in 2026: service workers, declarativeNetRequest, storage changes, and migration strategies for Chrome extension developers.
Using WebRTC in Chrome Extensions
A technical guide to implementing WebRTC features in Chrome extensions, covering screen sharing, peer-to-peer data channels, video capture, and signaling via extension messaging.