A Developer's Guide to Chrome Extension Manifest V3 Declarative Net Request API
Learn to build Chrome Manifest V3 network filters, manage dynamic rulesets, and modify HTTP headers using the declarativeNetRequest API.
Join the DZone community and get the full member experience.
Join For FreeGoogle's transition from Manifest V2 to Manifest V3 has been one of the most significant architectural overhauls in the history of browser extension development. For developers building ad blockers, privacy shields, or developer tools, the biggest impact is the deprecation of the blocking capabilities of the chrome.webRequest API.
In its place is the chrome.declarativeNetRequest (DNR) API. Instead of letting extensions intercept and inspect network traffic in real-time, the browser now executes filtering on behalf of the extension using declarative rules.
Understanding how to design, register, and optimize these declarative rules is essential for building modern web-filtering software. Here is a technical breakdown of the DNR API architecture, rule structure, dynamic rule updates, and current platform constraints.
The Architectural Shift: Interception vs. Declaration
In Manifest V2, network filtering occurred within the extension's background page or service worker. The extension registered a listener that executed JavaScript on every request before it was sent:
// The MV2 blocking request pattern (deprecated)
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
if (shouldBlock(details.url)) {
return { cancel: true };
}
},
{ urls: ["<all_urls>"] },
["blocking"]
);
While highly flexible, this design introduced two major problems:
- Performance Overhead: The browser had to pause network requests, spin up the extension's background process, serialize the request metadata, run the extension's custom JavaScript, and wait for a response.
- User Privacy: Extensions required the broad
<all_urls>permission, giving them access to read every request header, URL query parameter, and POST payload.
Manifest V3 solves this by moving the execution engine into the browser itself. The extension defines what needs to be blocked or redirected beforehand. The browser reads these rules and applies them natively during the network stack lifecycle. The extension’s code is never executed during the request, which reduces memory consumption and protects user privacy.
The Anatomy of a Declarative Rule
Under the DNR model, everything is defined using rules. Each rule is a JSON object that specifies an action and the conditions under which that action should execute.
Here is the standard structure of a declarative rule:
{
"id": 1,
"priority": 1,
"action": {
"type": "block"
},
"condition": {
"urlFilter": "||doubleclick.net",
"resourceTypes": ["script", "sub_frame"]
}
}
Every rule requires four primary keys:
id: A unique integer (1 or greater) that identifies the rule.priority: An integer indicating order of execution. Rules with higher priority numbers override lower priority rules.action: Specifies what the browser should do when a match occurs. Valid types includeblock,redirect,allow(bypasses other blocks),allowAllRequests(bypasses all rules on a page), andmodifyHeaders.condition: The criteria that must be met to trigger the action. This can filter by domain, URL pattern, initiator origin, request method, or resource type (such asimage,xmlhttprequest, orstylesheet).
Implementing Static Rulesets
Extensions can bundle pre-defined rule lists within their distribution package. These are defined as static JSON files and declared in the manifest.json:
{
"name": "Custom Focus Blocker",
"version": "1.0",
"manifest_version": 3,
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
"rule_resources": [{
"id": "ruleset_social",
"enabled": true,
"path": "rules/social.json"
}]
}
}
The referenced social.json file contains an array of rules:
[
{
"id": 101,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||facebook.com",
"resourceTypes": ["main_frame"]
}
}
]
Managing Dynamic Rules Programmatically
Static rulesets are read-only once compiled into the extension package. To allow users to add custom blocked domains or configure personal schedules, you must update the extension's dynamic rules at runtime.
Chrome provides chrome.declarativeNetRequest.updateDynamicRules to modify rules programmatically. This method accepts arrays of rules to remove and rules to add.
Here is a JavaScript helper class to manage dynamic site blocking:
class BlocklistManager {
// Add a domain to the dynamic blocklist
static async addDomain(ruleId, domain) {
const newRule = {
id: ruleId,
priority: 1,
action: { type: 'block' },
condition: {
urlFilter: `*://${domain}/*`,
resourceTypes: ['main_frame', 'sub_frame']
}
};
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [ruleId], // Remove old rule with same ID to prevent duplicates
addRules: [newRule]
});
}
// Remove a rule from the active dynamic set
static async removeRule(ruleId) {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [ruleId]
});
}
// Retrieve all currently active dynamic rules
static async getActiveRules() {
return await chrome.declarativeNetRequest.getDynamicRules();
}
}
Session Rules vs. Dynamic Rules
In addition to dynamic rules, Manifest V3 introduces Session Rules via the chrome.declarativeNetRequest.updateSessionRules API.
- Dynamic Rules: Persist across browser restarts and extension updates. They are stored in Chrome's internal extension storage.
- Session Rules: Saved purely in memory. They are cleared when the browser session ends, or the extension is reloaded.
Session rules are ideal for temporary focus sessions, one-time study blocks, or incognito mode rules that should not write data permanently to the disk.
Modifying HTTP Headers
The DNR API also supports modifying HTTP request and response headers natively using the modifyHeaders action. This is useful for removing tracking cookies, injecting authentication tokens, or overriding Referrer headers.
Here is a rule structure that strips the Cookie header from requests sent to a third-party tracking domain:
{
"id": 201,
"priority": 2,
"action": {
"type": "modifyHeaders",
"requestHeaders": [
{
"header": "cookie",
"operation": "remove"
}
]
},
"condition": {
"urlFilter": "||tracker-domain.com",
"resourceTypes": ["xmlhttprequest", "sub_frame"]
}
}
Platform Constraints and Rule Limits
Because the browser must parse and evaluate all active rules in linear time to avoid latency, Google enforces strict limits on the number of rules you can register:
- Static Rulesets: An extension can declare up to 100 static rulesets, but only a limited number can be enabled simultaneously (typically 50).
- Dynamic and Session Rules: Extensions are limited to 5,000 dynamic rules and 5,000 session rules.
- Regex Filter Performance: You can use regular expressions in the
regexFilterkey under conditions, but the regex patterns must conform to a restricted syntax. Lookaheads, lookbehinds, backreferences, and lazy quantifiers are disabled to guarantee that matching runs in linear time. If a regex pattern is too complex, the API will fail to register the rule.
Conclusion and Best Practices
When building extensions under Manifest V3:
- Use Priorities Wisely: Use higher priority values for user-defined whitelists to ensure they override system-level blocklists.
- Minimize Rule Count: Instead of creating separate rules for
sub.domain.comanddomain.com, use wildcard patterns or regex expressions to group matches into single rules. - Optimize Storage: Clean up unused dynamic rule IDs periodically. Retrieve active rules using
getDynamicRules()to prevent collisions.
By moving execution to the browser engine, Manifest V3 requires developers to change their approach to web filtering. Designing within these declarative constraints ensures your extension runs efficiently without compromising user privacy.
Opinions expressed by DZone contributors are their own.
Comments