CORS Unblock Development Chrome (2026)

Cross-Origin Resource Sharing (CORS) errors are a common obstacle when developing web applications that communicate with APIs. If you’ve worked on frontend projects that fetch data from external services, you’ve likely encountered the dreaded “No ‘Access-Control-Allow-Origin’ header” error. This guide shows you how to create a Chrome extension that helps manage CORS restrictions during development workflows.

Understanding the CORS Problem in Development

When your application running on localhost:3000 tries to fetch data from an API at api.example.com, browsers block the request due to the Same-Origin Policy. This security mechanism prevents malicious scripts from accessing resources on different domains, but it also blocks legitimate development requests.

Chrome extensions have a significant advantage over regular web pages: they can make cross-origin requests without being subject to the same CORS restrictions. This is because extension contexts are treated differently by the browser security model. However, there are still specific patterns you need to follow to implement this correctly.

Building Your CORS Helper Extension

The core of a CORS unblock extension relies on the web_accessible_resources manifest key and background scripts that proxy requests. Here’s a practical implementation:

Manifest Configuration (manifest.json)

{
 "manifest_version": 3,
 "name": "CORS Dev Helper",
 "version": "1.0.0",
 "description": "Helps bypass CORS during development",
 "permissions": [
 "activeTab",
 "scripting",
 "nativeMessaging"
 ],
 "host_permissions": [
 "<all_urls>"
 ],
 "background": {
 "service_worker": "background.js"
 },
 "action": {
 "default_popup": "popup.html",
 "default_icon": {
 "16": "icon16.png",
 "48": "icon48.png",
 "128": "icon128.png"
 }
 }
}

Background Script (background.js)

The background script acts as a proxy, handling requests that would otherwise be blocked:

// background.js - Handles cross-origin requests from content scripts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
 if (request.action === 'fetchProxy') {
 fetch(request.url, {
 method: request.method || 'GET',
 headers: request.headers || {},
 body: request.body ? JSON.stringify(request.body) : undefined
 })
 .then(response => response.json())
 .then(data => sendResponse({ success: true, data }))
 .catch(error => sendResponse({ success: false, error: error.message }));
 return true; // Keep channel open for async response
 }
});

Content Script Integration

From your web application, you communicate with the extension to make cross-origin requests:

// In your web application - call this instead of fetch()
async function corsFetch(url, options = {}) {
 return new Promise((resolve, reject) => {
 chrome.runtime.sendMessage({
 action: 'fetchProxy',
 url: url,
 method: options.method || 'GET',
 headers: options.headers || {},
 body: options.body
 }, (response) => {
 if (response.success) {
 resolve(response.data);
 } else {
 reject(new Error(response.error));
 }
 });
 });
}
// Usage example
const data = await corsFetch('https://api.example.com/data', {
 method: 'POST',
 headers: { 'Content-Type': 'application/json' },
 body: { key: 'value' }
});

Alternative Approaches

There are several ways to handle CORS during development, each with trade-offs:

Server-Side Proxy

Set up a simple Node.js proxy:

// server.js - Simple proxy server
const express = require('express');
const cors = require('cors');
const app = express();
app.use(cors());
app.use('/proxy/:url(*)', async (req, res) => {
 const targetUrl = req.params.url;
 const response = await fetch(targetUrl);
 const data = await response.json();
 res.json(data);
});
app.listen(3001);

Browser Flags

For quick testing, you can launch Chrome with security disabled:

macOS
open -a Google\ Chrome --args --disable-web-security --user-data-dir
Linux
google-chrome --disable-web-security
Windows
"C:\Program Files\Google\Chrome\Application\chrome.exe" --disable-web-security

This approach is useful for rapid debugging but should never be used in production or on machines with sensitive data.

Security Considerations

Building a CORS bypass extension requires careful security thinking:

Restrict to Development Environments: Add checks to ensure requests only go to known development URLs. You can store allowed domains in extension storage:

