“Blocked by CORS policy” from origin 'null'

You double-click index.html. The chart area is empty, and the console has one red line:

Access to fetch at 'file:///Users/you/report/data.json' from origin 'null'
has been blocked by CORS policy: Cross origin requests are only supported
for protocol schemes: http, data, isolated-app, chrome-extension, chrome,
https, chrome-untrusted.

Or, for a built app:

Access to script at 'file:///…/dist/assets/index-4f2a.js' from origin 'null'
has been blocked by CORS policy

Nothing is wrong with the file. The browser is refusing to let a page opened from disk read other files on disk, and it is right to.

Why file:// pages are origin 'null'

Every web page has an origin — scheme, host and port. Two pages share an origin and can read each other's responses; otherwise the server has to opt in with an Access-Control-Allow-Origin header.

A file has no host. Chrome and Edge give every file:// page an opaque origin, which serialises as the string 'null', and opaque origins match nothing — not even the folder the page sits in. There is no server to send an allow header either. So any request that runs in CORS mode fails.

If that sounds paranoid: without it, any HTML attachment you open could read ~/.ssh/ with one fetch().

What triggers it, and what doesn't

In the page From file://
<img src="chart.png"> Works
<link rel="stylesheet" href="app.css"> Works
<script src="app.js"> (classic) Works
<script type="module" src="app.js"> Blocked — modules use CORS
import "./chart.js" inside a module Blocked
fetch("data.json") / XMLHttpRequest Blocked
d3.csv("data.csv"), Plotly.d3.json(…) Blocked — they wrap fetch
Web workers, @font-face in some browsers Often blocked

That table is why the failure feels arbitrary. The styled shell of the page loads, so it looks broken rather than missing — only the part that reads data stays empty.

Fix 1: serve the folder (your machine)

Run a static server from the folder that contains the page:

python3 -m http.server 8000
npx serve .

Open http://localhost:8000/. Everything now comes from http://localhost:8000, one origin, and both fetch and module scripts load. For a Vite build, npx vite preview does the same for dist/. More options in serve an HTML file locally.

This is the fix for development. It does nothing for a colleague you email the file to — and a localhost link only works on your machine.

Fix 2: put the data inside the page

If the page fetches one JSON or CSV file, embed it instead:

<script type="application/json" id="report-data">
  {
    "rows": [
      ["2026-09", 1840],
      ["2026-10", 2215]
    ]
  }
</script>
<script>
  const data = JSON.parse(document.getElementById("report-data").textContent);
  draw(data.rows);
</script>

A JSON block is inert — it is never executed, just read — and escaping is easy: the only sequence that can break it is </script, which you write as <\/script. For CSV, put it in the same kind of block with type="text/csv" and parse the string.

Most notebook and charting exports already have a switch for this. Plotly's include_plotlyjs=True keeps the library in the file (the four modes); Altair and Bokeh embed their data by default.

Fix 3: stop loading modules from disk

For a bundled app, make sure no module has to be fetched. With Vite, vite-plugin-singlefile inlines every chunk into index.html — an inline <script type="module"> has nothing to fetch, so the CORS check never runs. With relative paths (base: './') the remaining images and fonts resolve from disk. Details for the common build tools: Vite, Angular, Next.js static export.

Images, stylesheets and fonts referenced from the HTML can be embedded the same way — the free HTML inliner rewrites a whole folder into one file and lists anything it couldn't resolve.

Fix 4: don't send a file at all

The flag people find on Stack Overflow — --allow-file-access-from-files — makes the error go away on the one machine that was launched with it, and lowers the guard for every other file that browser opens. It is a test, not a fix.

The durable version is a URL. Publish the page with its assets and send the link: the recipient opens it in a normal browser tab, nothing is downloaded, and nothing depends on their filesystem.

One honest caveat for Comma specifically: reports run in a sandboxed iframe with scripts enabled but no same-origin access, so a script that calls fetch("data.json") at runtime still won't find a sibling file after publishing. Inline the data first (fix 2), then publish — charts, filters and sorting keep working.

Firefox and Safari

Firefox used to allow file:// reads within the page's own directory and now treats each file as a unique origin too (security.fileuri.strict_origin_policy). Safari blocks local XHR unless Disable Local File Restrictions is ticked in the Develop menu. Same conclusion in every browser: don't rely on reading from disk.

Try it

Comma is free — publish a self-contained HTML file, get a link, and collect comments pinned to the exact chart.

Publish a report →

Related