CORS (Cross-Origin Resource Sharing) dosyć poważnie wpływa na wydajność naszej aplikacji.
W momencie gdy po stronie klienckiej wykonujemy jedno z zapytań, które wymaga wykorzystania CORS jak na przykład:
- kontakt z API serwera przy użyciu XMLHttpRequest/Fetch API,
- import czcionek dzięki @font-face
nasza przeglądarka w tle wykonuje tak na prawdę dwa zapytania HTTP, a nie jedno – tak jak byśmy tego oczekiwali.
Pierwsze zapytanie (tak zwane: preflight request) służy w celu sprawdzenia czy w ogóle możemy wykonać nasze docelowe zapytanie.
W Chrome niestety od jakiegoś czasu preflight request są niewidoczne w zakładce Network. Aby je zobaczyć należy przejść pod stronę
chrome://flags/#out-of-blink-cors
A następnie przestawić Out of blink CORS na Disabled
W Firefoxie działa bez takich cudów.
Teraz wystarczy wykonać proste odpytanie CORS do zewnętrznego serwera:
var xhr = new XMLHttpRequest; xhr.open("GET", "https://google.com"); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send();I zobaczymy, iż nasze zapytanie zostało poprzedzone przez wykonanie zapytania OPTION w celu sprawdzenia czy jest możliwe wykonanie zapytania CORS (docelowego zapytania GET).
Jeżeli zapytanie OPTION, tak jak w tym przypadku, nie powiedzie się (dlatego, iż serwer nie zezwala na zapytanie CORS z naszej domeny) to docelowe zapytanie nie jest wykonywane, a w konsoli zobaczymy informację:
Podsumowując
Jeżeli jesteśmy na stronie A i w tle chcemy wykonać zapytanie CORS do serwera B to zawsze wykonane zostaną dwa zapytania.
Jak uniknąć preflight requests?
Skoro wiemy już w którym momencie wykonywane są preflight requests, możemy spróbować jakoś im zapobiec.
Rozwiązaniem jest stworzenie mechanizmu proxy pomiędzy zapytaniami wysyłanymi z przeglądarki (czyli przez klienta dzięki aplikacji klienckiej), a serwerem API. Zamiast wykonywać zapytania bezpośrednio do serwera, będziemy wykonywać zapytania do aplikacji klienckiej, która będzie robiła za pośrednika.
Dzięki takiemu rozwiązaniu przeglądarka widzi, iż nasze zapytania nie są zapytaniami CORS (wykonywane są do tej samej domeny na której aktualnie jesteśmy) przez co nie będzie potrzeby wykonywania preflight request.
Można to zrobić na kilka sposobów. Ja wykorzystałem w tym celu bibliotekę http-proxy-middleware. W najprostszej konfiguracji wystarczy taka konfiguracja:
var express = require('express'); var proxy = require('http-proxy-middleware'); var apiProxy = proxy('/api', { target: 'http://localhost:8080/', pathRewrite: { '^/api/': '/' }, }); var app = express(); app.use('/api', apiProxy); app.listen(3000);Teraz w naszej aplikacji klienckiej wystarczy, iż zapytania nie będziemy kierować bezpośrednio do serwera, a do naszego proxy pod adresem:
http[s]://domena_kliencka/api/[...]Repozytorium:
- Kod, który wywołuje preflight requests: https://github.com/mtszpater/cors-eliminate/tree/95a176bc43a0c0b783f31fbc6330c78728f96fd1
- Ostateczna wersja z proxy, które zapobiega preflight requests:
https://github.com/mtszpater/cors-eliminate
Na koniec – inny sposób
Można spróbować obejść to jeszcze inaczej.
Według dokumentacji: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS tak zwane Simple requests nie wywołują dodatkowych zapytań OPTION. Muszą zostać spełnione następujące warunki:
Simple requests:
One of the allowed methods:– GET
– HEAD
– POST
Apart from the headers automatically set by the user agent (for example, Connection, User-Agent, or the other headers defined in the Fetch spec as a “forbidden header name”), the only headers which are allowed to be manually set are those which the Fetch spec defines as a “CORS-safelisted request-header”, which are:
– Accept
– Accept-Language
– Content-Language
– Content-Type (but note the additional requirements below)
– DPR
– Downlink
– Save-Data
– Viewport-Width
– Width
The only allowed values for the Content-Type header are:
– application/x-www-form-urlencoded
– multipart/form-data
– text/plain
No event listeners are registered on any XMLHttpRequestUpload object used in the request; these are accessed using the XMLHttpRequest.upload property.
No ReadableStream object is used in the request.
I rzeczywiście wydaje się, iż można spróbować przerobić wszystkie nasze zapytania na takie, które dostosowują się do tych warunków.
Niestety jest to rozwiązanie dosyć żmudne i w większości sytuacji będzie to po prostu nieopłacalne. Temat został całkiem dobrze opisany w tym artykule: Killing CORS Preflight Requests on a React SPA