Build an API Request Interceptor Chrome Extension

27 min read

Build an API Request Interceptor Chrome Extension

Build an API Request Interceptor Chrome Extension

API request interception represents one of the most valuable capabilities available to Chrome extension developers. Whether you need to debug API calls, modify request headers for testing, simulate different server responses, or build developer tools, understanding how to intercept and manipulate HTTP requests is essential. This comprehensive guide walks you through building a complete API request interceptor extension using modern Chrome extension APIs.


Understanding API Interception in Chrome Extensions

Chrome extensions can intercept, analyze, and modify network requests through several APIs, each with different capabilities and limitations. The primary approaches include the DeclarativeNetRequest API for Manifest V3 extensions, the Web Request API for legacy extensions, and content script-based interception for specific page-level scenarios.

The DeclarativeNetRequest API serves as the modern, privacy-focused approach to network request manipulation. It uses a declarative rule-based system where you define rules in JSON format, and Chrome evaluates these rules internally without exposing sensitive request data to your extension code. This approach provides excellent privacy guarantees while maintaining powerful request blocking and modification capabilities.

The Web Request API offers more granular control over network requests, allowing you to observe, block, or modify requests in flight with full access to request and response details. However, due to privacy concerns, Google restricted this API in Manifest V3, requiring specific justification for its use in extensions published to the Chrome Web Store.

For most use cases, DeclarativeNetRequest provides the best balance of functionality and compliance. However, understanding both APIs enables you to choose the right approach for your specific requirements.


Setting Up Your Extension Project

Before implementing API interception functionality, you need to set up a proper Chrome extension project structure. Create a new directory for your extension and add the necessary configuration files.

Project Structure

Create the following directory structure for your API interceptor extension:

api-interceptor/
├── manifest.json
├── background.js
├── popup/
│   ├── popup.html
│   └── popup.js
├── rules/
│   └── rules.json
└── icons/
    ├── icon16.png
    ├── icon48.png
    └── icon128.png

Manifest Configuration

Your manifest.json defines the extension’s permissions and capabilities. For API interception, you need to declare the appropriate permissions:

{
  "manifest_version": 3,
  "name": "API Request Interceptor",
  "version": "1.0",
  "description": "Intercept and modify HTTP requests with ease",
  "permissions": [
    "declarativeNetRequest",
    "declarativeNetRequestWithHostAccess",
    "storage"
  ],
  "host_permissions": [
    "<all_urls>"
  ],
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "background": {
    "service_worker": "background.js"
  }
}

The declarativeNetRequestWithHostAccess permission allows your extension to modify requests to websites while maintaining the privacy benefits of the declarative approach. The storage permission enables saving user preferences and interception rules between sessions.


Implementing the DeclarativeNetRequest API

DeclarativeNetRequest uses a rule-based system where you define conditions and actions. Rules are evaluated by Chrome internally, ensuring that sensitive request data remains private while still enabling powerful request manipulation.

Understanding Rule Structure

Each rule consists of an ID, priority, action, and condition. The condition determines when the rule applies, while the action defines what happens when conditions are met:

{
  "id": 1,
  "priority": 1,
  "action": {
    "type": "modifyHeaders",
    "requestHeaders": [
      { "header": "X-Custom-Header", "operation": "set", "value": "intercepted" }
    ]
  },
  "condition": {
    "urlFilter": "api.example.com",
    "resourceTypes": ["xmlhttprequest", "fetch"]
  }
}

Creating Interception Rules

Create a rules/rules.json file with various rule types to demonstrate different interception capabilities:

[
  {
    "id": 1,
    "priority": 1,
    "action": {
      "type": "block"
    },
    "condition": {
      "urlFilter": ".*analytics\\.tracker\\.com.*",
      "resourceTypes": ["script", "image", "beacon"]
    }
  },
  {
    "id": 2,
    "priority": 1,
    "action": {
      "type": "modifyHeaders",
      "requestHeaders": [
        { "header": "X-Debug-Mode", "operation": "set", "value": "true" },
        { "header": "X-Request-Interceptor", "operation": "set", "value": "active" }
      ]
    },
    "condition": {
      "urlFilter": ".*api\\.example\\.com/.*",
      "resourceTypes": ["xmlhttprequest", "fetch"]
    }
  },
  {
    "id": 3,
    "priority": 1,
    "action": {
      "type": "redirect",
      "redirect": {
        "url": "https://api.staging.example.com/alternative"
      }
    },
    "condition": {
      "urlFilter": ".*api\\.production\\.example\\.com/.*",
      "resourceTypes": ["xmlhttprequest", "fetch"]
    }
  },
  {
    "id": 4,
    "priority": 1,
    "action": {
      "type": "allow"
    },
    "condition": {
      "urlFilter": ".*api\\.example\\.com/health.*",
      "resourceTypes": ["xmlhttprequest"]
    }
  }
]

