Chrome S3 Cloudfront: nessuna intestazione "Access-Control-Allow-Origin" sulla richiesta XHR iniziale


30

Ho una pagina web ( https://smartystreets.com/contact ) che utilizza jQuery per caricare alcuni file SVG da S3 attraverso la CDN CloudFront.

In Chrome aprirò una finestra di navigazione in incognito e la console. Quindi caricherò la pagina. Man mano che la pagina viene caricata, riceverò in genere da 6 a 8 messaggi nella console simili a questo:

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.

Se eseguo un ricaricamento standard della pagina, anche più volte, continuo a ricevere gli stessi errori. Se lo faccio Command+Shift+R, la maggior parte, e talvolta tutte, delle immagini verranno caricate senza XMLHttpRequesterrori.

A volte anche dopo che le immagini sono state caricate, mi aggiorno e una o più immagini non si caricano e restituiscono di XMLHttpRequestnuovo quell'errore.

Ho controllato, modificato e ricontrollato le impostazioni su S3 e Cloudfront. In S3 la mia configurazione CORS è simile alla seguente:

<?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>

(Nota: inizialmente aveva solo lo <AllowedOrigin>*</AllowedOrigin>stesso problema.)

Nel CloudFront il comportamento di distribuzione è impostato per consentire i metodi HTTP: GET, HEAD, OPTIONS. I metodi memorizzati nella cache sono gli stessi. Forward Headers è impostato su "Whitelist" e tale whitelist include "Access-Control-Request-Headers, Access-Control-Request-Method, Origin".

Il fatto che funzioni dopo un ricaricamento del browser senza cache sembra indicare che tutto va bene sul lato S3 / CloudFront, altrimenti perché il contenuto dovrebbe essere consegnato. Ma allora perché il contenuto non dovrebbe essere consegnato nella visualizzazione della pagina iniziale?

Sto lavorando in Google Chrome su macOS. Firefox non ha problemi a recuperare i file ogni volta. Opera non ottiene MAI i file. Safari raccoglierà le immagini dopo diversi aggiornamenti.

Utilizzando curlnon ottengo alcun problema:

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==

Alcuni hanno suggerito di eliminare la distribuzione CloudFront e ricrearla. Sembra una soluzione piuttosto dura e scomoda.

Cosa causa questo problema?

Aggiornare:

Aggiunta di intestazioni di risposta da un'immagine che non è stata caricata.

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

Hai ragione: eliminare e ricreare è estremo e semplicemente non dovrebbe mai essere necessario. Puoi mostrarci le intestazioni di richiesta e risposta del browser per una richiesta non riuscita? E forse per una richiesta riuscita dello stesso oggetto esatto?
Michael - sqlbot,

@ Michael-sqlbot, speravo in qualche modo che tu visitassi l'URL ( smartystreets.com/contact ) e vedessi se stesse succedendo la stessa cosa sul tuo computer. :) La cosa interessante degli errori è che a parte l'errore nella console, il browser riporta uno stato di 200, citando che sta usando l'immagine "(dalla cache del disco)", che non dovrebbe essere possibile con Incognito, I pensato. Anche dopo aver svuotato la cache locale.
SunSparc,

1
Sì, così spesso le persone "inventano" nomi di dominio (che si rivelano essere siti reali, ma non il sito in questione) che inizialmente non mi ero reso conto che tu avessi dato il link effettivo e corretto al tuo sito. Grazie per questo, puoi ignorare la mia richiesta. Posso duplicare il problema. Sembra un problema sul lato client. Sto inseguendo una teoria.
Michael - sqlbot,

Penso che potresti avere ragione sul fatto che si tratta di un problema sul lato client. Le immagini sono collegate con tag A nell'HTML e quindi sembrano essere nuovamente richieste in jQuery. Forse l'errore proviene da una chiamata e il 200 dall'altra.
SunSparc,

1
Questo è esattamente quello che credo sia il caso. Chrome e S3 interagiscono in modo da interrompere una richiesta CORS che segue una richiesta non CORS per lo stesso oggetto. Probabilmente, entrambi hanno torto ... ma probabilmente nessuno dei due ha torto. Non credo che tu possa risolvere questo problema senza archiviare due copie dell'oggetto con chiavi diverse ... o usando due diverse distribuzioni CloudFront (nomi host diversi) in modo da non effettuare una richiesta CORS e non CORS. Lo scriverò con i dettagli di come arrivo a questa conclusione, se vuoi.
Michael - sqlbot,

Risposte:


57

Stai facendo due richieste per lo stesso oggetto, una da HTML, una da XHR. La seconda ha esito negativo, poiché Chrome utilizza la risposta memorizzata nella cache della prima richiesta, che non ha Access-Control-Allow-Originun'intestazione di risposta.

Perché?

Chromium bug 409090 La richiesta di origine incrociata dalla cache non riuscita dopo che la richiesta regolare è stata memorizzata nella cache descrive questo problema ed è un "non risolto" - credono che il loro comportamento sia corretto. Chrome considera utilizzabile la risposta memorizzata nella cache, apparentemente perché la risposta non include Vary: Originun'intestazione.

Ma S3 non viene restituito Vary: Originquando viene richiesto un oggetto senza Origin:un'intestazione di richiesta, anche quando CORS è configurato sul bucket. Vary: Originviene inviato solo quando Originnella richiesta è presente un'intestazione.

E CloudFront non si aggiunge Vary: Originnemmeno quando Originè inserito nella whitelist per l'inoltro, il che per definizione dovrebbe significare che la modifica dell'intestazione potrebbe modificare la risposta - questo è il motivo per cui si inoltra e memorizza nella cache le intestazioni della richiesta.

CloudFront ottiene un passaggio, perché la sua risposta sarebbe corretta se gli S3 fossero più corretti, poiché CloudFront lo restituisce quando viene fornito da S3.

S3, un po 'più fuzzi. Non è sbagliato restituire Vary: Some-Headerquando non vi era alcuna Some-Headerrichiesta.

Ad esempio, una risposta che contiene

Vary: accept-encoding, accept-language

indica che il server di origine potrebbe aver utilizzato i campi Accept-Encodinge la richiesta (o la loro mancanza) come fattori determinanti nella scelta del contenuto per questa risposta. (enfasi aggiunta)Accept-Language

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

Chiaramente, Vary: Some-Absent-Headerè valido, quindi S3 sarebbe corretto se si aggiungesse Vary: Originalla sua risposta se CORS fosse configurato, dal momento che ciò avrebbe potuto variare la risposta.

E, a quanto pare, questo farebbe sì che Chrome facesse la cosa giusta. Oppure, se in questo caso non fa la cosa giusta, violerebbe a MUST NOT. Dalla stessa sezione:

Un server di origine potrebbe inviare Varycon un elenco di campi per due scopi:

  1. Informare i destinatari della cache che MUST NOTusano questa risposta per soddisfare una richiesta successiva a meno che la richiesta successiva abbia gli stessi valori per i campi elencati della richiesta originale (Sezione 4.1 di [RFC7234]). In altre parole, Vary espande la chiave cache richiesta per abbinare una nuova richiesta alla voce cache memorizzata.

...

Quindi, S3 SHOULDtornerà davvero Vary: Originquando CORS è configurato sul bucket, se Originè assente dalla richiesta, ma non lo è.

Tuttavia, S3 non è assolutamente sbagliato per non restituire l'intestazione, perché è solo un SHOULD, non un MUST. Ancora una volta, dalla stessa sezione di RFC-7231:

Un server di origine SHOULDinvia un campo di intestazione Vary quando il suo algoritmo per selezionare una rappresentazione varia in base ad aspetti del messaggio di richiesta diversi dal metodo e dalla destinazione richiesta, ...

D'altra parte, si potrebbe sostenere che Chrome dovrebbe implicitamente sapere che variare l' Originintestazione dovrebbe essere una chiave cache perché potrebbe cambiare la risposta nello stesso modo in cui Authorizationpotrebbe cambiare la risposta.

... a meno che la varianza non possa essere superata o il server di origine non sia stato deliberatamente configurato per impedire la trasparenza della cache. Ad esempio, non è necessario inviare il Authorizationnome del campo Varyperché il riutilizzo tra gli utenti è vincolato dalla definizione del campo [...]

Allo stesso modo, il riutilizzo attraverso le origini è probabilmente vincolato dalla natura di Originma questo argomento non è forte.


tl; dr: a quanto pare non è possibile recuperare correttamente un oggetto dall'HTML e quindi recuperarlo nuovamente con successo come richiesta CORS con Chrome e S3 (con o senza CloudFront), a causa delle peculiarità delle implementazioni.


Soluzione:

Questo comportamento può essere aggirato con CloudFront e Lambda @ Edge, usando il seguente codice come trigger di Origin Response.

Ciò si aggiunge Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Origina qualsiasi risposta da S3 che non ha Varyintestazione. Altrimenti, l' Varyintestazione nella risposta non viene modificata.

'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);
};

