Реализация перехвата и модификации HTTP-запросов в браузерном расширении
Перехват HTTP-запросов — одна из самых мощных возможностей расширений. Блокировщики рекламы, VPN-расширения, прокси-инструменты, инжекторы заголовков — всё это строится на chrome.webRequest (MV2) или chrome.declarativeNetRequest (MV3). Переход на MV3 кардинально изменил подход.
MV3: declarativeNetRequest
В Manifest V3 динамическая модификация запросов заменена на декларативные правила. Браузер применяет правила сам, без выполнения JS-кода расширения. Это быстрее и безопаснее, но менее гибко.
{
"manifest_version": 3,
"permissions": ["declarativeNetRequest", "declarativeNetRequestWithHostAccess"],
"host_permissions": ["<all_urls>"],
"declarative_net_request": {
"rule_resources": [
{
"id": "static-rules",
"enabled": true,
"path": "rules/static.json"
}
]
}
}
Статические правила
Файл rules/static.json содержит массив правил, применяемых постоянно:
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "||analytics.example.com^",
"resourceTypes": ["script", "xmlhttprequest", "image"]
}
},
{
"id": 2,
"priority": 2,
"action": {
"type": "modifyHeaders",
"requestHeaders": [
{ "header": "X-Custom-Token", "operation": "set", "value": "my-token" },
{ "header": "Referer", "operation": "remove" }
]
},
"condition": {
"urlFilter": "https://api.internal.corp/*",
"resourceTypes": ["xmlhttprequest"]
}
},
{
"id": 3,
"priority": 1,
"action": {
"type": "redirect",
"redirect": { "regexSubstitution": "https://cdn.example.com\\1" }
},
"condition": {
"regexFilter": "^https://slow-cdn\\.com(.*)",
"resourceTypes": ["image", "media", "font"]
}
}
]
Лимиты: максимум 30 000 статических правил суммарно по всем файлам, 5 000 включённых правил одновременно (можно менять динамически).
Динамические правила из service worker
Правила можно добавлять и удалять в рантайме:
// background/sw.js
async function blockDomain(domain) {
const existingRules = await chrome.declarativeNetRequest.getDynamicRules();
const maxId = existingRules.reduce((max, r) => Math.max(max, r.id), 0);
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: maxId + 1,
priority: 10,
action: { type: 'block' },
condition: {
urlFilter: `||${domain}^`,
resourceTypes: [
'main_frame', 'sub_frame', 'script', 'stylesheet',
'image', 'xmlhttprequest', 'other'
]
}
}],
removeRuleIds: []
});
}
async function unblockDomain(domain) {
const rules = await chrome.declarativeNetRequest.getDynamicRules();
const toRemove = rules
.filter(r => r.condition.urlFilter === `||${domain}^`)
.map(r => r.id);
if (toRemove.length > 0) {
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [],
removeRuleIds: toRemove
});
}
}
// Модификация заголовков с динамическим значением
async function setAuthHeader(apiUrl, token) {
const rules = await chrome.declarativeNetRequest.getDynamicRules();
const existingRule = rules.find(r =>
r.condition.urlFilter === apiUrl && r.action.type === 'modifyHeaders'
);
const newRule = {
id: existingRule?.id ?? (rules.reduce((m, r) => Math.max(m, r.id), 0) + 1),
priority: 5,
action: {
type: 'modifyHeaders',
requestHeaders: [
{ header: 'Authorization', operation: 'set', value: `Bearer ${token}` }
]
},
condition: {
urlFilter: apiUrl,
resourceTypes: ['xmlhttprequest', 'fetch']
}
};
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [newRule],
removeRuleIds: existingRule ? [existingRule.id] : []
});
}
Наблюдение за запросами: chrome.webRequest (только MV2)
В MV2 можно было перехватывать запросы синхронно и модифицировать их в JS. В MV3 chrome.webRequest доступен только в режиме наблюдения (без блокировки):
// MV2 или MV3 (только чтение, без blocking)
chrome.webRequest.onCompleted.addListener(
(details) => {
if (details.statusCode >= 400) {
logFailedRequest({
url: details.url,
status: details.statusCode,
tabId: details.tabId,
timestamp: details.timeStamp
});
}
},
{ urls: ['https://api.your-service.com/*'] },
['responseHeaders']
);
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
// В MV3 — только наблюдение, без blocking
analytics.track('request', {
url: new URL(details.url).pathname,
method: details.method,
tabId: details.tabId
});
},
{ urls: ['<all_urls>'], types: ['xmlhttprequest'] }
);
Перенаправление запросов через declarativeNetRequest
Сложные редиректы с подстановкой:
// Перенаправить запросы к staging на production
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 100,
priority: 1,
action: {
type: 'redirect',
redirect: {
regexSubstitution: 'https://api.production.com\\1'
}
},
condition: {
regexFilter: '^https://api\\.staging\\.com(.*)',
resourceTypes: ['xmlhttprequest', 'fetch']
}
}],
removeRuleIds: []
});
Инъекция заголовков ответа
С разрешением declarativeNetRequestWithHostAccess можно изменять заголовки ответа — например, убирать CORS-ограничения для разработки:
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: 200,
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: 'X-Frame-Options', operation: 'remove' }
]
},
condition: {
urlFilter: 'https://api.internal.corp/*',
resourceTypes: ['xmlhttprequest']
}
}],
removeRuleIds: []
});
Отладка правил
// Проверить, какие правила применились к последним запросам
const matched = await chrome.declarativeNetRequest.getMatchedRules({
tabId: tab.id,
minTimeStamp: Date.now() - 60000
});
console.log('Применённые правила:', matched.rulesMatchedInfo);
// Проверить, сработает ли правило для конкретного URL
const result = await chrome.declarativeNetRequest.testMatchOutcome({
url: 'https://analytics.example.com/track',
type: 'xmlhttprequest',
method: 'GET',
tabId: -1,
initiator: 'https://example.com'
});
console.log('Результат:', result.matchedRule); // null или объект правила
Ограничения MV3 и обходные пути
Нельзя модифицировать тело запроса через declarativeNetRequest. Для этого нужен content script, который перехватывает fetch/XMLHttpRequest через monkey-patching в world: 'MAIN':
// content script с world: 'MAIN'
const originalFetch = window.fetch;
window.fetch = async function(input, init = {}) {
const url = typeof input === 'string' ? input : input.url;
if (url.includes('api.target.com')) {
init.headers = {
...init.headers,
'X-Injected-Header': 'value',
};
// Модифицируем тело
if (init.body) {
const body = JSON.parse(init.body);
body.extraField = 'injected';
init.body = JSON.stringify(body);
}
}
return originalFetch.call(this, input, init);
};
Это работает, но имеет ограничение: monkey-patching не затрагивает запросы из worker-потоков страницы и не перехватывает WebSocket.
Для расширений, которым нужна полная власть над трафиком (корпоративные прокси, инструменты безопасности), MV2 ещё доступен в корпоративной политике Chrome, но не будет поддерживаться в публичном Chrome Web Store.







