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.
| Page and API | Cross-origin | Reason |
|---|---|---|
| http://localhost:5173 calling http://localhost:5173/api/items | No | Same scheme, host and port |
| http://localhost:5173 calling http://localhost:8080/api/items | Yes | Different port |
| http://localhost:5173 calling http://127.0.0.1:5173/api/items | Yes | Different host name |
| http://localhost:5173 calling https://localhost:5173/api/items | Yes | Different scheme |
| file:///index.html calling http://localhost:8080/api/items | Yes | The 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
| Situation | Fix | After you deploy |
|---|---|---|
| Your API on another port | Allow the dev origin on the API | Add the production origin too |
| Your API, no server changes | Proxy /api through the dev server | Needs another fix: the proxy is dev only |
| Page opened from disk | Serve the folder over HTTP | Hosted pages have an http or https origin |
| Public third-party API | Prefix the URL with https://proxy.cors.dev/ | The same code keeps working |
| Third-party API that needs a key | Call it from your own backend | The 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.
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.
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/itemsPages 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.
# 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 8000Public 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.
// 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 status0and 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-Originto 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: *.