These rules demonstrate four fundamental interception capabilities: blocking unwanted requests, modifying request headers, redirecting requests to different URLs, and allowing specific requests to bypass other rules.


Implementing the Background Service Worker

The background service worker handles rule management and communicates with other extension components. It loads rules on startup, allows dynamic rule updates, and provides an interface for the popup to manage interception settings.

Background Script Implementation

Create the background.js file with comprehensive rule management functionality:

// background.js - Service Worker for API Request Interceptor

const RULE_FILE = 'rules/rules.json';
let currentRules = [];
let isInterceptionEnabled = true;

// Load and update rules when the extension starts
chrome.runtime.onInstalled.addListener(async () => {
  console.log('API Interceptor extension installed');
  await loadRules();
});

// Load rules from the rules file
async function loadRules() {
  try {
    const response = await fetch(chrome.runtime.getURL(RULE_FILE));
    const rules = await response.json();
    await updateRules(rules);
  } catch (error) {
    console.error('Failed to load rules:', error);
  }
}

// Update declarativeNetRequest rules
async function updateRules(rules) {
  try {
    // First, remove any existing rules
    await chrome.declarativeNetRequest.updateSessionRules({
      removeRuleIds: currentRules.map(r => r.id)
    });
    
    // Then add new rules
    if (isInterceptionEnabled && rules.length > 0) {
      await chrome.declarativeNetRequest.updateSessionRules({
        addRules: rules
      });
      currentRules = rules;
    }
    
    console.log(`Loaded ${rules.length} interception rules`);
    return { success: true, ruleCount: rules.length };
  } catch (error) {
    console.error('Failed to update rules:', error);
    return { success: false, error: error.message };
  }
}

// Toggle interception on/off
async function toggleInterception(enabled) {
  isInterceptionEnabled = enabled;
  
  if (enabled) {
    await updateRules(currentRules);
  } else {
    await chrome.declarativeNetRequest.updateSessionRules({
      removeRuleIds: currentRules.map(r => r.id)
    });
  }
  
  // Notify popup of state change
  chrome.runtime.sendMessage({
    type: 'INTERCEPTION_STATE_CHANGED',
    enabled: isInterceptionEnabled
  });
}

// Handle messages from popup and content scripts
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  switch (message.type) {
    case 'GET_STATE':
      sendResponse({
        enabled: isInterceptionEnabled,
        ruleCount: currentRules.length
      });
      break;
      
    case 'UPDATE_RULES':
      updateRules(message.rules).then(result => sendResponse(result));
      return true; // Keep channel open for async response
      
    case 'TOGGLE_INTERCEPTION':
      toggleInterception(message.enabled).then(() => {
        sendResponse({ success: true });
      });
      return true;
      
    case 'GET_MATCHED_REQUESTS':
      // Query recent matched requests (Manifest V3 feature)
      chrome.declarativeNetRequest.getMatchedRules({
        URLFilter: message.urlFilter || undefined
      }).then(result => {
        sendResponse({ rules: result.rules });
      });
      return true;
  }
});

// Log rule matching events for debugging (requires declarativeNetRequestFeedback)
chrome.declarativeNetRequest.onRuleMatchedDebug.addListener((info) => {
  console.log('Rule matched:', info);
  
  // Store matched requests for display in popup
  chrome.storage.local.get(['matchedRequests'], (result) => {
    const requests = result.matchedRequests || [];
    requests.unshift({
      timestamp: Date.now(),
      url: info.request.url,
      ruleId: info.rule.ruleId
    });
    
    // Keep only last 100 matched requests
    const trimmed = requests.slice(0, 100);
    chrome.storage.local.set({ matchedRequests: trimmed });
  });
});

This background service worker provides comprehensive rule management, state toggling, and matched request logging capabilities.


The popup provides users with a graphical interface to manage interception rules and view matched requests.

