Twój frontend działa na http://localhost:5173, API na http://localhost:4000, a zapytanie fetch, które bez problemu działało w Postmanie, w przeglądarce kończy się ścianą czerwonego tekstu: Access to fetch at 'http://localhost:4000/api/users' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. Całkiem możliwe, że twój serwer obsłużył to zapytanie i zwrócił 200. Przeglądarka po prostu odmówiła przekazania odpowiedzi do twojego JavaScriptu.

Ta odmowa to same-origin policy. CORS to sposób, w jaki serwer mówi przeglądarce, żeby złagodziła tę zasadę dla konkretnych stron. Każda poprawka poniżej sprowadza się do jednego nagłówka odpowiedzi, którego przeglądarka nie dostała.

Co właściwie blokuje same-origin policy

Origin to kombinacja schematu, hosta i portu. http://localhost:5173 i http://localhost:4000 to różne origin, bo różni się port, mimo że host i schemat się zgadzają. https://api.example.com i http://api.example.com to też różne origin, bo różni się schemat: jedna działa przez TLS, druga nie.

Same-origin policy to zasada przeglądarki, nie serwera. Skrypt działający na jednej origin nie może odczytać odpowiedzi zapytania wysłanego do innej origin, chyba że ta origin wyraźnie na to pozwoli. Zwykłe zapytanie GET i tak trafia do serwera, który je przetwarza: widać je w logach serwera i w zakładce Network z prawdziwym kodem statusu. To, co przeglądarka zatrzymuje, to treść odpowiedzi. Dlatego błąd CORS wygląda jak problem po stronie klienta, podczas gdy serwer nie zgłasza niczego złego.

Chodzi o to, żeby strona na evil.example nie mogła użyć twojej zalogowanej sesji w przeglądarce do odczytania odpowiedzi z API twojego banku. Bez tej zasady każda odwiedzona strona mogłaby wysyłać uwierzytelnione zapytania do dowolnej innej strony i czytać odpowiedzi.

Co pozwala CORS, a czego nie chroni

Cross-Origin Resource Sharing to wyjątek od tej zasady, który serwer musi świadomie włączyć. Dodaje nagłówki odpowiedzi określające, które origin mogą czytać jego odpowiedzi, a przeglądarka je respektuje zamiast blokować.

Serwer niczego nie odrzuca. Nagłówki CORS to instrukcje dla przeglądarki, nie kontrola dostępu. Zapytanie curl, wywołanie backend-to-backend czy natywna aplikacja mobilna trafiają do tego samego endpointu bez żadnej weryfikacji CORS. Jeśli twoje API potrzebuje ochrony, to zadanie dla uwierzytelniania i autoryzacji.

Kiedy przeglądarka wysyła zapytanie preflight CORS

Przeglądarka dzieli każde zapytanie cross-origin na jeden z dwóch rodzajów, i tylko jeden z nich najpierw pyta o pozwolenie.

Zapytanie jest simple, gdy spełnione jest wszystko poniżej:

  • Metoda to GET, HEAD lub POST.
  • Jedyne ustawione nagłówki to te, które przeglądarka ustawia automatycznie, plus krótka dozwolona lista (Accept, Accept-Language, Content-Language, Content-Type, Range).
  • Jeśli Content-Type jest ustawiony, jego wartość to application/x-www-form-urlencoded, multipart/form-data lub text/plain.

Zapytanie simple leci od razu, a przeglądarka sprawdza nagłówki odpowiedzi, żeby zdecydować, czy twój skrypt dostanie body. Wszystko inne jest preflighted: PUT lub DELETE, body JSON (Content-Type: application/json), albo niestandardowy nagłówek jak Authorization czy X-API-Key. Zanim wyśle właściwe zapytanie, przeglądarka wysyła zapytanie OPTIONS, pytając serwer, co ten dopuszcza:

1
2
3
4
5
OPTIONS /api/users HTTP/1.1
Host: localhost:4000
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type, authorization

Serwer odpowiada tym, na co pozwala. Dopiero jeśli ta odpowiedź obejmuje rzeczywiste zapytanie, przeglądarka je wysyła:

