Jeg fik min første CORS-fejl en tirsdag aften, og så var alt i stykker
Jeg ville bare lave et lille fetch-kald til et API. I stedet fik jeg en gul fejl i konsollen og en følelse af at browseren var sur på mig personligt.
I den her artikel går jeg igennem, hvad CORS egentlig er, hvorfor din browser blokerer dine requests, hvad de typiske CORS fejl betyder, og hvordan du sætter det rigtigt op i Node/Express uden at åbne hele butikken.
Hvad CORS er, og hvorfor det kun driller i browseren
Først: CORS er ikke en teknologi, du “tænder”. Det er et sæt regler for, hvordan browsere må snakke med andre domæner.
CORS står for Cross-Origin Resource Sharing. Oversat til almindeligt dansk: må en side fra ét sted på nettet hente data fra et andet sted.
En origin er kombinationen af:
- protokol (http / https)
- domæne (fx codingclass.dk eller localhost)
- port (fx :3000 eller :8000)
Hvis bare én af de tre er forskellig, er det en anden origin.
Det vigtige: CORS er kun et problem i browseren. Hvis du laver samme request med curl eller fra en Node server, kommer der ingen CORS-fejl.
Det er fordi CORS er en del af browserens sikkerhedsmodel. Serveren aner faktisk ikke, at “der var en CORS-fejl”. Den har bare fået en request og måske svaret fint. Browseren vælger så ikke at give svaret videre til din JavaScript.
Derfor hjælper det heller ikke at “disable CORS” i din kode. Du kan kun:
- ændre, hvordan serveren svarer (headers)
- eller ændre, hvordan du kalder serveren (origin, proxy osv.)
Same-origin policy i 3 konkrete eksempler
Same-origin policy er grundreglen: en side må kun læse svar fra sin egen origin, medmindre serveren eksplicit giver lov via CORS-headers.
Eksempel 1: Alt matcher, ingen CORS
Frontend:
http://localhost:3000
API:
http://localhost:3000/api/users
Samme protokol, domæne og port. Same origin.
Her sker der ingenting specielt. Ingen CORS-headers nødvendig. Din fetch virker bare.
Eksempel 2: Samme domæne, forskellig port
Frontend:
http://localhost:3000
API:
http://localhost:5000/api/users
Domænet er det samme (localhost), men porten er forskellig. Det er to forskellige origins.
Din JavaScript gør måske noget som:
fetch('http://localhost:5000/api/users')
.then(res => res.json())
.then(data => console.log(data));
Her vil browseren spørge: “har serveren på :5000 sagt, at jeg må læse svaret?”. Det kræver typisk en header som:
Access-Control-Allow-Origin: http://localhost:3000
Eksempel 3: Helt andet domæne
Frontend:
https://minfrontend.dk
API:
https://api.andetfirma.dk/data
Her skal API’et aktivt tillade din origin. Ellers må din browser ikke læse svaret.
Det er blandt andet det, der beskytter mod at en tilfældig side henter følsomme data fra et andet site, hvor du er logget ind, og læser det direkte i JavaScript.
Hvad er en preflight (OPTIONS), og hvornår sker den?
Da jeg første gang så en OPTIONS-request i Network-tabben, troede jeg, at jeg havde fået virus. Jeg havde jo ikke selv skrevet noget med OPTIONS.
En preflight request er en ekstra request, som browseren sender før din rigtige request. Typisk sådan her:
- Din JavaScript kalder
fetch('https://api.example.com/data', {...}) - Browseren sender først en
OPTIONS-request til samme URL - Serveren svarer med CORS-headers (eller ikke)
- Hvis svaret er ok, sender browseren den rigtige
GET,POSTosv.
Preflight sker kun ved “ikke-simple” requests. “Simple” betyder cirka:
- Metode er GET, POST eller HEAD
- Content-Type er en af:
application/x-www-form-urlencoded,multipart/form-dataellertext/plain - Ingen “custom” headers (altså kun nogle få standard som Accept, Content-Language osv.)
Hvis du f.eks. laver:
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-My-Header': 'hej'
},
body: JSON.stringify({ name: 'Jonas' })
});
Så vil browseren typisk lave en preflight, fordi:
Content-Typeerapplication/json(ikke “simple”)- Du har en custom header
X-My-Header
Serveren skal så håndtere OPTIONS-metoden og svare med noget i stil med:
Access-Control-Allow-Origin: https://minfrontend.dk
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-My-Header
Hvis serveren ikke svarer korrekt på preflight, får du CORS-fejl, og din rigtige request bliver aldrig sendt.
De 5 mest almindelige CORS-fejl (og hvad de betyder)
Nu til den del, hvor man sidder og stirrer på Chrome DevTools og bander stille. Her er de fejl, jeg oftest ser, og hvad de signalerer.
1. “No ‘Access-Control-Allow-Origin’ header is present on the requested resource”
Oversat: serveren har ikke sendt den header, browseren leder efter.
Typiske årsager:
- Serveren har slet ingen CORS-headers
- Serveren sender
Access-Control-Allow-Origin, men ikke til din origin - Fejl på serveren (500), så CORS-headeren aldrig når med i svaret
Tjek:
- Netværkstab i devtools: se på svaret under “Headers”
- Se, om
Access-Control-Allow-Originfindes og matcher præcis din origin
2. “The ‘Access-Control-Allow-Origin’ header contains multiple values”
Det her sker ofte, når man manuelt sætter headeren flere steder.
Fx i Express:
res.set('Access-Control-Allow-Origin', 'http://localhost:3000');
// og samtidig bruger du cors-middleware, der også sætter den
Browseren vil kun have én værdi i headeren. Ikke en liste med kommaer, ikke flere headers.
3. “Response to preflight request doesn’t pass access control check”
Her fejler preflight, typisk fordi:
- Serveren svarer ikke på
OPTIONS Access-Control-Allow-Methodsmangler den metode, du vil bruge (fx DELETE)Access-Control-Allow-Headersmangler en af de custom headers, du sender
Tjek i Network-tabben, at din OPTIONS-request får et 200-svar med de rigtige CORS-headers.
4. “Credential is not supported if the CORS header ‘Access-Control-Allow-Origin’ is ‘*'”
Den her er klassikeren, når man arbejder med cookies eller Authorization-headers.
Hvis du kalder:
fetch(url, {
credentials: 'include'
});
eller bruger withCredentials i Axios, så må serveren ikke svare med:
Access-Control-Allow-Origin: *
Serveren skal i stedet sende den konkrete origin, fx:
Access-Control-Allow-Origin: https://minfrontend.dk
Access-Control-Allow-Credentials: true
5. Der står CORS-fejl, men det egentlige problem er 500/404
En lille fælde: nogle gange er CORS-fejlen bare støj oven på en almindelig serverfejl.
Tjek altid:
- Hvilken HTTP-status får du? 200, 404, 500?
- Kan du ramme samme endpoint via
curleller Postman?
Hvis din API i forvejen smider 500, hjælper det ikke at rode videre med CORS.
Løsning i praksis med Node/Express
Nu til noget, du kan kopiere og teste. Jeg bruger express og det officielle cors-middleware her.
Først et minimalt API uden CORS:
// server.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/api/hello', (req, res) => {
res.json({ message: 'Hej fra API'et' });
});
app.listen(5000, () => {
console.log('Server kører på http://localhost:5000');
});
Hvis du nu fra en frontend på http://localhost:3000 laver:
fetch('http://localhost:5000/api/hello')
.then(r => r.json())
.then(console.log)
.catch(console.error);
så får du helt sikkert en CORS-fejl.
Tillad kun bestemte origins
Nu tilføjer vi cors-middleware og begrænser til en whitelist af origins.
// server.js
const express = require('express');
const cors = require('cors');
const app = express();
const allowedOrigins = [
'http://localhost:3000',
'https://minfrontend.dk'
];
const corsOptions = {
origin: function(origin, callback) {
// origin er undefined ved f.eks. curl eller Postman
if (!origin) return callback(null, true);
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Origin not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
};
app.use(cors(corsOptions));
app.use(express.json());
app.get('/api/hello', (req, res) => {
res.json({ message: 'Hej fra API'et' });
});
app.listen(5000, () => {
console.log('Server kører på http://localhost:5000');
});
Det her gør tre ting:
- Tillader kun de origins, du selv har listet
- Sætter de nødvendige CORS-headers på både preflight og normale svar
- Tillader credentials (cookies / Authorization) på en kontrolleret måde
Hvis du er nysgerrig på flere muligheder, kan du kigge i Express’ egen CORS-dokumentation og sammenligne med din kode.
Credentials, cookies og hvorfor “*” ikke virker
Hvis du vil sende cookies eller auth-tokens mellem frontend og backend, skal du have styr på tre ting:
- I fetch-kaldet:
fetch('https://api.example.com/data', {
credentials: 'include'
});
- På serveren:
const corsOptions = {
origin: 'https://minfrontend.dk',
credentials: true
};
app.use(cors(corsOptions));
- Og du må ikke bruge
Access-Control-Allow-Origin: *
Browseren siger ganske enkelt nej, hvis du både vil have wildcard-origin og credentials.
Så i udvikling kan du godt slippe afsted med noget ala:
app.use(cors({ origin: 'http://localhost:3000', credentials: true }));
I produktion på en rigtig side skal du være lige så præcis med origin, som du er med dine adgangskoder.
Hvis du vil se et større flow med cookies, session og CORS, så er MDN’s CORS-artikel faktisk ret god at bladre igennem sammen med din egen kode.
Dev-setup: brug en proxy i udvikling uden at gemme problemet
Noget af det mest forvirrende er, at CORS nogle gange
Det sker ofte, hvis du bruger en dev-server-proxy (som i React, Vite eller lignende). Så ser din frontend sådan her ud:
// React + Vite eksempel (vite.config.js)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
});
Din browser taler i virkeligheden kun med http://localhost:5173 (eller hvad dev-serveren nu bruger). Proxien sender trafikken videre til backend.
Det betyder:
- Ingen CORS mellem frontend og backend i dev
- Men i produktion har du måske to separate origins, og så dukker CORS-fejlene op
Jeg bruger ofte proxier i udvikling, fordi det gør livet lettere. Men jeg prøver at tænke mit produktionssetup igennem samtidig:
- Skal frontend og backend køre på samme domæne i produktion? Så efterligner proxien det scenarie fint
- Skal de køre på hver sit domæne? Så giver det mening at slå CORS til lokalt og teste det også
På Coding Class ligger mange af eksemplerne med simple setups, så du kan se, hvordan CORS ændrer sig, når man splitter tingene op på flere domæner eller porte.
Tjekliste: sådan tester du, at CORS sidder der, hvor det skal
Når jeg selv sidder fast i en CORS-fejl, kører jeg stort set altid samme lille rutine.
1. Virker API’et uden browser?
curl -i http://localhost:5000/api/hello
Får du et fornuftigt svar (fx JSON)? Hvis ikke, så er problemet ikke CORS endnu. Fix API’et først.
2. Hvilken origin har din frontend?
Åbn devtools i browseren og skriv i konsollen:
window.location.origin
Det er den værdi, serveren skal sætte i Access-Control-Allow-Origin (eller matche i sin whitelist).
3. Hvad svarer serveren egentlig?
Se i Network-tabben på den request, der fejler. Kig på:
- “Headers” fanen
- Response headers: er der en
Access-Control-Allow-Origin? - Matcher den din origin?
Tjek også, om der kom en OPTIONS-request før, og om den fik et fornuftigt svar.
4. Har du blandet flere CORS-løsninger sammen?
Se din serverkode igennem:
- Bruger du både
cors-middleware og manuelleres.set('Access-Control-Allow-Origin', ...)? - Sætter du CORS-headers både globalt og i enkelte routes?
Ryd op, så du har én tydelig måde at gøre det på.
5. Test uden credentials først
Hvis du samtidig kæmper med cookies, tokens og login, så prøv lige et helt simpelt GET-endpoint uden auth, hvor du får CORS til at spille først.
Når du kan lave en enkel GET /api/hello fra din frontend uden fejl, kan du skrue op for sværhedsgraden derfra.
Hvis du vil øve patterns og se flere komplette små projekter, der bruger fetch, API og CORS, kan du kigge på de andre backend-artikler på codingclass.dk eller kombinere det med de simple fetch-eksempler i vores indlæg om JavaScript fetch API.









1 kommentar