Create the popup/popup.html file:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>API Interceptor</title>
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      width: 350px;
      padding: 16px;
      background: #f5f5f5;
    }
    
    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      margin-bottom: 16px;
      padding-bottom: 12px;
      border-bottom: 1px solid #e0e0e0;
    }
    
    .header h1 {
      font-size: 16px;
      color: #333;
    }
    
    .toggle-container {
      display: flex;
      align-items: center;
      gap: 8px;
    }
    
    .toggle-label {
      font-size: 12px;
      color: #666;
    }
    
    .toggle {
      position: relative;
      width: 44px;
      height: 24px;
    }
    
    .toggle input {
      opacity: 0;
      width: 0;
      height: 0;
    }
    
    .toggle-slider {
      position: absolute;
      cursor: pointer;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background-color: #ccc;
      transition: 0.3s;
      border-radius: 24px;
    }
    
    .toggle-slider:before {
      position: absolute;
      content: "";
      height: 18px;
      width: 18px;
      left: 3px;
      bottom: 3px;
      background-color: white;
      transition: 0.3s;
      border-radius: 50%;
    }
    
    .toggle input:checked + .toggle-slider {
      background-color: #4CAF50;
    }
    
    .toggle input:checked + .toggle-slider:before {
      transform: translateX(20px);
    }
    
    .section {
      background: white;
      border-radius: 8px;
      padding: 12px;
      margin-bottom: 12px;
      box-shadow: 0 1px 3px rgba(0,0,0,0.1);
    }
    
    .section h2 {
      font-size: 14px;
      color: #333;
      margin-bottom: 12px;
    }
    
    .stats {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 12px;
    }
    
    .stat {
      text-align: center;
      padding: 8px;
      background: #f9f9f9;
      border-radius: 6px;
    }
    
    .stat-value {
      font-size: 24px;
      font-weight: bold;
      color: #4CAF50;
    }
    
    .stat-label {
      font-size: 11px;
      color: #666;
    }
    
    .matched-list {
      max-height: 200px;
      overflow-y: auto;
    }
    
    .matched-item {
      padding: 8px;
      border-bottom: 1px solid #eee;
      font-size: 11px;
    }
    
    .matched-item:last-child {
      border-bottom: none;
    }
    
    .matched-url {
      color: #333;
      word-break: break-all;
    }
    
    .matched-time {
      color: #999;
      font-size: 10px;
    }
    
    .empty-state {
      text-align: center;
      color: #999;
      padding: 20px;
      font-size: 12px;
    }
    
    .status {
      display: flex;
      align-items: center;
      gap: 8px;
      padding: 8px;
      background: #e8f5e9;
      border-radius: 6px;
      margin-bottom: 12px;
    }
    
    .status-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: #4CAF50;
    }
    
    .status.inactive {
      background: #ffebee;
    }
    
    .status.inactive .status-dot {
      background: #f44336;
    }
    
    .status-text {
      font-size: 12px;
      color: #333;
    }
  </style>
</head>
<body>
  <div class="header">
    <h1>API Request Interceptor</h1>
    <div class="toggle-container">
      <span class="toggle-label">Active</span>
      <label class="toggle">
        <input type="checkbox" id="interceptionToggle" checked>
        <span class="toggle-slider"></span>
      </label>
    </div>
  </div>
  
  <div class="status" id="status">
    <div class="status-dot"></div>
    <span class="status-text" id="statusText">Interception Active</span>
  </div>
  
  <div class="section">
    <h2>Statistics</h2>
    <div class="stats">
      <div class="stat">
        <div class="stat-value" id="ruleCount">0</div>
        <div class="stat-label">Active Rules</div>
      </div>
      <div class="stat">
        <div class="stat-value" id="matchedCount">0</div>
        <div class="stat-label">Matched Requests</div>
      </div>
    </div>
  </div>
  
  <div class="section">
    <h2>Recent Matched Requests</h2>
    <div class="matched-list" id="matchedList">
      <div class="empty-state">No requests intercepted yet</div>
    </div>
  </div>
  
  <script src="popup.js"></script>
</body>
</html>

Create the popup/popup.js file to handle user interactions:

// popup.js - Popup script for API Interceptor