1
2
3
4
5
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

Access-Control-Max-Age mówi, przez ile sekund przeglądarka może zapamiętać tę odpowiedź w cache i pominąć preflight przy kolejnym pasującym zapytaniu. API JSON wysyłające bearer token wywołuje preflight niemal przy każdym wywołaniu, więc ten nagłówek oszczędza jedną turę komunikacji za każdym razem. Przeglądarki to ograniczają: Chromium nie cache’uje dłużej niż 7200 sekund, niezależnie od wysłanej wartości.

Nagłówki odpowiedzi CORS i co robi każdy z nich

NagłówekCo robi
Access-Control-Allow-OriginOrigin uprawniona do odczytu odpowiedzi: konkretna origin albo * dla dowolnej. * jest odrzucane przy zapytaniach z poświadczeniami.
Access-Control-Allow-MethodsMetody dozwolone dla tego zasobu, zwracane w odpowiedzi na preflight.
Access-Control-Allow-HeadersDozwolone nagłówki zapytania, zwracane w odpowiedzi na preflight. Authorization musi być wymieniony po nazwie; * go nie obejmuje.
Access-Control-Allow-Credentialstrue pozwala przeglądarce udostępnić odpowiedź zapytaniu z poświadczeniami (cookies, uwierzytelnianie HTTP). Wymaga konkretnej origin, nie *.
Access-Control-Expose-HeadersNagłówki odpowiedzi, poza domyślną safelist, które JavaScript może odczytać.

Zasada dotycząca poświadczeń to ta, która najczęściej wprowadza w błąd. “Poświadczenia” oznaczają tu cookies i uwierzytelnianie HTTP zarządzane przez przeglądarkę, wysyłane przy wywołaniu fetch(url, { credentials: 'include' }). Przy takim zapytaniu serwer musi wysłać Access-Control-Allow-Credentials: true i podać dokładną origin w Access-Control-Allow-Origin. Odpowiedź z poświadczeniami i Access-Control-Allow-Origin: * zostanie odrzucona, nawet jeśli wszystkie inne nagłówki są poprawne.

Bearer token, który sam ustawiasz w nagłówku Authorization, nie jest poświadczeniem w tym sensie. To niestandardowy nagłówek: wywołuje preflight i musi pojawić się w Access-Control-Allow-Headers, ale nie potrzebuje Allow-Credentials.

Jak naprawić błędy CORS w Express

Pakiet cors ustawia te nagłówki za ciebie.

1
npm install cors
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const express = require("express");
const cors = require("cors");

const app = express();

app.use(
  cors({
    origin: "http://localhost:5173",
    methods: ["GET", "POST", "PUT", "DELETE"],
    allowedHeaders: ["Content-Type", "Authorization"],
    credentials: true,
  }),
);

app.get("/api/users", (req, res) => {
  res.json([{ id: 1, name: "Ada" }]);
});

app.listen(4000);

W produkcji zwykle masz więcej niż jeden uprawniony frontend. Zamiast pojedynczego stringa przekaż tablicę albo funkcję:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const allowedOrigins = ["https://app.example.com", "https://admin.example.com"];

app.use(
  cors({
    origin: (origin, callback) => {
      if (!origin || allowedOrigins.includes(origin)) {
        callback(null, true);
      } else {
        callback(new Error("Not allowed by CORS"));
      }
    },
    credentials: true,
  }),
);

Zamontowany przez app.use, cors() odpowiada na preflight OPTIONS dla każdej trasy. Jeśli montujesz go tylko na konkretnych trasach używających metody innej niż GET, dodaj app.options("/api/users", cors()), żeby preflight dla tej ścieżki też dostał odpowiedź.

Jak włączyć CORS w ASP.NET Core

Zarejestruj nazwaną policy w Program.cs i wywołaj UseCors między UseRouting a UseAuthorization. Kolejność ma znaczenie: preflight nie zawiera żadnych danych uwierzytelniających, więc jeśli autoryzacja zadziała pierwsza, odrzuci zapytanie OPTIONS, zanim CORS zdąży odpowiedzieć.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var builder = WebApplication.CreateBuilder(args);

