Chrome S3 Cloudfront: Brak nagłówka „Access-Control-Allow-Origin” na początkowym żądaniu XHR

30

Mam stronę internetową ( https://smartystreets.com/contact ), która używa jQuery do ładowania niektórych plików SVG z S3 za pośrednictwem CDN CloudFront.

W Chrome otworzę okno Incognito oraz konsolę. Następnie załaduję stronę. Gdy strona się ładuje, zazwyczaj w konsoli pojawia się od 6 do 8 wiadomości, które wyglądają podobnie do tego:

XMLHttpRequest cannot load 
https://d79i1fxsrar4t.cloudfront.net/assets/img/feature-icons/documentation.08e71af6.svg.
No 'Access-Control-Allow-Origin' header is present on the requested resource.
Origin 'https://smartystreets.com' is therefore not allowed access.

Jeśli wykonam standardowe ładowanie strony, nawet wielokrotnie, nadal pojawiają się te same błędy. Jeśli to zrobię, Command+Shift+Rwiększość, a czasem wszystkie obrazy zostaną załadowane bez XMLHttpRequestbłędu.

Czasami nawet po załadowaniu obrazów odświeżę się, a jeden lub więcej obrazów nie załaduje się i nie zwróci XMLHttpRequestponownie tego błędu.

Sprawdziłem, zmieniłem i ponownie sprawdziłem ustawienia S3 i Cloudfront. W S3 moja konfiguracja CORS wygląda następująco:

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedOrigin>http://*</AllowedOrigin>
    <AllowedOrigin>https://*</AllowedOrigin>
    <AllowedMethod>GET</AllowedMethod>
    <MaxAgeSeconds>3000</MaxAgeSeconds>
    <AllowedHeader>Authorization</AllowedHeader>
</CORSRule>
</CORSConfiguration>

(Uwaga: początkowo miał tylko ten <AllowedOrigin>*</AllowedOrigin>sam problem).

W CloudFront zachowanie dystrybucja jest ustawiony, aby umożliwić metod HTTP: GET, HEAD, OPTIONS. Metody buforowane są takie same. Przekazywanie nagłówków jest ustawione na „Biała lista”, a ta biała lista zawiera „Nagłówki kontroli dostępu, nagłówki żądania, metoda kontroli dostępu, pochodzenie”.

Fakt, że działa po ponownym załadowaniu przeglądarki bez pamięci podręcznej, wydaje się wskazywać, że wszystko jest dobrze po stronie S3 / CloudFront, w przeciwnym razie dlaczego treść miałaby być dostarczona. Ale dlaczego więc treść nie byłaby dostarczana przy pierwszym wyświetleniu strony?

Pracuję w Google Chrome na macOS. Firefox nie ma problemu z otrzymywaniem plików za każdym razem. Opera NIGDY nie pobiera plików. Safari zbierze obrazy po kilku odświeżeniach.

Korzystanie curlNie mam żadnych problemów:

curl -I -H 'Origin: smartystreets.com' https://d79i1fxsrar4t.cloudfront.net/assets/img/phone-icon-outline.dc7e4079.svg

HTTP/1.1 200 OK
Content-Type: image/svg+xml
Content-Length: 508
Connection: keep-alive
Date: Tue, 20 Jun 2017 17:35:57 GMT
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET
Access-Control-Max-Age: 3000
Last-Modified: Thu, 15 Jun 2017 16:02:19 GMT
ETag: "dc7e4079f937e83291f2174853adb564"
Cache-Control: max-age=31536000
Expires: Wed, 01 Jan 2020 23:59:59 GMT
Accept-Ranges: bytes
Server: AmazonS3
Vary: Origin,Access-Control-Request-Headers,Access-Control-Request-Method
Age: 4373
X-Cache: Hit from cloudfront
Via: 1.1 09fc52f58485a5da8e63d1ea27596895.cloudfront.net (CloudFront)
X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g==

Niektórzy sugerują, że usunę dystrybucję CloudFront i odtworzę ją. Wydaje się to dość trudnym i niewygodnym rozwiązaniem.

Co powoduje ten problem?

Aktualizacja:

Dodanie nagłówków odpowiedzi z obrazu, którego nie udało się załadować.

age:1709
cache-control:max-age=31536000
content-encoding:gzip
content-type:image/svg+xml
date:Tue, 20 Jun 2017 17:27:17 GMT
expires:2020-01-01T23:59:59.999Z
last-modified:Tue, 11 Apr 2017 18:17:41 GMT
server:AmazonS3
status:200
vary:Accept-Encoding
via:1.1 022c901b294fedd7074704d46fce9819.cloudfront.net (CloudFront)
x-amz-cf-id:i0PfeopzJdwhPAKoHpbCTUj1JOMXv4TaBgo7wrQ3TW9Kq_4Bx0k_pQ==
x-cache:Hit from cloudfront
SunSparc
źródło
Masz rację - usuwanie i odtwarzanie jest ekstremalne i po prostu nigdy nie powinno być konieczne. Czy możesz pokazać nam nagłówki żądań i odpowiedzi przeglądarki w przypadku nieudanego żądania? A może udane żądanie dokładnie tego samego obiektu?
Michael - sqlbot
@ Michael-sqlbot, miałem nadzieję, że odwiedzisz adres URL ( smartystreets.com/contact ) i zobaczysz, czy to samo dzieje się na twoim komputerze. :) Interesującą rzeczą dotyczącą błędów jest to, że oprócz błędu w konsoli przeglądarka zgłasza stan 200, powołując się na to, że używa obrazu „(z pamięci podręcznej dysku)”, co nie powinno być możliwe w Incognito, I myśl. Nawet po wyczyszczeniu lokalnej pamięci podręcznej.
SunSparc
1
Tak, ludzie tak często „tworzą” nazwy domen (które okazują się prawdziwymi witrynami, ale nie stroną, o której mowa), że początkowo nie zdawałem sobie sprawy, że podałeś właściwy, prawidłowy link do swojej witryny. Dzięki za to, możesz zignorować moją prośbę. Mogę zduplikować problem. Wygląda to na problem po stronie klienta. Gonię teorię.
Michael - sqlbot
Myślę, że możesz mieć rację, jeśli chodzi o problem po stronie klienta. Obrazy są połączone ze znacznikami A w kodzie HTML, a następnie wygląda na to, że są ponownie wymagane w jQuery. Być może błąd pochodzi z jednego połączenia, a 200 z drugiego.
SunSparc
1
Dokładnie tak uważam. Chrome i S3 oddziałują w sposób, który przerywa żądanie CORS następujące po żądaniu innego niż CORS dla tego samego obiektu. Prawdopodobnie oba są w błędzie ... ale prawdopodobnie żadne z nich nie jest w błędzie. Nie sądzę, że można to naprawić bez przechowywania dwóch kopii obiektu z różnymi kluczami ... lub używania dwóch różnych dystrybucji CloudFront (różnych nazw hostów), aby nie składać zarówno żądania CORS, jak i żądania innego niż CORS. Napiszę ze szczegółami, jak doszedłem do tego wniosku, jeśli chcesz.
Michael - sqlbot