// background.js - Domain validation
const ALLOWED_DEV_DOMAINS = ['localhost', '127.0.0.1', '*.dev'];
function isDevUrl(url) {
 return ALLOWED_DEV_DOMAINS.some(domain => 
 url.includes(domain) || new URL(url).hostname.endsWith(domain.replace('*', ''))
 );
}

Validate All Inputs: Never blindly proxy requests. Validate URLs, methods, and content to prevent your extension from being used as an open proxy.

Use HTTPS in Production: Even for development proxies, establish secure connections to avoid exposing sensitive credentials.

Advanced: Programmatic Header Injection

For more complex scenarios, you might need to modify headers programmatically:

// background.js - Header manipulation
chrome.webRequest.onBeforeRequest.addListener(
 (details) => {
 if (details.url.includes('api.dev.local')) {
 return {
 redirectUrl: details.url + 
 (details.url.includes('?') ? '&' : '?') + 
 'cors-bypass=true'
 };
 }
 },
 { urls: ["<all_urls>"] },
 ["blocking"]
);

This approach intercepts requests at the network level, allowing you to add custom headers or modify request behavior.

Deployment and Testing

When your extension is ready:

  1. Enable Developer Mode in chrome://extensions
  2. Click “Load unpacked” and select your extension directory
  3. Test with a simple fetch call to verify the proxy works
  4. Check the background script console for any errors

When to Use Each Approach

For most development scenarios, consider this decision tree:

  • Quick debugging: Browser flags provide the fastest solution
  • Repeated API testing: A custom extension with the proxy pattern works best
  • Team environments: A shared proxy server ensures consistency
  • CI/CD pipelines: Server-side proxies integrate more naturally

Chrome extensions give you the most flexibility for local development, while server-side solutions scale better for team environments.

Handling Non-JSON Response Types

The background script shown above only handles JSON responses. Real APIs return HTML, plain text, binary data, and streaming responses. Here is a more solid version of the fetch proxy that preserves the response type:

// background.js - Type-aware proxy
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
 if (request.action === 'fetchProxy') {
 fetch(request.url, {
 method: request.method || 'GET',
 headers: request.headers || {},
 body: request.body ? JSON.stringify(request.body) : undefined
 })
 .then(async response => {
 const contentType = response.headers.get('content-type') || '';
 let data;
 if (contentType.includes('application/json')) {
 data = await response.json();
 } else if (contentType.includes('text/')) {
 data = await response.text();
 } else {
 // Binary: return as base64
 const buffer = await response.arrayBuffer();
 data = btoa(String.fromCharCode(...new Uint8Array(buffer)));
 }
 sendResponse({
 success: true,
 data,
 status: response.status,
 contentType
 });
 })
 .catch(error => sendResponse({ success: false, error: error.message }));
 return true;
 }
});

The caller can inspect contentType in the response to decide how to consume data, making this a drop-in proxy for any API surface.

Persisting Allowed Origins in Extension Storage

Hardcoding ALLOWED_DEV_DOMAINS as a constant gets unwieldy when you work across many projects. A better pattern stores the allowed list in chrome.storage.sync and exposes a settings popup to manage it:

// popup.js - Load and save allowed origins
document.addEventListener('DOMContentLoaded', () => {
 const list = document.getElementById('origins');
 const input = document.getElementById('new-origin');
 const addBtn = document.getElementById('add');
 chrome.storage.sync.get({ allowedOrigins: [] }, ({ allowedOrigins }) => {
 allowedOrigins.forEach(origin => appendOriginItem(origin, list));
 });
 addBtn.addEventListener('click', () => {
 const origin = input.value.trim();
 if (!origin) return;
 chrome.storage.sync.get({ allowedOrigins: [] }, ({ allowedOrigins }) => {
 if (!allowedOrigins.includes(origin)) {
 allowedOrigins.push(origin);
 chrome.storage.sync.set({ allowedOrigins });
 appendOriginItem(origin, list);
 }
 input.value = '';
 });
 });
});
function appendOriginItem(origin, container) {
 const li = document.createElement('li');
 li.textContent = origin;
 container.appendChild(li);
}

In background.js, replace the static array with a lookup against chrome.storage.sync:

