CORS-fejl uden panik – brug Chrome som lup på 10 minutter
CORS på 60 sekunder uden fluff
Jeg har brugt aftener på at stirre på en CORS fejl og tænke “men mit API virker jo”. Det gør det tit også, men browseren siger nej, fordi reglerne for hvem der må kalde hvad, ikke matcher.
CORS (Cross-Origin Resource Sharing) er bare en sikkerhedsregel i browseren, som bestemmer om din frontend på fx http://localhost:5173 må hente data fra dit API på fx http://localhost:3000. Serveren svarer med nogle HTTP headers, og hvis de ikke passer til requesten, blokerer browseren og viser en CORS fejl i konsollen.
Find CORS-fejlen i Network-tab i Chrome
Hvis du kun kigger på den røde fejl i Console, bliver du mest forvirret. Du skal over i Network-tab, det er der CORS dramaet faktisk står.
Gør det her:
- Åbn DevTools (F12 eller højreklik → Inspect).
- Gå til Network.
- Reproducer fejlen (reload eller klik den knap der fejler).
- Filtrer evt. på Fetch/XHR, så du kun ser API-kald.
Nu ser du typisk to requests til samme URL, hvis der er CORS på spil:
- Et OPTIONS request (preflight).
- Det rigtige request, fx GET eller POST.
Klik på OPTIONS-requestet først. Kig på fanerne Headers og Response Headers. Her står fx Access-Control-Allow-Origin, Access-Control-Allow-Headers, Access-Control-Allow-Methods osv. Det er de linjer, der afgør om browseren siger ja eller nej.
Preflight vs main request – hvad du skal kigge efter
En CORS preflight er bare browserens “må jeg godt?” request før det rigtige kald. Den er automatisk og bruger OPTIONS-metoden.
Du får en preflight hvis du fx:
- Bruger andre metoder end GET/HEAD/POST.
- Bruger custom headers (fx
Authorization,X-Whatever). - Sætter
Content-Typetil noget ikke-standard (fxapplication/jsoni en POST).
Din fejlsøgningsrutine kan være sådan her:
- Har jeg et OPTIONS-request? Hvis nej, så er det typisk en simpel CORS fejl i det rigtige request (fx forkert
Access-Control-Allow-Origin). - Hvis ja: svarer serveren med 200 på OPTIONS? Hvis den returnerer 404/405/500, er det første problem dér.
- Hvis OPTIONS er 200: matcher
Access-Control-Allow-Originmin frontend origin? MatcherAllow-MethodsogAllow-Headersdet request jeg sender?
Resten af artiklen er egentlig bare variationer over de fejl.
Fejl 1 – Mangler OPTIONS handler / 405 på preflight
Den her har ødelagt min aften mere end en gang. Du har lavet et fint endpoint til POST /api/data, men din server aner ikke hvad den skal gøre ved OPTIONS /api/data.
I Network-tab ser du:
- OPTIONS request → status 404 eller 405 (Method Not Allowed).
- Console: noget i stil med “CORS preflight channel did not succeed”.
Løsningen er: din backend skal svare fornuftigt på OPTIONS-kald for de ruter, hvor der kommer CORS preflight.
Eksempel i Express
import express from "express";
import cors from "cors";
const app = express();
app.use(cors({
origin: "http://localhost:5173",
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "Authorization"],
}));
app.options("/api/data", cors()); // svar på preflight
app.post("/api/data", (req, res) => {
res.json({ ok: true });
});
Mange frameworks håndterer OPTIONS automatisk, hvis du aktiverer deres CORS middleware rigtigt. Men hvis du selv har rullet din server, skal du eksplicit tillade OPTIONS.
Fejl 2 – Forkert Access-Control-Allow-Origin
Typisk symptom: din request rammer serveren fint (status 200), men i Console står der CORS-fejl, og du får ingen data i JavaScript.
Network → klik på request → kig under Response Headers:
Access-Control-Allow-Origin: *eller en anden origin end den du kører fra.
Browseren sammenligner:
- Origin header i din request, fx
http://localhost:5173. - Access-Control-Allow-Origin i svaret.
De skal matche (eller være * i nogle tilfælde). Gør de ikke det, bliver svaret blokeret, selv om serveren synes alt er fint.
Løsning, hvis du vil tillade én origin
app.use(cors({
origin: "http://localhost:5173",
}));
Eller hvis du kører React dev server/Vite på en anden port, så skift origin, fx http://localhost:3000.
Dynamisk origin (men med omtanke)
const allowedOrigins = [
"http://localhost:5173",
"https://mit-frontend.dk",
];
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
return callback(null, true);
}
return callback(new Error("Not allowed by CORS"));
},
}));
Her tillader du en lille whitelist. Det er typisk et bedre mønster end bare at smide stjernen på alt.
Fejl 3 – Credentials, cookies og hvorfor wildcard ikke virker
Hvis du har loginkager (cookies), sessions eller fetch(..., { credentials: 'include' }), kan du hurtigt få en særlig irriterende CORS fejl.
Reglen er:
- Hvis du bruger credentials (cookies, HTTP auth), må
Access-Control-Allow-Originikke være*. - Serveren skal også sende
Access-Control-Allow-Credentials: true.
Et klassisk setup der fejler:
// frontend
fetch("http://localhost:3000/api/me", {
credentials: "include",
});
// backend-response headers
Access-Control-Allow-Origin: *
Browseren siger stop, selv om alt andet er ok.
Sådan konfigurerer du det rigtigt
// backend (Express + cors)
app.use(cors({
origin: "http://localhost:5173", // specifik origin
credentials: true,
}));
Og i fetch:
fetch("http://localhost:3000/api/me", {
credentials: "include",
});
Hvis du vil nørde detaljerne, er MDN ret god her, fx artiklen om CORS. Men start med at sikre tre ting i Network-tab: origin matcher, Allow-Credentials: true og at du faktisk sender credentials: 'include' fra frontend.
Fejl 4 – Manglende headers i Access-Control-Allow-Headers
Den her viser sig typisk som en preflight der fejler, selv om din origin og metode ser rigtige ud.
I Network-tab, kig på OPTIONS-requestet. Under Request Headers ser du:
Access-Control-Request-Headers: authorization, content-type(for eksempel).
Det betyder: “Hej server, jeg vil gerne sende de her headers i mit rigtige request, er det ok?”. Serveren skal svare med et sæt, der mindst indeholder dem:
Access-Control-Allow-Headers: authorization, content-type
Hvis den header mangler, eller ikke inkluderer fx authorization, så siger browseren nej.
Løsning i Express-cors
app.use(cors({
origin: "http://localhost:5173",
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "Authorization"],
}));
Hvis du retter i din frontend og tilføjer nye custom headers, så husk at opdatere backendens Access-Control-Allow-Headers. Det er en af de der små ting, man glemmer, når man er dybt begravet i fetch-kald.
Fejl 5 – Redirects og CORS, hvor problemet gemmer sig
CORS + redirects kan være lidt sneaky, fordi du i Network-tab kan se en 301/302, og så et request videre til en ny URL.
Eksempel:
- Du kalder
http://api.localhost/api/data. - Serveren svarer med 301 til
https://api.localhost/api/data. - Dine CORS headers sidder kun på det endelige svar, ikke på redirect-svaret.
Nogle browsere er ret strikse med CORS og redirects, specielt med credentials. Du kan også komme ud i at din preflight faktisk går til én URL, og din rigtige request går til en anden.
Sådan tjekker du det
- Kig i Network på alle led i kæden, ikke kun det sidste.
- Tjek om både redirect-svar og det endelige svar har korrekte CORS headers.
Hvis du kan, så peg din frontend direkte mod den endelige URL, i stedet for at stole på redirect. Det gør fejlsøgning og CORS meget mindre mærkeligt.
Fejl 6 – Dev proxy vs produktion, hvor CORS “virker” lokalt
En af de sjoveste (og mest irriterende) oplevelser er, når alt bare spiller i dev, og så eksploderer det i prod med CORS fejl.
Hvis du bruger noget som Vite, Create React App eller lignende, har du ofte en dev proxy-konfiguration, der sørger for, at din browser kun ser én origin.
Eksempel i Vite:
// vite.config.js
export default defineConfig({
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
},
},
},
});
Din frontend kalder bare /api/..., og Vite sender det videre til backend. Fra browserens synspunkt er det samme origin, så ingen CORS.
I produktion er din frontend måske på https://app.mitsite.dk og dit API på https://api.mitsite.dk. Nu er det pludselig cross-origin, og du opdager at din backend slet ikke var sat op til CORS.
Hvordan du undgår falsk tryghed
- Test også lokalt uden dev proxy, fx ved at kalde API’et med fuld URL fra frontend.
- Sæt CORS op i backend, selvom dev proxy gør det “unødvendigt” lokalt.
- Overvej at køre backend på en anden origin i dev, så du fanger CORS issues tidligt.
Jeg har en dårlig vane med at glemme CORS-konfiguration i backend, når proxyen bare virker. Jeg prøver at tvinge mig selv til mindst én gang at køre frontend direkte mod API’et lokalt, inden jeg kalder noget “klart”.
En lille CORS tjekliste til backend
Det her er den tjekliste, jeg selv burde have haft åbent første gang, jeg fik en CORS fejl i et fetch kald.
1. Origin
- Har du sat
Access-Control-Allow-Origintil den korrekte frontend-URL? - Matcher den præcis det, du ser i Origin-headeren i Network-tab?
2. Metoder
- Står der fx
Access-Control-Allow-Methods: GET, POST, hvis du bruger dem? - Hvis du laver PUT/DELETE/PATCH, har du husket at whiteliste dem?
3. Headers
- Sender du custom headers fra frontend, fx
AuthorizationellerX-Client? - Har du tilføjet dem i
Access-Control-Allow-Headers?
4. Credentials
- Bruger du cookies/sessions eller
credentials: 'include'i fetch? - Har du så både
Access-Control-Allow-Credentials: trueog en specifik origin (ikke*)?
5. OPTIONS
- Svarer din backend 200 på preflight (OPTIONS) for de ruter, du kalder?
- Er CORS headersne også til stede i svaret på OPTIONS?
Hvis du vil bygge det mere systematisk, kan du tage et kig på vores andre artikler, fx om miljøvariabler eller om at få et lille CI/CD-setup. De hjælper med at få forskellen på dev/prod på plads, som også spiller ind på CORS.
Sikkerhed – hvad du ikke bare skal åbne for
Det kan være fristende at skrive Access-Control-Allow-Origin: * og komme videre. Det kender jeg godt. Men CORS er i høj grad en sikkerhedsfeature, ikke kun en irritationsfaktor.
Nogle tommelfingerregler:
- Åbn kun op for origins, du stoler på. Brug whitelist, ikke alt muligt.
- Brug ikke
*sammen med credentials. Det er direkte imod specifikationen af en grund. - Lad være med at tillade alle metoder og alle headers bare fordi det er nemt.
Hvis dit API på sigt bliver offentligt, kan det være værd at kigge på OWASP’s anbefalinger til CORS og API-sikkerhed. MDN har også konkrete eksempler på både gode og dårlige CORS konfigurationer, som er værd at læse igennem, når du ikke lige sidder midt i en deadline.
En lille CORS decision tree direkte i DevTools
Jeg ender tit med at lave den samme mentale tjekliste, så her er den som en slags simpel decision tree, der starter i Chrome:
Trin 1 – Se på Network
- Find requestet der fejler.
- Tjek: er der også et OPTIONS-request?
Trin 2 – Hvis OPTIONS fejler
- Status 404/405? Du mangler en OPTIONS handler.
- Status 200, men uden CORS headers? Tilføj CORS middleware også for OPTIONS.
Trin 3 – Hvis OPTIONS er ok, men main request blokeres
- Matcher
Access-Control-Allow-Origindin frontend origin? - Bruger du credentials? Tjek
Allow-Credentialsog at origin ikke er*. - Bruger du custom headers? Tjek
Access-Control-Allow-Headers.
Trin 4 – Hvis alt ser rigtigt ud, men det virker kun i dev
- Bruger du dev proxy (Vite, CRA)? Test uden proxy.
- Er URL’er og origins de samme i prod, som du troede?
- Er der redirects i spil, som mangler CORS headers?
Den tager sjældent mere end 10 minutter, når du først har prøvet den et par gange. Derfra handler det mest om at opsætte din backend pænt, så du ikke skal rode med CORS hver uge.
Til sidst – den aften hvor CORS slog min pixel art
Jeg havde engang et lille hobbyprojekt hvor jeg tegnede pixel art i browseren og gemte det i et lille API på en gratis server. Hele UI’et var på én origin, og API’et på en anden, og jeg kunne ikke få det til at gemme i produktion.
Jeg var overbevist om, at min database var nede. I virkeligheden manglede der bare Access-Control-Allow-Credentials: true og en specifik origin, fordi jeg havde sat sessions på uden at tænke over det. Jeg fandt det først, da jeg klikkede mig igennem preflight-requestet i Network og læste headers linje for linje.
Siden da har jeg haft en lidt mere afslappet relation til CORS. Det føles dramatisk, når det fejler, men hvis du tør klikke dig rundt i Chrome DevTools og kigge på de faktiske headers, er det mere som at løse et lille puslespil end som en sort boks, der bare siger “Access blocked”.









Send kommentar
Du skal være logget ind for at skrive en kommentar.