document.addEventListener('DOMContentLoaded', async () => {
  const toggle = document.getElementById('interceptionToggle');
  const status = document.getElementById('status');
  const statusText = document.getElementById('statusText');
  const ruleCount = document.getElementById('ruleCount');
  const matchedCount = document.getElementById('matchedCount');
  const matchedList = document.getElementById('matchedList');
  
  // Load initial state
  async function loadState() {
    try {
      const response = await chrome.runtime.sendMessage({ type: 'GET_STATE' });
      
      toggle.checked = response.enabled;
      ruleCount.textContent = response.ruleCount;
      
      updateStatus(response.enabled);
      await loadMatchedRequests();
    } catch (error) {
      console.error('Failed to load state:', error);
    }
  }
  
  // Update status display
  function updateStatus(enabled) {
    if (enabled) {
      status.classList.remove('inactive');
      statusText.textContent = 'Interception Active';
    } else {
      status.classList.add('inactive');
      statusText.textContent = 'Interception Disabled';
    }
  }
  
  // Load matched requests from storage
  async function loadMatchedRequests() {
    try {
      const result = await chrome.storage.local.get(['matchedRequests']);
      const requests = result.matchedRequests || [];
      
      matchedCount.textContent = requests.length;
      
      if (requests.length === 0) {
        matchedList.innerHTML = '<div class="empty-state">No requests intercepted yet</div>';
        return;
      }
      
      matchedList.innerHTML = requests.map(req => `
        <div class="matched-item">
          <div class="matched-url">${escapeHtml(req.url)}</div>
          <div class="matched-time">${formatTime(req.timestamp)}</div>
        </div>
      `).join('');
    } catch (error) {
      console.error('Failed to load matched requests:', error);
    }
  }
  
  // Toggle interception
  toggle.addEventListener('change', async () => {
    const enabled = toggle.checked;
    
    try {
      await chrome.runtime.sendMessage({
        type: 'TOGGLE_INTERCEPTION',
        enabled: enabled
      });
      
      updateStatus(enabled);
    } catch (error) {
      console.error('Failed to toggle interception:', error);
      toggle.checked = !enabled;
    }
  });
  
  // Utility functions
  function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
  }
  
  function formatTime(timestamp) {
    const date = new Date(timestamp);
    return date.toLocaleTimeString();
  }
  
  // Initialize
  await loadState();
  
  // Refresh matched requests periodically
  setInterval(loadMatchedRequests, 2000);
});

Advanced Interception Techniques

Beyond basic request blocking and header modification, you can implement more sophisticated interception patterns for complex use cases.

Dynamic Rule Generation

For applications requiring runtime rule generation based on user configuration, implement dynamic rule creation in your background script:

// Dynamic rule generation based on user configuration
async function generateDynamicRules(config) {
  const rules = [];
  let ruleId = 1;
  
  // Generate blocking rules for blacklisted domains
  config.blockedDomains.forEach(domain => {
    rules.push({
      id: ruleId++,
      priority: 1,
      action: { type: 'block' },
      condition: {
        urlFilter: `.*${escapeRegex(domain)}.*`,
        resourceTypes: ['script', 'image', 'sub_frame', 'xmlhttprequest']
      }
    });
  });
  
  // Generate header modification rules
  config.headerModifications.forEach(mod => {
    rules.push({
      id: ruleId++,
      priority: 1,
      action: {
        type: 'modifyHeaders',
        requestHeaders: [
          { header: mod.header, operation: mod.operation, value: mod.value }
        ]
      },
      condition: {
        urlFilter: mod.urlPattern,
        resourceTypes: ['xmlhttprequest', 'fetch']
      }
    });
  });
  
  // Generate redirect rules
  config.redirects.forEach(redirect => {
    rules.push({
      id: ruleId++,
      priority: 1,
      action: {
        type: 'redirect',
        redirect: { url: redirect.to }
      },
      condition: {
        urlFilter: redirect.from,
        resourceTypes: ['xmlhttprequest', 'fetch', 'main_frame']
      }
    });
  });
  
  return rules;
}