async function isAllowedOrigin(url) {
 return new Promise(resolve => {
 chrome.storage.sync.get({ allowedOrigins: [] }, ({ allowedOrigins }) => {
 try {
 const hostname = new URL(url).hostname;
 const allowed = allowedOrigins.some(pattern => {
 if (pattern.startsWith('*.')) {
 return hostname.endsWith(pattern.slice(2));
 }
 return hostname === pattern;
 });
 resolve(allowed);
 } catch {
 resolve(false);
 }
 });
 });
}

Now adding a new development API host takes three seconds through the popup instead of a code change and extension reload.

Debugging the Extension Itself

When the proxy silently fails, the most common culprits are the message channel closing before the async response arrives, a missing return true in the listener, or a permission gap in manifest.json. The following checklist catches most issues:

Check the service worker console. In chrome://extensions, click the “service worker” link next to your extension. This opens a DevTools window attached to the background context where console.log calls and uncaught errors appear.

Verify host permissions match the target URL. A request to https://api.internal.example.com will silently fail if your manifest only grants http://localhost/*. Use <all_urls> during development and scope it down before any wider distribution.

Confirm the message channel stays open. The return true at the end of the onMessage listener is not optional. omitting it closes the channel synchronously, so sendResponse called inside a promise always arrives after the channel is gone, and the caller receives undefined.

Test the background fetch in isolation. Open the service worker DevTools console and call fetch directly on the target URL. If this fails, the problem is network-level (firewall, DNS, TLS certificate) not extension logic.

Using declarativeNetRequest for Header Injection in MV3

The webRequest API with the "blocking" option shown earlier is not permitted in Manifest V3 for extensions distributed through the Chrome Web Store. The modern equivalent is declarativeNetRequest, which lets you declare header modification rules statically or update them dynamically at runtime:

// background.js - Add CORS headers to responses from a dev API
chrome.declarativeNetRequest.updateDynamicRules({
 addRules: [
 {
 id: 1,
 priority: 1,
 action: {
 type: 'modifyHeaders',
 responseHeaders: [
 {
 header: 'Access-Control-Allow-Origin',
 operation: 'set',
 value: '*'
 }
 ]
 },
 condition: {
 urlFilter: '*://api.dev.local/*',
 resourceTypes: ['xmlhttprequest', 'fetch']
 }
 }
 ],
 removeRuleIds: []
});

This requires the declarativeNetRequest permission in your manifest rather than webRequestBlocking. The rule activates immediately and persists across browser restarts until you remove it programmatically. For development use, call updateDynamicRules on extension startup and clear rules matching your dev hosts when the popup is toggled off.

Packaging the Extension for Your Team

A custom CORS helper extension does not need to go through the Chrome Web Store to be shared with teammates. You can distribute it as a packed .crx file or, more practically, as a zipped source directory that each developer loads unpacked.

For teams using a monorepo, commit the extension source under a tools/cors-dev-helper/ directory. Add a brief note in the project README explaining how to load it. This keeps the tooling versioned alongside the project so updates to allowed origins or proxy logic ship with normal pull requests.

If the extension needs to be installed on many developer machines, an organization-level Chrome policy can force-install it from a local path or internal hosting without requiring the Chrome Web Store. The ExtensionInstallForcelist policy accepts a path to a .crx and an update manifest URL, enabling centralized distribution and updates.


Try it: Paste your error into our Error Diagnostic for an instant fix.

I'm a solo developer in Vietnam. 50K Chrome extension users. $500K+ on Upwork. 5 Claude Max subscriptions running agent fleets in parallel. These are my actual CLAUDE.md templates, orchestration configs, and prompts. Not a course. Not theory. The files I copy into every project before I write a line of code. **[See what's inside →](https://zovo.one/lifetime?utm_source=ccg&utm_medium=cta-default&utm_campaign=chrome-extension-cors-unblock-development)** $99 once. Free forever. 47/500 founding spots left.

Related Reading

Built by theluckystrike. More at zovo.one

Find the right skill → Browse 155+ skills in our Skill Finder.