Connecting a Chrome Extension to a Node.js API

TIL that a fetch call from a content script that worked fine in the console failed silently from the actual extension, because content scripts run in an isolated world with their own CORS and CSP rules.

3 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The Problem #

A fetch call to the delivery API worked when I pasted it directly into DevTools' console on the POS page, and failed silently — no response, no obvious error in my own code's try/catch — when the identical call ran from the content script itself.

Context #

Content scripts run in an "isolated world": same DOM access as the page, but a separate JavaScript execution context with its own extension-specific security rules, which don't behave identically to a script the page itself would run.

What I Tried #

Added more logging around the fetch call, assuming I was swallowing an error somewhere.

What Went Wrong #

There was no error to catch — the request was being blocked before it ever resolved or rejected in a way my try/catch could see, because the extension's manifest hadn't declared the API's origin as a permitted host, so Chrome blocked the cross-origin request at a level below my own code.

The Solution #

Declared the API's origin explicitly in the extension's host_permissions, and moved the actual network call into the background service worker (which has broader network permissions than a content script) rather than firing it directly from the content script.

// manifest.json
{
  "host_permissions": ["https://api.delivery-app.com/*"],
  "permissions": ["storage"]
}
// content script asks the background worker to make the request
chrome.runtime.sendMessage({ type: "SYNC_ORDER", order }, (response) => { ... });

Why It Works #

Manifest V3 restricts what a content script can do to the page it's injected into and requires the extension to explicitly declare which external origins it's allowed to talk to. Routing the network call through the background worker — which is the extension's own privileged context, not the isolated-but-still-page-adjacent content script — is both more permission-appropriate and keeps network logic out of a script that's re-injected on every page load. Debugging "it works in DevTools but not in my code" inside an extension needs a different mental model than debugging a normal web page — content scripts, background workers, and the page itself are three separate execution contexts with different rules, not one script running in three places.

Lessons Learned #

A silent failure with no thrown error is often a permissions or context boundary issue in extension code, not a logic bug — worth checking host_permissions and execution context before assuming the fetch call itself is wrong.

What I Would Do Differently #

I'd set up the background-worker message-passing pattern for all network calls from the start, instead of discovering content scripts aren't the right place for them after an afternoon of debugging a fetch that silently went nowhere.

Content script isolated worlds, host_permissions, chrome.runtime.sendMessage for cross-context communication.