Odpowiedzi:

55

Składasz dwa żądania dotyczące tego samego obiektu, jeden z HTML, jeden z XHR. Drugi kończy się niepowodzeniem, ponieważ Chrome używa buforowanej odpowiedzi z pierwszego żądania, które nie ma Access-Control-Allow-Originnagłówka odpowiedzi.

Czemu?

Błąd chromu 409090 Żądanie krzyżowego pochodzenia z pamięci podręcznej nie powiodło się po buforowaniu zwykłego żądania opisuje ten problem i jest to „nie można naprawić” - uważają, że ich zachowanie jest prawidłowe. Chrome uważa buforowaną odpowiedź za przydatną, najwyraźniej dlatego, że nie zawierała Vary: Originnagłówka.

Ale S3 nie zwraca, Vary: Origingdy obiekt jest żądany bez Origin:nagłówka żądania, nawet gdy CORS jest skonfigurowany w segmencie. Vary: Originjest wysyłany tylko wtedy, gdy Originnagłówek jest obecny w żądaniu.

A CloudFront nie dodaje, Vary: Originnawet gdy Originjest na białej liście do przesyłania, co z definicji powinno oznaczać, że zmiana nagłówka może modyfikować odpowiedź - to jest powód, dla którego przekierowujesz i buforujesz nagłówki żądań.

CloudFront dostaje przepustkę, ponieważ jego odpowiedź byłaby poprawna, gdyby S3 były bardziej poprawne, ponieważ CloudFront zwraca to, gdy jest dostarczone przez S3.

