web-dev-qa-db-fra.com

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

J'ai une page Web ( https://smartystreets.com/contact ) qui utilise jQuery pour charger certains fichiers SVG 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, j'obtiendra généralement 6 à 8 messages dans la console qui ressemblent à cette:

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 fais Command+Shift+R alors la plupart et parfois toutes les images se chargeront sans l'erreur XMLHttpRequest.

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

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: initialement, il n'y avait que <AllowedOrigin>*</AllowedOrigin>, même problème.)

Dans CloudFront, le comportement de distribution est défini pour autoriser les méthodes HTTP: GET, HEAD, OPTIONS. Les méthodes mises en cache sont les mêmes. Les en-têtes avant 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 du 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 ne récupère JAMAIS les fichiers. Safari récupérera les images après plusieurs rafraîchissements.

En utilisant curl je n'ai 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
34
SunSparc

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 de Access-Control-Allow-Origin en-tête de réponse.

Pourquoi?

Le bogue Chrome 409090 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 sera pas résolu" - ils croient que leur comportement est correct. Chrome considère que la réponse mise en cache est utilisable, apparemment car la réponse ne comprenait pas de Vary: Origin entête.

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

Et CloudFront n'ajoute pas Vary: Origin même lorsque Origin est 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 retourner Vary: Some-Header quand il n'y avait pas de Some-Header dans la demande.

Par exemple, une réponse qui contient

Vary: accept-encoding, accept-language

indique que le serveur d'origine a peut-être utilisé le Accept-Encoding et Accept-Language champs (ou leur absence) comme facteurs déterminants lors du choix du contenu de cette réponse. (italique ajouté)

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

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

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

Un serveur Origin peut envoyer Vary avec une liste de champs à deux fins:

  1. Pour informer les destinataires du cache qu'ils MUST NOT utilise 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 vraiment SHOULD retourne Vary: Origin lorsque CORS est configuré sur le compartiment, si Origin est 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 ce n'est qu'un SHOULD, pas un MUST. Encore une fois, à partir de la même section de la RFC-7231:

Un serveur Origin SHOULD envoie 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 requête autres que la méthode et la cible de la requête, ...

D'autre part, l'argument pourrait être avancé que Chrome devrait implicitement savoir que la variation de l'en-tête Origin devrait être une clé de cache car cela pourrait changer la réponse de la même manière Authorization pourrait 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 nom du champ Authorization dans Vary car 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 Origin mais cet argument n'est pas fort.


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


Solution:

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

Cela ajoute Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Origin à toute réponse de S3 qui n'a pas d'en-tête Vary. Sinon, l'en-tête Vary dans 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 d'origine sur les forums de support AWS où ce code a été initialement partagé.


La solution Lambda @ Edge ci-dessus donne un comportement entièrement correct, mais voici deux alternatives qui peuvent vous être utiles, selon 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: à 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 remplace 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 accédant à votre contenu via XHR, par exemple https://example.com, vous pouvez l'ajouter. En utilisant * est douteux, mais pourrait 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 doit pas être x-amz-*.

Disons que vous inventez le nom x-request. Donc <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à ce qu'il faut, en mettant en cache différentes versions des objets à l'aide de l'en-tête Origin ou en l'absence de celui-ci dans la clé de cache, car vous avez transféré 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.

64
Michael - sqlbot

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.

1
unixguy