Chrome S3 Cloudfront: aucun en-tête "Access-Control-Allow-Origin" sur la demande XHR initiale

30

J'ai une page Web ( https://smartystreets.com/contact ) qui utilise jQuery pour charger certains fichiers SVG à partir de S3 via le CDN CloudFront.

Dans Chrome, j'ouvrirai une fenêtre de navigation privée ainsi que la console. Ensuite, je chargerai la page. Au fur et à mesure que la page se charge, je reçois généralement 6 à 8 messages dans la console qui ressemblent à ceci:

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.

Si je fais un rechargement standard de la page, même plusieurs fois, je continue à obtenir les mêmes erreurs. Si je le fais Command+Shift+R, la plupart des images, et parfois toutes, se chargeront sans l' XMLHttpRequesterreur.

Parfois, même après le chargement des images, je rafraîchis et une ou plusieurs images ne se chargent pas et renvoient à XMLHttpRequestnouveau cette erreur.

J'ai vérifié, modifié et revérifié les paramètres sur S3 et Cloudfront. Dans S3, ma configuration CORS ressemble à ceci:

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

(Remarque: au départ, il n'y avait que le <AllowedOrigin>*</AllowedOrigin>même problème.)

Dans CloudFront le comportement de distribution est configuré pour autoriser les méthodes HTTP: GET, HEAD, OPTIONS. Les méthodes mises en cache sont les mêmes. Les en-têtes de transfert sont définis sur "Liste blanche" et cette liste blanche comprend "En-têtes de demande de contrôle d'accès, Méthode de demande de contrôle d'accès, origine".

Le fait qu'il fonctionne après un rechargement de navigateur sans cache semble indiquer que tout va bien du côté S3 / CloudFront, sinon pourquoi le contenu serait-il livré. Mais alors pourquoi le contenu ne serait-il pas livré lors de la première visualisation de la page?

Je travaille dans Google Chrome sur macOS. Firefox n'a aucun problème à récupérer les fichiers à chaque fois. Opera n'obtient JAMAIS les fichiers. Safari récupérera les images après plusieurs rafraîchissements.

En utilisant curlje ne reçois aucun problème:

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

Certains ont suggéré que je supprime la distribution CloudFront et que je la recrée. Cela semble être une solution plutôt dure et peu pratique.

Quelle est la cause de ce problème?

Mise à jour:

Ajout d'en-têtes de réponse à partir d'une image qui n'a pas pu se charger.

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
la source
Vous avez raison - supprimer et recréer est extrême et ne devrait tout simplement jamais être nécessaire. Pouvez-vous nous montrer les en-têtes de demande et de réponse du navigateur pour une demande qui a échoué? Et peut-être pour une demande réussie du même objet exact?
Michael - sqlbot
@ Michael-sqlbot, j'espérais un peu que vous visiteriez l'URL ( smartystreets.com/contact ) et verriez si la même chose se produisait sur votre machine. :) La chose intéressante à propos des erreurs est que, à part l'erreur dans la console, le navigateur signale un état de 200, citant qu'il utilise l'image "(du cache disque)", ce qui ne devrait pas être possible avec Incognito, je pensée. Même après avoir vidé le cache local.
SunSparc
1
Oui, les gens "inventent" si souvent des noms de domaine (qui se révèlent être de vrais sites, mais pas le site en question) que je ne savais pas au départ que vous aviez donné le lien réel et correct vers votre site. Merci pour cela, vous pouvez ignorer ma demande. Je peux reproduire le problème. Cela semble être un problème côté client. Je chasse une théorie.
Michael - sqlbot
1
C'est exactement ce que je pense être le cas. Chrome et S3 interagissent de manière à interrompre une demande CORS qui suit une demande non CORS pour le même objet. Sans doute, les deux ont tort ... mais sans doute, aucun d'eux n'a tort. Je ne pense pas que vous pouvez résoudre ce problème sans stocker deux copies de l'objet avec des clés différentes ... ou en utilisant deux distributions CloudFront différentes (noms d'hôtes différents) afin de ne pas faire à la fois une demande CORS et non-CORS. Je vais l'écrire avec des détails sur la façon dont j'arrive à cette conclusion, si vous le souhaitez.
Michael - sqlbot
1
Lorsque vous vous amusez avec les en-têtes, etc., ils sont mis en cache, pour revenir aux paramètres actuels, invalidez simplement ceux dans CloudFront
tarikakyol Il y a

Réponses:

57

Vous faites deux demandes pour le même objet, une à partir de HTML, une à partir de XHR. La seconde échoue, car Chrome utilise la réponse mise en cache de la première demande, qui n'a pas d'en- Access-Control-Allow-Origintête de réponse.

Pourquoi?

Chromium bug 409090 La demande d'origine croisée du cache échoue après la mise en cache de la demande régulière décrit ce problème, et c'est un "ne résoudra pas" - ils croient que leur comportement est correct. Chrome considère que la réponse mise en cache est utilisable, apparemment parce que la réponse ne comprenait pas d'en- Vary: Origintête.

Mais S3 ne retourne pas Vary: Originlorsqu'un objet est demandé sans en- Origin:tête de demande, même lorsque CORS est configuré sur le compartiment. Vary: Originn'est envoyé que lorsqu'un en- Origintête est présent dans la demande.

Et CloudFront n'ajoute rien Vary: Originmême lorsqu'il Originest sur liste blanche pour le transfert, ce qui devrait par définition signifier que la modification de l'en-tête peut modifier la réponse - c'est la raison pour laquelle vous transférez et mettez en cache les en-têtes de demande.

CloudFront obtient un laissez-passer, car sa réponse serait correcte si les S3 étaient plus corrects, car CloudFront le renvoie quand il est fourni par S3.

S3, un peu plus flou. Il n'est pas faux de revenir Vary: Some-Headerquand il n'y en avait pas Some-Headerdans la demande.

Par exemple, une réponse qui contient

Vary: accept-encoding, accept-language

indique que le serveur d'origine a peut-être utilisé les demandes Accept-Encodinget les Accept-Languagechamps (ou leur absence) comme facteurs déterminants lors du choix du contenu de cette réponse. (pas d'italique dans l'original)

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

Clairement, Vary: Some-Absent-Headerest valide, donc S3 serait correct s'il s'ajoutait Vary: Originà sa réponse si CORS est configuré, car cela pourrait en effet faire varier la réponse.

Et, apparemment, cela ferait de Chrome la bonne chose. Ou, s'il ne fait pas la bonne chose dans ce cas, il violerait a MUST NOT. De la même section:

Un serveur d'origine peut envoyer Varyavec une liste de champs à deux fins:

  1. Pour informer les destinataires du cache qu'ils MUST NOTutilisent cette réponse pour satisfaire une demande ultérieure, sauf si la demande ultérieure a les mêmes valeurs pour les champs répertoriés que la demande d'origine (Section 4.1 de [RFC7234]). En d'autres termes, Vary étend la clé de cache requise pour faire correspondre une nouvelle demande à l'entrée de cache stockée.

...

Donc, S3 SHOULDrevient vraiment Vary: Originlorsque CORS est configuré sur le compartiment, s'il Originest absent de la demande, mais ce n'est pas le cas.

Pourtant, S3 n'a pas strictement tort de ne pas retourner l'en-tête, car c'est seulement un SHOULD, pas un MUST. Encore une fois, à partir de la même section de la RFC-7231:

Un serveur d'origine SHOULDenvoie un champ d'en-tête Vary lorsque son algorithme de sélection d'une représentation varie en fonction d'aspects du message de demande autres que la méthode et la cible de la demande, ...

D'un autre côté, l'argument pourrait être avancé que Chrome devrait implicitement savoir que la variation de l'en- Origintête devrait être une clé de cache, car cela pourrait changer la réponse de la même manière Authorizationpourrait changer la réponse.

... sauf si l'écart ne peut pas être franchi ou que le serveur d'origine a été délibérément configuré pour empêcher la transparence du cache. Par exemple, il n'est pas nécessaire d'envoyer le Authorizationnom du champ Varycar la réutilisation entre les utilisateurs est limitée par la définition du champ [...]

De même, la réutilisation entre les origines est sans doute limitée par la nature de Originmais cet argument n'est pas fort.


tl; dr: Vous ne pouvez apparemment pas récupérer un objet à partir de HTML, puis le récupérer à nouveau avec une demande CORS avec Chrome et S3 (avec ou sans CloudFront), en raison de particularités dans les implémentations.


Solution de contournement:

Ce comportement peut être contourné avec CloudFront et Lambda @ Edge, en utilisant le code suivant comme déclencheur de réponse d'origine.

Cela s'ajoute Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Originà toute réponse de S3 qui n'a pas d'en- Varytête. Sinon, l'en- Varytête de la réponse n'est pas modifié.

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

Attribution: je suis également l'auteur de la publication originale sur les forums de support AWS où ce code a été initialement partagé.


La solution Lambda @ Edge ci-dessus se traduit par un comportement entièrement correct, mais voici deux alternatives qui peuvent vous être utiles, en fonction de vos besoins spécifiques:

Alternative / Hackaround # 1: Forgez les en-têtes CORS dans CloudFront.

CloudFront prend en charge les en-têtes personnalisés qui sont ajoutés à chaque demande. Si vous définissez Origin:sur chaque demande, même celles qui ne sont pas d'origine croisée, cela permettra un comportement correct dans S3. L'option de configuration est appelée en-têtes d'origine personnalisés, le mot «origine» signifiant quelque chose de complètement différent de ce qu'il signifie dans CORS. La configuration d'un en-tête personnalisé comme celui-ci dans CloudFront écrase ce qui est envoyé dans la demande avec la valeur spécifiée ou l'ajoute en cas d'absence. Si vous avez exactement une origine qui accède à votre contenu via XHR, par exemple https://example.com, vous pouvez l'ajouter. L'utilisation *est douteuse, mais peut fonctionner pour d'autres scénarios. Examinez attentivement les implications.

Alternative / Hackaround # 2: utilisez un paramètre de chaîne de requête "factice" qui diffère pour HTML et XHR ou qui est absent de l'un ou de l'autre. Ces paramètres sont généralement nommés x-*mais ne doivent pas l'être x-amz-*.

Disons que vous inventez le nom x-request. Alors <img src="https://dzczcexample.cloudfront.net/image.png?x-request=html">. Lorsque vous accédez à l'objet à partir de JS, n'ajoutez pas le paramètre de requête. CloudFront fait déjà la bonne chose, en mettant en cache différentes versions des objets en utilisant l'en- Origintête ou son absence dans le cadre de la clé de cache, car vous avez transmis cet en-tête dans votre comportement de cache. Le problème est que votre navigateur ne le sait pas. Cela convainc le navigateur qu'il s'agit en fait d'un objet distinct qui doit être demandé à nouveau, dans un contexte CORS.

Si vous utilisez ces suggestions alternatives, utilisez l'une ou l'autre - pas les deux.

Michael - sqlbot
la source
5
Votre réponse est une bouée de sauvetage, une excellente réponse. Tu m'as fait gagner du temps.
mtyurt
Salut, je n'utilise pas cloudfront pour mon s3, donc cette solution de contournement n'aide pas, puis-je faire autre chose?
Jeffin
1
@Jeffin, l'alternative n ° 2 ci-dessus fonctionnera uniquement pour S3, sans CloudFront. L'ajout d'un ?x-some-key=some-valueparamètre de chaîne de requête arbitraire convaincra le navigateur que la demande est différente.
Michael - sqlbot
1
@ Michael-sqlbot: Oui, a fonctionné comme un charme
Jeffin
1
@Lionel oui, cela semble correct.
Michael - sqlbot
1

Je ne sais pas pourquoi vous obtiendriez des résultats si différents de différents navigateurs, mais:

X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==

Cette ligne est là ce que (si vous pouvez attirer leur attention) un ingénieur CloudFront ou de support utilisera pour suivre l'une de vos demandes ayant échoué. Si la demande parvient à un serveur CloudFront, il doit avoir cet en-tête dans la réponse. Si cet en-tête n'est pas là, la demande échoue probablement quelque part avant d'arriver à CloudFront.

unixguy
la source
Merci, je vais voir si je peux obtenir des réponses sur les forums AWS.
SunSparc
1
Vous devrez peut-être payer les 29 $ pour l'assistance aux développeurs. C'est une somme d'argent insignifiante pour toute entreprise, compte tenu du coût du temps d'une personne.
Tim
1
@Tim, notez que le support développeur n'est pas seulement 29 $. Voilà le prix de base. Si 3% de votre facture mensuelle AWS est> = 29 $, vous payez 3% au lieu de la base.
Michael - sqlbot
Merci @ Michael-sqlbot, je ne m'en étais pas rendu compte. Je sais que le prix du support peut s'additionner rapidement lorsque vous avez des choses comme des instances réservées, mais je n'ai jamais regardé la tarification des développeurs lorsque vous avez beaucoup de ressources.
Tim