Chrome Extension Cross Origin Requests — Best Practices

7 min read

Cross-Origin Request Patterns for Chrome Extensions

This guide covers making cross-origin HTTP requests from Chrome extensions, including permission configuration, content script limitations, and robust request handling patterns.

Extension Privileges vs Content Scripts

Chrome extensions have different CORS capabilities depending on where the code runs:

Host Permissions Configuration

Declare required host permissions in your manifest:

{
  "manifest_version": 3,
  "permissions": ["storage"],
  "host_permissions": [
    "https://api.example.com/*",
    "https://*.external-service.com/*"
  ]
}

Use <all_urls> sparingly—it grants access to all websites:

"host_permissions": ["<all_urls>"]

Optional Host Permissions

Request permissions on-demand for extensions in the Chrome Web Store:

async function requestHostPermission(host) {
  const granted = await chrome.permissions.request({
    origins: [host]
  });
  return granted;
}

Fetch from Service Worker

The background service worker uses the standard Fetch API with no CORS restrictions when proper host permissions are declared:

async function fetchFromBackground(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    credentials: 'include' // For cookies
  });
  
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
  }
  
  return response.json();
}

Access cookies in cross-origin requests using the credentials option:

async function fetchWithCookies(url) {
  return fetch(url, {
    credentials: 'include' // Sends cookies for the target origin
  });
}

For explicit cookie control, use the Cookies API:

async function setCookie(url, name, value) {
  const { origin, pathname } = new URL(url);
  await chrome.cookies.set({
    url: origin,
    name: name,
    value: value,
    path: pathname
  });
}

Error Handling Patterns

Implement robust error handling for network failures, HTTP errors, and timeouts:

async function fetchWithTimeout(url, options = {}, timeout = 10000) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);
  
  try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    
    if (!response.ok) {
      throw new HttpError(response.status, await response.text());
    }
    
    return response;
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error('Request timeout');
    }
    throw error;
  } finally {
    clearTimeout(timeoutId);
  }
}

class HttpError extends Error {
  constructor(status, body) {
    super(`HTTP ${status}`);
    this.status = status;
    this.body = body;
  }
}

Caching Responses

Cache API responses using the Cache API or chrome.storage for offline support:

const CACHE_NAME = 'api-cache-v1';

async function fetchWithCache(url, options = {}) {
  const cache = await caches.open(CACHE_NAME);
  const cached = await cache.match(url);
  
  if (cached && isFresh(cached)) {
    return cached.json();
  }
  
  const response = await fetch(url, options);
  
  if (response.ok) {
    cache.put(url, response.clone());
  }
  
  return response.json();
}

function isFresh(response) {
  const date = new Date(response.headers.get('date'));
  return (Date.now() - date.getTime()) < 3600000; // 1 hour
}

Rate Limiting

Implement rate limiting to avoid API bans:

const rateLimiter = {
  requests: [],
  maxRequests: 10,
  windowMs: 60000,
  
  async canProceed() {
    const now = Date.now();
    this.requests = this.requests.filter(t => now - t < this.windowMs);
    
    if (this.requests.length >= this.maxRequests) {
      const waitTime = this.windowMs - (now - this.requests[0]);
      await new Promise(r => setTimeout(r, waitTime));
      return this.canProceed();
    }
    
    this.requests.push(now);
    return true;
  }
};

Request Queuing

Queue sequential API calls to maintain order:

class RequestQueue {
  constructor() {
    this.queue = Promise.resolve();
  }
  
  async enqueue(fn) {
    return this.queue = this.queue.then(fn);
  }
}

const apiQueue = new RequestQueue();

async function queuedFetch(url) {
  return apiQueue.enqueue(() => fetch(url).then(r => r.json()));
}

Authentication Headers

Add Bearer tokens or API keys to requests:

async function fetchWithAuth(url, token, options = {}) {
  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`,
      'X-API-Key': API_KEY
    }
  });
}

Content Script Proxy Pattern

Content scripts must relay requests through the background:

// Content script
async function fetchFromContentScript(url) {
  return new Promise((resolve, reject) => {
    chrome.runtime.sendMessage({ type: 'FETCH', url }, response => {
      if (response.error) reject(new Error(response.error));
      else resolve(response.data);
    });
  });
}

// Background service worker
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'FETCH') {
    fetch(message.url)
      .then(data => sendResponse({ data }))
      .catch(error => sendResponse({ error: error.message }));
    return true; // Async response
  }
});

See Also

Part of the Chrome Extension Guide by theluckystrike. Built at zovo.one.