Development11 min read

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.

C
CWS Kit Team
Share
🌐

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).

FeatureAspectwebRequest (MV2)declarativeNetRequest (MV3)
Execution modelJavaScript callback per requestPre-compiled rules in browser engine
Performance impactService worker wakeup per requestNear-zero — native C++ matching
PrivacyExtension sees full request dataExtension never sees request content
Rule limitUnlimited (code-based)Static: 300K, Dynamic: 30K, Session: 5K
PersistenceRequires service worker runningRules active even when SW inactive
Request body accessYes — can read POST bodiesNo — cannot inspect request bodies
Async decisionYes — can defer blocking decisionNo — rules are evaluated synchronously
Header readingCan read all headersCan 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.

The simplest rule type. Matches a URL pattern and prevents the request from completing. The browser returns a network error as if the server was unreachable. Used for ad blocking, tracker blocking, and malware protection. Block rules are the most common — the EasyList ad filter set translates to roughly 70,000 block rules.

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 URL

For 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.

1

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.

2

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.

3

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.

4

Convert Header Modifications

Map setRequestHeader/setResponseHeader calls to modifyHeaders rules. Remember: DNR can set, append, or remove headers but cannot read existing header values.

5

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.

6

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"]
    }
  }
]
Knowledge Check

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

View all posts