Cross-Origin Requests in Chrome Extensions: Complete CORS & Fetch API Guide
When building Chrome extensions that interact with external APIs or fetch data from third-party servers, understanding cross-origin requests is essential. Unlike regular web applications, Chrome extensions have a unique permission model that determines how and from which contexts you can make network requests. This guide covers everything you need to know about handling cross-origin requests, CORS, and the Fetch API in Chrome extensions built with Manifest V3.
Understanding Cross-Origin Requests in Chrome Extensions
Cross-origin requests occur when a web page or extension makes a request to a domain different from the one that served the original page. In regular web development, the Same-Origin Policy (SOP) restricts these requests for security reasons. However, Chrome extensions operate in a special security context that provides more flexibility while still maintaining strict controls.
Chrome extensions consist of several distinct contexts: the background service worker, popup pages, options pages, and content scripts. Each of these contexts has different capabilities when it comes to making cross-origin requests. The background service worker and extension pages enjoy privileged access that bypasses many CORS restrictions, while content scripts are more limited because they run in the context of web pages.
This distinction is crucial for extension developers. Making network requests from the wrong context can lead to frustrating errors, failed API calls, and rejected Chrome Web Store submissions. By understanding how each extension context handles cross-origin requests, you can architect your extension to work reliably across all scenarios.
Host Permissions: The Foundation of Cross-Origin Access
Host permissions are the primary mechanism controlling which external resources your Chrome extension can access. These permissions are declared in the host_permissions field of your manifest.json file and determine the scope of network access available to your extension.
Declaring Host Permissions
To allow your extension to make requests to specific domains, you must declare the appropriate host permissions:
{
"manifest_version": 3,
"name": "My API Extension",
"version": "1.0",
"host_permissions": [
"https://api.example.com/*",
"https://*.github.com/*",
"https://*.googleapis.com/*"
]
}
Each permission pattern follows a specific format. The https://api.example.com/* pattern grants access to all paths on that specific domain. Using wildcards like https://*.google.com/* allows access to multiple subdomains, which is useful when working with APIs that span different services.
The All-Urls Permission
You can also use the special <all_urls> permission to request access to all websites:
{
"host_permissions": [
"<all_urls>"
]
}
However, this broad permission comes with significant drawbacks. When users install your extension, they will see a warning that the extension “can read and change all your data on all websites.” This frightening message can reduce installation rates significantly. Additionally, the Chrome Web Store review team may scrutinize extensions requesting this permission more closely, and overly broad permissions can affect your extension’s approval process.
Best practice dictates being as specific as possible with host permissions. Only request access to the exact domains your extension needs. If your extension only communicates with one API, declare just that domain. This follows the security principle of least privilege and gives users confidence in installing your extension.
How CORS Works in Chrome Extensions
Cross-Origin Resource Sharing (CORS) behaves differently in Chrome extensions compared to regular web pages. Understanding these differences is crucial for building extensions that work reliably.
Extension Context vs. Web Page Context
In extension contexts (background service worker, popup, options page), cross-origin requests bypass standard CORS restrictions when the appropriate host permissions are granted. This means you can make requests to external APIs without encountering the typical CORS errors that plague regular web applications.
This privileged behavior exists because extension contexts are considered more trustworthy than arbitrary web pages. Extensions are installed deliberately by users who have granted specific permissions, and they can be managed or removed through Chrome’s extension management interface.
However, this CORS relaxation does not extend to content scripts. Content scripts run in the context of web pages, which means they inherit the page’s origin for network requests. When a content script makes a fetch request, it is subject to the same CORS restrictions as any JavaScript running on that page.
The Service Worker as a Bridge
This architectural difference leads to a common pattern in Chrome extension development. Content scripts should not make direct fetch calls to external APIs. Instead, they communicate with the background service worker, which then makes the cross-origin request on behalf of the extension:
// Content script - sends request to service worker
chrome.runtime.sendMessage(
{ action: "fetchData", url: "https://api.example.com/users" },
(response) => {
if (response.error) {
console.error("API Error:", response.error);
return;
}
console.log("User data:", response.data);
}
);
// Background service worker - makes the actual API call
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === "fetchData") {
fetch(message.url)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => sendResponse({ data: data }))
.catch(error => sendResponse({ error: error.message }));
return true; // Keep message channel open for async response
}
});
This messaging pattern keeps your sensitive API logic in the privileged extension context while allowing content scripts to trigger network requests when needed.
Using the Fetch API in Chrome Extensions
The Fetch API works seamlessly in Chrome extensions, but there are important considerations depending on where you use it.
Fetch from Background Service Worker
The background service worker is the recommended location for making cross-origin requests. With appropriate host permissions, fetch calls work just like in regular JavaScript:
// background.js - Service worker context
async function fetchUserData(userId) {
const response = await fetch(
`https://api.example.com/users/${userId}`,
{
method: 'GET',
headers: {
'Authorization': 'Bearer your-api-token',
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
throw new Error(`Failed to fetch user: ${response.status}`);
}
return response.json();
}
// Using the function
fetchUserData(123)
.then(userData => {
console.log("User:", userData);
})
.catch(error => {
console.error("Error fetching user:", error);
});
The service worker context supports all fetch features including POST requests, custom headers, and request bodies. You can also use async/await syntax for cleaner asynchronous code.
Fetch from Popup or Options Page
Extension popup and options pages also enjoy privileged access to cross-origin requests, similar to the service worker:
// popup.js - Popup context
document.getElementById('fetchBtn').addEventListener('click', async () => {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
displayData(data);
} catch (error) {
showError(error.message);
}
});
These pages are part of your extension, so they inherit the extension’s host permissions. However, remember that popup pages have a short lifespan—they close when users click away. If you need to make long-running requests, consider using the service worker instead.
Why Content Script Fetch Fails
Attempting to use fetch directly in content scripts will typically fail due to CORS restrictions:
// ❌ This will likely fail in content scripts
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
This request fails because content scripts execute in the context of the web page, not the extension. The browser treats the request as if it originated from the website, and if the website’s server doesn’t send appropriate CORS headers, the request is blocked.
The solution is simple: always route cross-origin requests through the background service worker using message passing.
Common Pitfalls and How to Avoid Them
Understanding these common mistakes will save you hours of debugging frustration.
Missing Host Permissions
The most common issue is forgetting to declare the necessary host permissions. If your extension attempts to fetch from an undeclared domain, the request will fail. Always verify your manifest.json includes all domains you need to access.
Requesting from the Wrong Context
New extension developers often attempt to make cross-origin requests from content scripts, not understanding the limitations. Always use the service worker for API calls, and use content scripts only for page interaction and DOM manipulation.
Ignoring Async Response Handling
When using message passing to communicate with the service worker, remember to return true from your message listener to keep the message channel open for asynchronous responses:
// ❌ Incorrect - response sent before fetch completes
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetch(message.url)
.then(response => response.json())
.then(data => sendResponse({ data: data }));
// Missing return true!
});
// ✅ Correct - keeps channel open for async response
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
fetch(message.url)
.then(response => response.json())
.then(data => sendResponse({ data: data }))
.catch(error => sendResponse({ error: error.message }));
return true; // Important!
});
Not Handling Network Errors
Always implement proper error handling for network requests. Users may have no internet connection, servers may be down, or API keys may be invalid. Your extension should handle these scenarios gracefully:
async function safeFetch(url, options = {}) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timed out');
}
throw error;
}
}
Content Security Policy Considerations
Chrome extensions are subject to Content Security Policy (CSP) restrictions that add another layer of control over network requests.
Default CSP in Manifest V3
Manifest V3 includes a restrictive default CSP that limits what your extension can do. While this enhances security, it can also break functionality if not properly configured.
Customizing CSP
You can customize CSP in your manifest to allow specific connections:
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self'; connect-src https://api.example.com https://api.github.com"
}
}
This policy restricts scripts to your extension’s own files and limits cross-origin connections to specific domains. When setting custom CSP, balance security with functionality—allow only the connections your extension genuinely needs.
Testing with Strict CSP
Always test your extension with the strictest CSP settings possible before publishing. What works during development with relaxed settings may fail in production. Run Chrome with additional CSP enforcement or temporarily add overly strict policies to catch issues early.
Best Practices for Cross-Origin Requests
Follow these guidelines to build reliable, secure extensions that handle cross-origin requests properly.
Request Minimal Permissions
Only declare host permissions for domains you actually need. Requesting unnecessary permissions triggers warnings that can reduce installation rates and draw extra scrutiny during review.
Centralize API Logic
Keep all network logic in the service worker. This centralizes authentication, error handling, and request management in one place. Your content scripts become simpler and more focused on DOM manipulation.
Implement Request Caching
Consider implementing caching in your service worker to reduce API calls and improve performance:
// Simple in-memory cache
const cache = new Map();
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes
async function fetchWithCache(url, options = {}) {
const cached = cache.get(url);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
return cached.data;
}
const response = await fetch(url, options);
const data = await response.json();
cache.set(url, { data, timestamp: Date.now() });
return data;
}
Handle Authentication Securely
Never hardcode API keys or tokens in your extension’s source code. Instead, use Chrome’s storage API to store credentials securely, or implement OAuth flows for user authentication:
// Storing a token securely
chrome.storage.session.setAccessLevel({
accessLevel: 'TRUSTED_AND_UNTRUSTED_CONTEXTS'
}).then(() => {
chrome.storage.session.set({ apiToken: 'user-token' });
});
Add User Feedback
When making network requests, provide visual feedback to users. Show loading states, success messages, and clear error notifications. Network failures can happen for many reasons—help users understand what went wrong.
Troubleshooting Cross-Origin Issues
When cross-origin requests fail, use these techniques to diagnose and fix the problem.
Check the Console
Open the service worker console to see error messages. Navigate to chrome://extensions, find your extension, and click “service worker” to access the console.
Verify Host Permissions
Double-check that all required domains are declared in your manifest’s host_permissions array. Even small typos can cause requests to fail.
Test with Fetch from Service Worker
Isolate the problem by testing fetch directly in the service worker console. If it works there but fails in content scripts, the issue is likely the content script context limitation.
Use Chrome’s Network Logging
Navigate to chrome://net-export to capture network logs, or use the Network tab in DevTools when debugging extension pages. This shows detailed information about each request and response.
Conclusion
Cross-origin requests in Chrome extensions require a different approach than regular web development. The key takeaways are: declare specific host permissions in your manifest, always make cross-origin requests from the background service worker, use message passing to communicate between content scripts and the service worker, implement proper error handling, and respect Content Security Policy restrictions.
By following these patterns, you can build extensions that reliably interact with external APIs while maintaining security and passing Chrome Web Store review. The initial setup takes a bit more effort than simple fetch calls, but the result is a more robust and trustworthy extension.
Remember that Chrome’s extension platform continues to evolve. Stay current with the latest Manifest V3 documentation and best practices to ensure your extensions continue to work as browser security models evolve.
Related Articles
- Chrome Web Request API Complete Guide - Intercept and modify network requests
- Chrome Extension Permissions Explained - Understand extension permissions and security
- Chrome Storage API Overview - Store data securely in your extensions