const string AllowFrontend = "_allowFrontend";

builder.Services.AddCors(options =>
{
    options.AddPolicy(AllowFrontend, policy =>
    {
        policy.WithOrigins("http://localhost:5173")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials();
    });
});

builder.Services.AddControllers();

var app = builder.Build();

app.UseRouting();
app.UseCors(AllowFrontend);
app.UseAuthorization();
app.MapControllers();
app.Run();

WithOrigins przyjmuje tylko konkretne origin, z tego samego powodu co surowy nagłówek. Połącz AllowCredentials() z AllowAnyOrigin(), a ASP.NET Core rzuci InvalidOperationException zamiast wysłać odpowiedź, którą przeglądarka i tak by odrzuciła. Dla kontroli na poziomie endpointu umieść [EnableCors("_allowFrontend")] na kontrolerze albo akcji i wywołaj app.UseCors() bez nazwy policy: middleware nadal musi być w pipeline, żeby atrybut zadziałał.

Jak uniknąć CORS podczas developmentu dzięki proxy Vite

Na swoim laptopie najprostszym rozwiązaniem jest w ogóle przestać wysyłać zapytanie cross-origin. Vite, webpack-dev-server i większość innych dev serverów potrafi przekierować ścieżkę do twojego backendu jako proxy, dzięki czemu przeglądarka rozmawia tylko z jedną origin:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// vite.config.js
export default {
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:4000",
        changeOrigin: true,
      },
    },
  },
};

Zapytanie fetch do /api/users trafia teraz do dev servera Vite na http://localhost:5173, który przekazuje je dalej do http://localhost:4000 i zwraca odpowiedź. Przeglądarka widzi jedną origin, więc CORS nie ma czego blokować.

To wygoda na czas developmentu. Produkcyjnym odpowiednikiem tego samego pomysłu jest reverse proxy, który serwuje frontend i API z jednej publicznej origin, co całkowicie usuwa potrzebę nagłówków CORS w tym API.

Obejścia CORS, których nie warto stosować

Każde z nich sprawia, że czerwony tekst znika u ciebie, a prawdziwy problem, nagłówek, którego twoje API nie wysyła, zostaje na miejscu:

  • Access-Control-Allow-Origin: * na endpoincie korzystającym z cookies. Przeglądarka odrzuca odpowiedź z poświadczeniami, więc to nawet nie naprawia błędu. Usuń cookies, żeby wildcard zadziałał, i zostaniesz z endpointem, który odpowiada każdej stronie w internecie bez żadnej weryfikacji origin.
  • Rozszerzenie przeglądarki “CORS unblock”. Modyfikuje odpowiedzi wewnątrz twojej własnej przeglądarki. Błąd znika u ciebie i zostaje dla każdego prawdziwego użytkownika, w tym dla kolegi testującego staging z innej maszyny.
  • Uruchomienie Chrome z --disable-web-security. Wyłącza same-origin policy dla wszystkich kart tej instancji przeglądarki, nie tylko twojej aplikacji, dopóki pozostaje otwarta. Zapomniana karta na złośliwej stronie unieważnia dokładnie tę ochronę, dla której ta zasada istnieje.

Które rozwiązanie CORS wybrać

Jeśli błąd pojawia się tylko lokalnie, przy backendzie na innym porcie, użyj proxy dev servera i całkowicie pomiń konfigurację CORS.

Jeśli prawdziwi użytkownicy będą wywoływać twoje API cross-origin z osobnej domeny frontendu albo strony trzeciej, ustaw Access-Control-Allow-Origin na dokładną origin, którą zamierzasz obsługiwać. Dodaj Access-Control-Allow-Credentials tylko wtedy, gdy przeglądarka wysyła cookies. Potem otwórz zakładkę Network w DevTools i sprawdź zapytanie OPTIONS, zanim uznasz, że wszystko zadziałało: 2xx z Allow-Origin, Allow-Methods i Allow-Headers obejmującymi twoje rzeczywiste zapytanie oznacza, że kolejne zapytanie przejdzie.

Powiązane artykuły