Jeg fik min første CORS-fejl en tirsdag aften, og så var alt i stykker

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:

  1. Din JavaScript kalder fetch('https://api.example.com/data', {...})
  2. Browseren sender først en OPTIONS-request til samme URL
  3. Serveren svarer med CORS-headers (eller ikke)
  4. Hvis svaret er ok, sender browseren den rigtige GET, POST osv.

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-data eller text/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-Type er application/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-Origin findes 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-Methods mangler den metode, du vil bruge (fx DELETE)
  • Access-Control-Allow-Headers mangler 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 curl eller 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:

  1. I fetch-kaldet:
fetch('https://api.example.com/data', {
  credentials: 'include'
});
  1. På serveren:
const corsOptions = {
  origin: 'https://minfrontend.dk',
  credentials: true
};

app.use(cors(corsOptions));
  1. 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 driller i udvikling, men så bryder alt sammen i produktion.

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å

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 manuelle res.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.

Jonas Kirkeby har skrevet kode siden han som teenager forsøgte at lave en helt simpel hjemmeside til sin fars lille vvs-firma – og endte med at sidde oppe hele natten for at få en knap til at skifte farve. Siden da har han lært sig det meste ved at prøve sig frem, kopiere andres eksempler, ødelægge dem og langsomt forstå, hvorfor tingene virker, som de gør.

Til daglig arbejder han slet ikke med IT, men bruger aftener og morgener på små projekter: en lille side til en forening, et simpelt værktøj til at holde styr på familiens madplan eller et Python-script, der rydder op i rodede filer. Det er den slags konkrete hverdags-behov, der har formet hans måde at tænke kodning på – hvad kan jeg bygge nu, som faktisk hjælper mig eller nogen, jeg kender?

På Coding Class deler Jonas de guides, han selv ville ønske, han havde haft: korte, konkrete forløb, hvor du kan se noget på skærmen efter få minutters læsning. Han viser hele vejen fra idé til færdig løsning, inklusive de typiske fejl og små snubletråde på vejen, så du ikke kun får den pæne, polerede version.

Hans mål er, at du som begynder eller let øvet hurtigt får følelsen af: “Det her kan jeg faktisk selv finde ud af” – uanset om du vil bygge din første lille hjemmeside, forstå JavaScript-funktioner eller bruge Python til at automatisere en kedelig opgave.

1 kommentar

comments user
Pernille

Jeg læste det med CORS, min datter troede jeg havde ødelagt nettet, vi grinede, jeg bruger det til vintage-biks.

Send kommentar

You May Have Missed