S3, trochę bardziej niepewny. Zwrot nie jest błędny,Vary: Some-Header gdy nie ma go Some-Headerw żądaniu.

Na przykład odpowiedź, która zawiera

Vary: accept-encoding, accept-language

wskazuje, że serwer źródłowy mógł użyć pola żądania Accept-Encodingi Accept-Languagepól (lub ich braku) jako czynników determinujących przy wyborze treści dla tej odpowiedzi. (podkreślenie dodane)

https://tools.ietf.org/html/rfc7231#section-7.1.4

Oczywiście Vary: Some-Absent-Headerjest poprawny, więc S3 byłby poprawny, gdyby dodał Vary: Origindo swojej odpowiedzi, jeśli skonfigurowano CORS, ponieważ to rzeczywiście mogłoby zmienić odpowiedź.

Wygląda na to, że Chrome postąpiłby właściwie. Lub, jeśli nie zrobi to dobrze w tym przypadku, naruszałoby to MUST NOT. Z tej samej sekcji:

Serwer pochodzenia może wysłać Varylistę pól do dwóch celów:

  1. Aby poinformować odbiorców pamięci podręcznej, że MUST NOTużywają tej odpowiedzi do zaspokojenia późniejszego żądania, chyba że późniejsze żądanie ma takie same wartości dla wymienionych pól, jak pierwotne żądanie (sekcja 4.1 [RFC7234]). Innymi słowy, Vary rozwija klucz pamięci podręcznej wymagany do dopasowania nowego żądania do zapisanego wpisu pamięci podręcznej.

...

Tak więc S3 naprawdę SHOULDpowraca, Vary: Origingdy CORS jest skonfigurowany w segmencie, jeśli Originnie ma go w żądaniu, ale tak nie jest.

Mimo to S3 nie jest całkowicie zły, że nie zwraca nagłówka, ponieważ jest to tylko a SHOULD, a nie MUST. Ponownie z tej samej sekcji RFC-7231:

Serwer pochodzenia SHOULDwysyła zmienne pole nagłówka, gdy jego algorytm wyboru reprezentacji zmienia się w zależności od aspektów komunikatu żądania innych niż metoda i cel żądania ...

Z drugiej strony można argumentować, że Chrome powinien domyślnie wiedzieć, że zmiana Originnagłówka powinna być kluczem pamięci podręcznej, ponieważ może zmienić odpowiedź w ten sam sposób, Authorizationmoże zmienić odpowiedź.

... chyba że nie można przekroczyć wariancji lub serwer źródłowy został celowo skonfigurowany, aby zapobiec przezroczystości pamięci podręcznej. Na przykład nie ma potrzeby wysyłania Authorizationnazwy pola, Varyponieważ ponowne użycie przez użytkowników jest ograniczone definicją pola [...]

Podobnie ponowne wykorzystanie w różnych krajach pochodzenia jest prawdopodobnie ograniczone charakterem, Originale ten argument nie jest mocny.


tl; dr: Najwyraźniej nie można pomyślnie pobrać obiektu z HTML, a następnie pomyślnie pobrać go ponownie za pomocą żądania CORS z Chrome i S3 (z CloudFront lub bez), ze względu na specyfikę implementacji.


Obejście:

To zachowanie można obejść za pomocą CloudFront i Lambda @ Edge, używając następującego kodu jako wyzwalacza Origin Response.

To dodaje Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Origindo każdej odpowiedzi z S3, która nie ma Varynagłówka. W przeciwnym razie Varynagłówek w odpowiedzi nie zostanie zmodyfikowany.

'use strict';

// If the response lacks a Vary: header, fix it in a CloudFront Origin Response trigger.

exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    if (!headers['vary'])
    {
        headers['vary'] = [
            { key: 'Vary', value: 'Access-Control-Request-Headers' },
            { key: 'Vary', value: 'Access-Control-Request-Method' },
            { key: 'Vary', value: 'Origin' },
        ];
    }
    callback(null, response);
};

Uznanie autorstwa: Jestem także autorem oryginalnego postu na forach wsparcia AWS, gdzie ten kod został początkowo udostępniony.


Powyższe rozwiązanie Lambda @ Edge zapewnia w pełni poprawne zachowanie, ale oto dwie alternatywy, które mogą okazać się przydatne, w zależności od konkretnych potrzeb:

