GuidesLOCAL DEV

Fix a CORS error on localhost

A CORS error on localhost means the API response has no Access-Control-Allow-Origin header that matches your dev origin, for example http://localhost:5173. The port is part of the origin: http://localhost:5173 and http://localhost:8080 are different origins. To fix it, allow your dev origin on your API, proxy /api through your dev server, or put https://proxy.cors.dev/ in front of the URL for a public third-party API.

Why localhost ports are different origins

An origin is the scheme, host and port together. Vite serves on http://localhost:5173 by default; an API on http://localhost:8080 is a different origin, so the browser applies CORS. http://localhost:5173 and http://127.0.0.1:5173 are different origins too, because the host names differ. The request often reaches your API and succeeds there, but the browser then refuses to hand the response to your code; Postman and curl do not enforce CORS, which is why the same call works there: Postman vs the browser.

Which localhost requests are cross-origin
Page and APICross-originReason
http://localhost:5173 calling http://localhost:5173/api/itemsNoSame scheme, host and port
http://localhost:5173 calling http://localhost:8080/api/itemsYesDifferent port
http://localhost:5173 calling http://127.0.0.1:5173/api/itemsYesDifferent host name
http://localhost:5173 calling https://localhost:5173/api/itemsYesDifferent scheme
file:///index.html calling http://localhost:8080/api/itemsYesThe page origin is null

The console message names both origins: Access to fetch at 'http://localhost:8080/api/items' from origin 'http://localhost:5173' has been blocked by CORS policy. The rest of the message says which header is missing or wrong. See every variant explained: Access to fetch blocked by CORS policy.

Pick the fix

CORS fixes for local development
SituationFixAfter you deploy
Your API on another portAllow the dev origin on the APIAdd the production origin too
Your API, no server changesProxy /api through the dev serverNeeds another fix: the proxy is dev only
Page opened from diskServe the folder over HTTPHosted pages have an http or https origin
Public third-party APIPrefix the URL with https://proxy.cors.dev/The same code keeps working
Third-party API that needs a keyCall it from your own backendThe key stays on the server

Allow the dev origin on your API

For an API you run, add the exact dev origin to its allowed origins, and add your production origin at the same time. The value must match scheme, host and port exactly, with no trailing slash. Access-Control-Allow-Origin: * works for requests without cookies or other credentials; with credentials the API must return the exact origin plus Access-Control-Allow-Credentials: true. In Express, the cors package takes an array of origins.

Express: allow your dev origin and your production origin
import cors from 'cors';
import express from 'express';

const app = express();

app.use(
  cors({
    origin: ['http://localhost:5173', 'https://app.example.com'],
  }),
);

app.get('/api/items', (req, res) => res.json([{ id: 1, name: 'First item' }]));

app.listen(8080);

The same fix applies to other stacks. Consult Express, FastAPI, Flask, Django, Spring Boot, and Laravel.

Proxy /api through the dev server

A dev-server proxy forwards /api requests from the dev server to your API, so the browser only talks to one origin and CORS never applies. In Vite it is server.proxy in vite.config.js; in the Angular CLI it is proxyConfig, see Angular CORS error; more on Vite: Vite integration. Call relative URLs such as /api/items from your code. The proxy only exists while the dev server runs; a production build still needs one of the other fixes.

vite.config.js: forward /api to your backend
import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
});

// In the app, fetch('/api/items') now reaches http://localhost:8080/api/items

Pages opened from file://

A page opened straight from disk has a file:/// URL, and its origin is null. In Chromium, fetch() of another local file fails with URL scheme "file" is not supported, and XMLHttpRequest fails with Cross origin requests are only supported for protocol schemes. Requests from such a page to an API carry Origin: null, which most APIs do not allow. The cors.dev free tier answers Origin: null with Access-Control-Allow-Origin: *, so public API calls through the proxy work from such a page. Loading other local files still needs a server: npx serve . serves the folder on http://localhost:3000, and python3 -m http.server 8000 on http://localhost:8000.

Serve the folder over HTTP instead of file://
# Node.js: serves the current folder on http://localhost:3000
npx serve .

# Python 3: serves the current folder on http://localhost:8000
python3 -m http.server 8000

Public third-party APIs

You cannot change the headers of an API you do not run, such as api.deezer.com, which sends no CORS headers. For public data, put https://proxy.cors.dev/ in front of the URL: the proxy requests it server-side and returns it with Access-Control-Allow-Origin set to your page's origin. The free tier accepts http://localhost origins on any port, needs no signup or key, and handles GET and HEAD requests with JSON, XML, HTML, CSV and other text responses up to 1 MiB, from a shared pool with fair-use rate limits. The same code keeps working after you deploy.

Call a third-party public API from localhost
// Runs on http://localhost:5173; api.deezer.com sends no CORS headers
const response = await fetch('https://proxy.cors.dev/https://api.deezer.com/artist/27', {
  credentials: 'omit',
});
if (!response.ok) {
  throw new Error(`HTTP ${response.status}: ${response.headers.get('X-Cors-Error') ?? 'upstream error'}`);
}

const artist = await response.json();
console.log(artist.name);

Never send API keys or private data through a public proxy: the operator can observe the traffic. The free tier rejects Authorization, cookies and custom headers. For APIs that need a key, use your own backend: CORS proxy vs your own backend.

What does not fix it

  • mode: 'no-cors' returns an opaque response with status 0 and an empty body that your code cannot read: no-cors and opaque responses.
  • Starting Chrome with web security turned off only changes your own browser, and visitors still get the error: disable CORS in Chrome.
  • CORS browser extensions rewrite headers only in your browser: CORS Chrome extensions.
  • Adding Access-Control-Allow-Origin to your request headers does not help: it is a response header, and an extra request header makes the browser send a preflight first.

Good questions.

Why do I get a CORS error on localhost?

Your frontend and your API run on different ports, which makes them different origins. The API's response lacks an Access-Control-Allow-Origin header that matches your dev origin.

Is localhost:3000 a different origin from localhost:5173?

Yes: the port is part of the origin. http://localhost:3000 and http://localhost:5173 are two origins, so requests between them are cross-origin.

Are localhost and 127.0.0.1 the same origin?

No: the browser compares host names as written, so http://localhost:5173 and http://127.0.0.1:5173 are different origins. Allow both on the API if you use both.

Can I use Access-Control-Allow-Origin: * for localhost?

Yes, for requests without credentials. With credentials: 'include' or withCredentials, the API must return the exact origin and Access-Control-Allow-Credentials: true.

Does a dev-server proxy fix CORS in production?

No: it only runs with the dev server. Production needs CORS headers on the API or one origin for the app and the API.

Does proxy.cors.dev work from localhost?

Yes: it accepts http://localhost and http://127.0.0.1 origins on any port. The free tier also works from a file:/// page, whose origin is null: the proxy answers with Access-Control-Allow-Origin: *.