Attribuzione: sono anche l'autore del post originale sui forum di supporto AWS in cui questo codice è stato inizialmente condiviso.


La soluzione Lambda @ Edge sopra si traduce in un comportamento completamente corretto, ma qui ci sono due alternative che potresti trovare utili, a seconda delle tue esigenze specifiche:

Alternativa / Hackaround n. 1: crea le intestazioni CORS in CloudFront.

CloudFront supporta le intestazioni personalizzate che vengono aggiunte a ogni richiesta. Se si imposta Origin:su ogni richiesta, anche quelle non di origine incrociata, ciò consentirà un comportamento corretto in S3. L'opzione di configurazione si chiama Intestazioni origine personalizzate, con la parola "Origine" che significa qualcosa di completamente diverso da quello che significa in CORS. La configurazione di un'intestazione personalizzata come questa in CloudFront sovrascrive ciò che viene inviato nella richiesta con il valore specificato o lo aggiunge se assente. Se hai esattamente un'origine che accede ai tuoi contenuti su XHR, ad esempio https://example.com, puoi aggiungerlo. L'uso *è dubbio, ma potrebbe funzionare per altri scenari. Considera attentamente le implicazioni.

Alternativa / Hackaround n. 2: utilizzare un parametro di stringa di query "fittizio" che differisce per HTML e XHR o è assente dall'uno o dall'altro. Questi parametri sono in genere denominati x-*ma non dovrebbero esserlo x-amz-*.