function escapeRegex(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

Request and Response Logging

Implement comprehensive logging for debugging and analytics:

// Enhanced logging with request/response details
chrome.declarativeNetRequest.onRuleMatchedDebug.addListener(async (info) => {
  const logEntry = {
    timestamp: Date.now(),
    request: {
      url: info.request.url,
      method: info.request.method,
      type: info.request.type,
      frameId: info.request.frameId,
      tabId: info.request.tabId
    },
    matchedRule: {
      id: info.rule.ruleId,
      priority: info.rule.priority
    }
  };
  
  // Store log entry
  const result = await chrome.storage.local.get(['requestLogs']);
  const logs = result.requestLogs || [];
  logs.unshift(logEntry);
  
  // Keep last 500 log entries
  const trimmed = logs.slice(0, 500);
  await chrome.storage.local.set({ requestLogs: trimmed });
  
  // Notify popup if open
  chrome.runtime.sendMessage({
    type: 'NEW_REQUEST_LOG',
    entry: logEntry
  });
});

Using Web Request API for Complex Scenarios

For scenarios requiring access to request bodies or response content, you may need to use the Web Request API with appropriate manifest declarations:

{
  "permissions": [
    "webRequest",
    "webRequestBlocking"
  ],
  "host_permissions": [
    "<all_urls>"
  ]
}
// Web Request API implementation for complex interception
chrome.webRequest.onBeforeRequest.addListener(
  (details) => {
    // Analyze request body if available
    if (details.requestBody) {
      console.log('Request body:', details.requestBody);
    }
    
    // Redirect based on conditions
    if (shouldRedirect(details.url)) {
      return {
        redirectUrl: getRedirectUrl(details.url)
      };
    }
    
    // Block specific requests
    if (shouldBlock(details.url)) {
      return { cancel: true };
    }
  },
  {
    urls: ['<all_urls>'],
    types: ['xmlhttprequest', 'fetch']
  },
  ['requestBody', 'blocking']
);

chrome.webRequest.onBeforeSendHeaders.addListener(
  (details) => {
    // Modify headers
    const requestHeaders = details.requestHeaders || [];
    
    // Add custom header
    requestHeaders.push({
      name: 'X-Interception-Active',
      value: 'true'
    });
    
    // Remove headers
    const filtered = requestHeaders.filter(
      h => h.name !== 'X-Removed-Header'
    );
    
    return { requestHeaders: filtered };
  },
  {
    urls: ['<all_urls>']
  },
  ['requestHeaders', 'blocking']
);

Testing Your Extension

Proper testing ensures your interceptor works correctly across different scenarios.

Loading Your Extension

  1. Open Chrome and navigate to chrome://extensions/
  2. Enable “Developer mode” using the toggle in the top right
  3. Click “Load unpacked” and select your extension directory
  4. The extension icon should appear in your toolbar

Testing Interception Rules

Use the following approach to verify your rules work correctly:

// Test your extension programmatically
async function testInterception() {
  // 1. Check if rules are loaded
  const rules = await chrome.declarativeNetRequest.getSessionRules();
  console.log('Loaded rules:', rules);
  
  // 2. Enable interception
  await chrome.runtime.sendMessage({
    type: 'TOGGLE_INTERCEPTION',
    enabled: true
  });
  
  // 3. Make a test request
  const testUrl = 'https://api.example.com/test';
  await fetch(testUrl);
  
  // 4. Check matched requests
  const matched = await chrome.declarativeNetRequest.getMatchedRules({});
  console.log('Matched rules:', matched);
}

Debugging Tips

When developing interception extensions, keep these debugging strategies in mind:

Use the Chrome DevTools Network panel to verify requests are being intercepted. Look for modified headers and blocked requests. The extension service worker console provides logging output from your background script. The chrome://extensions page shows any errors in your extension’s background page. Use the Declarative Net Request Internals page (chrome://net-internals/#declarativeNet) to inspect rule evaluation.


Best Practices and Considerations

When building API interceptor extensions, follow these guidelines for optimal results.

Permission Management

Request only the minimum permissions necessary for your extension’s functionality. Use declarativeNetRequestWithHostAccess instead of broader permissions when possible. Clearly explain to users why your extension needs network access. Consider implementing optional host permissions for specific domains rather than requesting <all_urls> access.

Performance Optimization

Keep your rule sets small and efficient. Use URL filters that are as specific as possible to reduce evaluation time. Avoid complex regex patterns that may slow down request processing. Consider using the isUrlFilterCaseSensitive option when appropriate.

Privacy and Security

Never log or transmit sensitive user data without explicit consent. Store interception data locally rather than sending it to external servers. Implement user controls for what data gets logged. Provide clear privacy policies explaining data handling practices.

User Experience

Provide clear visual feedback when interception is active. Allow users to easily enable and disable interception. Offer whitelisting for domains that should not be intercepted. Include undo functionality for destructive operations like rule deletion.


Conclusion

Building an API request interceptor Chrome extension provides powerful capabilities for developers and users alike. Through this guide, you have learned how to implement comprehensive request interception using the DeclarativeNetRequest API, create dynamic rule management systems, build user-friendly popup interfaces, and follow best practices for privacy and performance.

The techniques covered here enable you to build developer tools, API testing utilities, privacy enhancers, and content filters. As Chrome continues to evolve its extension platform, the declarative approach ensures your extensions remain compliant while maintaining robust functionality.

Experiment with the examples provided, extend them with additional features, and adapt them to your specific use cases. The foundation you have built understanding API interception will serve as a valuable skill for countless Chrome extension projects.

No previous article
No next article