Alternative / Hackaround # 1: Wykuć nagłówki CORS w CloudFront.

CloudFront obsługuje niestandardowe nagłówki, które są dodawane do każdego żądania. Jeśli ustawisz Origin:dla każdego żądania, nawet te, które nie są pochodzenia krzyżowego, umożliwi to prawidłowe zachowanie w S3. Opcja konfiguracji nosi nazwę Nagłówki niestandardowego pochodzenia, a słowo „Pochodzenie” oznacza coś zupełnie innego niż oznacza w CORS. Skonfigurowanie takiego niestandardowego nagłówka w CloudFront zastępuje to, co jest wysyłane w żądaniu, określoną wartością lub dodaje go, jeśli jest nieobecny. Jeśli masz dokładnie jedno źródło dostępu do treści za pośrednictwem XHR, np. https://example.comMożesz to dodać. Korzystanie *jest wątpliwe, ale może działać w innych scenariuszach. Zastanów się dokładnie nad implikacjami.

Alternative / Hackaround # 2: Użyj parametru „dummy” ciągu zapytania, który różni się dla HTML i XHR lub jest nieobecny w jednym lub drugim. Te parametry są zwykle nazywane, x-*ale nie powinny x-amz-*.

Powiedzmy, że wymyśliłeś nazwę x-request. Tak <img src="https://dzczcexample.cloudfront.net/image.png?x-request=html">. Podczas uzyskiwania dostępu do obiektu z JS nie dodawaj parametru zapytania. CloudFront już działa dobrze, buforując różne wersje obiektów, używając Originnagłówka lub jego braku jako części klucza pamięci podręcznej, ponieważ przekazałeś ten nagłówek w swoim zachowaniu pamięci podręcznej. Problem w tym, że twoja przeglądarka tego nie wie. To przekonuje przeglądarkę, że w rzeczywistości jest to osobny obiekt, o który należy ponownie poprosić w kontekście CORS.

Jeśli korzystasz z tych alternatywnych sugestii, użyj jednej lub drugiej - nie obu.

Michael - sqlbot
źródło
5
Twoja odpowiedź ratuje życie, świetna odpowiedź. Zaoszczędziłeś mi trochę czasu.
mtyurt,
Cześć, nie używam Cloudfront dla mojego s3, więc to obejście nie pomaga, czy jest coś jeszcze, co mogę zrobić?
Jeffin
1
@Jeffin, powyższa alternatywa nr 2 będzie działać tylko dla S3, bez CloudFront. Dodanie dowolnego ?x-some-key=some-valueparametru ciągu zapytania spowoduje przekonanie przeglądarki, że żądanie jest inne.
Michael - sqlbot
1
@ Michael-sqlbot: Tak, działał jak urok
Jeffin,
1
@Lionel tak, to wygląda poprawnie.
Michael - sqlbot
1

Nie wiem, dlaczego otrzymujesz tak różne wyniki z różnych przeglądarek, ale:

X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==

Ten wiersz jest tym, co (jeśli możesz zwrócić ich uwagę) użyje CloudFront lub inżynier wsparcia, aby wykonać jedno z twoich nieudanych żądań. Jeśli żądanie dociera do serwera CloudFront, powinien mieć ten nagłówek w odpowiedzi. Jeśli tego nagłówka nie ma, to prawdopodobnie gdzieś nie powiedzie się, zanim dotrze do CloudFront.

unixguy
źródło
Dzięki, zobaczę, czy mogę uzyskać jakieś odpowiedzi na forach AWS.
SunSparc
1
Konieczne może być zapłacenie 29 USD za wsparcie programistów. To banalna kwota dla każdej firmy, biorąc pod uwagę, ile kosztuje czas osoby.
Tim
1
@ Tim, pamiętaj, że wsparcie programistów to nie tylko 29 USD. To jest cena podstawowa. Jeśli 3% miesięcznego rachunku AWS wynosi> = 29 USD, płacisz 3% zamiast podstawy.
Michael - sqlbot
Dzięki @ Michael-sqlbot, nie zdawałem sobie z tego sprawy. Wiem, że cena pomocy technicznej może się szybko sumować, gdy masz rzeczy takie jak zarezerwowane instancje, ale nigdy nie patrzyłem na ceny dla programistów, gdy masz dużo zasobów.
Tim