Supponiamo che tu abbia inventato il nome x-request. Così <img src="https://dzczcexample.cloudfront.net/image.png?x-request=html">. Quando si accede all'oggetto da JS, non aggiungere il parametro query. CloudFront sta già facendo la cosa giusta, memorizzando nella cache diverse versioni degli oggetti usando l' Originintestazione o l'assenza di essa come parte della chiave della cache, perché hai inoltrato quell'intestazione nel comportamento della cache. Il problema è che il tuo browser non lo sa. Ciò convince il browser che questo è in realtà un oggetto separato che deve essere nuovamente richiesto, in un contesto CORS.

Se usi questi suggerimenti alternativi, usa l'uno o l'altro, non entrambi.


5
La tua risposta è un vero toccasana, un'ottima risposta. Mi hai fatto risparmiare un po 'di tempo.
mtyurt,

Ciao, non uso cloudfront per il mio s3, quindi questa soluzione alternativa non aiuta, c'è qualcos'altro che posso fare?
Jeffin,

1
@Jeffin, l'alternativa n. 2 sopra funzionerà solo per S3, senza CloudFront. L'aggiunta di un ?x-some-key=some-valueparametro di stringa di query arbitraria convincerà il browser che la richiesta è diversa.
Michael - sqlbot,

1
@ Michael-sqlbot: Sì, ha funzionato come un fascino
Jeffin,

1
@Lionel sì, sembra corretto.
Michael - sqlbot

1

Non so perché otterresti risultati così diversi da vari browser, ma:

X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==

Quella riga proprio lì è ciò che (se riesci a ottenere la loro attenzione) un ingegnere CloudFront o di supporto utilizzerà per seguire una delle tue richieste non riuscite. Se la richiesta arriva a un server CloudFront, dovrebbe avere questa intestazione nella risposta. Se quell'intestazione non è presente, è probabile che la richiesta non riesca da qualche parte prima di arrivare a CloudFront.


Grazie, vedrò se riesco a ottenere risposte sui forum AWS.
SunSparc,

1
Potrebbe essere necessario pagare $ 29 per il supporto degli sviluppatori. Questa è una banale quantità di denaro per qualsiasi azienda, dato il costo di una persona.
Tim

1
@Tim, nota che il supporto per gli sviluppatori non è semplicemente $ 29. Questo è il prezzo base. Se il 3% della fattura mensile AWS è> = $ 29, si paga il 3% anziché la base.
Michael - sqlbot,

Grazie @Michael-sqlbot, non me ne sono reso conto. So che il prezzo del supporto può sommarsi rapidamente quando hai cose come le istanze riservate, ma non ho mai guardato i prezzi degli sviluppatori quando hai molte risorse.
Tim
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.