58 votes

Chrome S3 Cloudfront: Pas d'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 des fichiers SVG depuis S3 via le CDN CloudFront.

Dans Chrome, j'ouvrirai une fenêtre Incognito ainsi que la console. Ensuite, je chargerai la page. Au fur et à mesure que la page se charge, j'obtiendrai 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 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, si je rafraîchis la page, une ou plusieurs des images ne se chargeront pas et renverront à 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 :

    *
    http://*
    https://*
    GET
    3000
    Authorization

(Remarque : initialement, j'avais seulement *, même problème.)

Dans CloudFront, le comportement de la distribution est configuré pour autoriser les méthodes HTTP : GET, HEAD, OPTIONS. Les méthodes mises en cache sont les mêmes. Les entêtes transmis sont configurés sur "Whitelist" et cette liste blanche inclut "Access-Control-Request-Headers, Access-Control-Request-Method, Origin".

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

Je travaille sur Google Chrome sous 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 la recrée. Cela semble être une solution assez radicale et inconfortable.

Quelle est la cause de ce problème ?

Mise à jour :

Ajout des en-têtes de réponse 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

0 votes

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 requête et de réponse du navigateur pour une requête échouée ? Et peut-être pour une requête réussie du même objet exact ?

0 votes

@Michael-sqlbot, j'espérais un peu que vous visitiez l'URL (smartystreets.com/contact) et voyiez si la même chose se produisait sur votre machine. :) La chose intéressante à propos des erreurs est qu'en dehors de l'erreur dans la console, le navigateur signale un statut de 200, indiquant qu'il utilise l'image "(du cache disque)", ce qui ne devrait pas être possible avec l'Incognito, je pensais. Même après avoir vidé le cache local.

1 votes

Oui, les gens "inventent" si souvent des noms de domaine (qui s'avèrent être de vrais sites, mais pas le site en question) que je n'ai pas réalisé 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 du côté du client. Je suis en train d'explorer une théorie.

108voto

Michael - sqlbot Points 21488

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

Pourquoi?

Bug Chromium 409090 Demande cross-domain à partir du cache échouant après qu'une demande régulière soit mise en cache décrit ce problème, et c'est un "ne sera pas corrigé" -- ils pensent que leur comportement est correct. Chrome considère que la réponse mise en cache est utilisable, apparemment parce que la réponse n'incluait pas d'en-tête Vary: Origin.

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

Et CloudFront n'ajoute pas de Vary: Origin même lorsque Origin est autorisé pour être transmis, ce qui devrait par définition signifier que la variation de l'en-tête pourrait modifier la réponse -- c'est la raison pour laquelle vous transmettez et mettez en cache en fonction des en-têtes de demande.

CloudFront passe, car sa réponse serait correcte si celle de S3 l'était davantage, puisque CloudFront retourne ceci lorsqu'il est fourni par S3.

S3, un peu moins clair. Ce n'est pas incorrect 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 pourrait avoir utilisé les champs Accept-Encoding et Accept-Language de la demande comme facteurs déterminants lors du choix du contenu pour cette réponse. (mise en évidence ajoutée)

https://www.rfc-editor.org/rfc/rfc7231#section-7.1.4

Clairement, Vary: Some-Absent-Header est valable, 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 que Chrome fasse la bonne chose. Ou, s'il ne fait pas la bonne chose dans ce cas, il violerait un MUST NOT. De la même section:

Un serveur d'origine peut envoyer Vary avec une liste de champs pour deux raisons :

  1. Pour informer les destinataires du cache qu'ils DOIVENT PAS utiliser cette réponse pour satisfaire une demande ultérieure à moins que la demande ultérieure ait les mêmes valeurs pour les champs listés que la demande originale (Section 4.1 de [RFC7234]). En d'autres termes, Vary élargit la clé de cache nécessaire pour correspondre à une nouvelle demande à l'entrée du cache enregistrée.

...

Donc, S3 devrait vraiment retourner Vary: Origin lorsque CORS est configuré sur le compartiment, si Origin est absent de la demande, mais ce n'est pas le cas.

Cependant, S3 n'a pas strictement tort de ne pas retourner l'en-tête, car ce n'est qu'un DEVRAIT, pas un DOIT. Encore une fois, de la même section de RFC-7231 :

Un serveur d'origine DEVRAIT envoyer un champ d'en-tête Vary lorsque son algorithme pour choisir une représentation varie en fonction d'aspects de la demande autre que la méthode et la cible de la demande, ...

D'un autre côté, on pourrait soutenir que Chrome devrait implicitement savoir que faire varier l'en-tête Origin devrait être une clé de cache car cela pourrait changer la réponse de la même manière que Authorization pourrait changer la réponse.

...à moins que la variance ne puisse être franchie ou que le serveur d'origine ait é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 à travers les utilisateurs est contrainte par la définition du champ [...]

De même, la réutilisation à travers les origines est indiscutablement limitée par la nature de Origin, mais cet argument n'est pas un argument solide.


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


Contournement:

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

Ceci 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';

// Si la réponse manque d'un en-tête Vary, corrigez-le dans un déclencheur de réponse d'origine CloudFront.

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 du

13 votes

Votre réponse est un sauveur de vie, excellente réponse. Vous m'avez fait gagner un temps précieux.

0 votes

Salut, je n'utilise pas Cloudfront pour mon S3 donc cette solution de contournement ne m'aide pas, y a-t-il autre chose que je puisse faire?

3 votes

@Jeffin, l'alternative n°2 ci-dessus fonctionnera pour S3 seul, sans CloudFront. Ajouter un paramètre de chaîne de requête arbitraire ?x-some-key=some-value convaincra le navigateur que la requête est différente.

7voto

James Points 81

À partir de novembre 2021, CloudFront prend en charge directement les politiques de en-têtes de réponse. Celles-ci incluent CORS, les en-têtes de sécurité et personnalisés. Il n'est plus nécessaire d'injecter des en-têtes personnalisés via Lambda@Edge ou CloudFront Functions.

Mieux encore, il n'est plus nécessaire d'ajouter Vary comme en-tête personnalisé. La nouvelle implémentation CORS dans les politiques d'en-tête inclut une logique supplémentaire pour définir les en-têtes appropriés tels que Vary selon la norme d'interrogation.

0 votes

Grande nouvelle! Merci pour le partage! :)

0 votes

Ceci devrait être la nouvelle réponse acceptée. (La réponse de Michael-sqlbot de 2017 est excellente, mais le nouveau support des politiques d'en-tête de réponse est révolutionnaire.)

2voto

unixguy Points 356

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

X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g==

Cette ligne, si vous arrivez à attirer leur attention, est ce qu'un ingénieur de CloudFront ou du Support utilisera pour suivre une de vos requêtes échouées. Si la requête parvient à un serveur CloudFront, elle devrait avoir cet en-tête dans la réponse. Si cet en-tête n'est pas présent, alors la requête échoue probablement avant d'atteindre CloudFront.

0 votes

Merci, je vais voir si je peux obtenir des réponses sur les forums AWS.

1 votes

Vous devrez peut-être payer les 29 $ pour bénéficier du support du développeur. C'est une somme négligeable pour toute entreprise, étant donné le coût du temps d'une personne.

1 votes

@Tim, notez que le support du développeur n'est pas simplement de 29 $. C'est le prix de base. Si 3% de votre facture mensuelle AWS est >= 29 $, vous payez 3% au lieu de la base.

1voto

Chuck Ross Points 56

La solution acceptée aborde le problème, mais ce n'est pas la plus performante, en particulier pour les distributions CloudFront qui servent du contenu dynamique. La mise en place du cache des en-têtes avec une liste blanche entraîne le stockage en cache de CloudFront de plusieurs versions de l'objet demandé en fonction de l'en-tête. Cela signifie qu'internement, CloudFront peut avoir besoin de récupérer l'objet depuis l'origine S3 plusieurs fois. Le transfert de données de S3 à CloudFront est gratuit, mais cela n'explique pas la latence supplémentaire.

Une solution alternative serait de désactiver la configuration CORS sur le bucket S3, et instead de définir manuellement les en-têtes CORS en utilisant une fonction Lambda@Edge configurée sur la réponse du visualiseur. La fonction pourrait ressembler à ceci :

'use strict';

const AllowedOriginRegex = /^(.*\.)?example\.com$/;

exports.handler = async (event = {}) => {
  const request = event.Records[0].cf.request;
  const response = event.Records[0].cf.response;

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

  const origin = request.headers.origin && request.headers.origin[0].value;
  if (origin && AllowedOriginRegex.test(origin)) {
    response.headers['access-control-allow-origin'] = [
      {key: 'Access-Control-Allow-Origin', value: origin},
    ];
    response.headers['access-control-allow-methods'] = [
      {key: 'Access-Control-Allow-Methods', value: 'GET, HEAD'},
    ];
    response.headers['access-control-max-age'] = [
      {key: 'Access-Control-Max-Age', value: '3600'},
    ];
  }

  return response;
}

0 votes

Je m'attends à ce que cela soit beaucoup moins performant dans l'ensemble et plus cher que ma solution car le déclencheur Lambda doit être déclenché pour chaque réponse. Ma solution permet de stocker la réponse modifiée du déclencheur Lambda dans le cache CloudFront pour être rejouée lors de futures demandes avec les en-têtes de requête CORS pertinents définis sur des valeurs identiques, et la mise en cache de plusieurs versions sur cette base est exactement le comportement souhaité.

0 votes

const origin = request.headers.origin; est incorrect, car request.headers.origin sera soit indéfini soit un tableau, pas une chaîne de caractères.

0 votes

En ce qui concerne les performances, je pense que cela dépend vraiment du type de contenu que vous servez. Pour mon cas d'utilisation spécifique, je sers du contenu dynamique depuis S3 en utilisant CF, et le contenu peut être assez volumineux (~100 Mo). Si un navigateur envoie plusieurs demandes avec différentes en-têtes Origin, CloudFront devra récupérer l'objet depuis S3 deux fois.

1voto

Il existe une autre solution plus simple qui fonctionne pour moi avec l'utilisation d'un attribut HTML appelé crossorigin='anonymous' comme détaillé ici. Fondamentalement, vous pouvez ajouter cet attribut comme ceci:

![](votre_url_image_ici)

et cela rend essentiellement votre première requête d'image comme une requête CORS, maintenant si vous essayez de récupérer la même image à nouveau via XHR, même si Chrome décide d'utiliser le cache (réponse mise en cache pour la première requête), cela fonctionnera car elle sera désormais accompagnée de l'en-tête Access-Control-Allow-Origin.

SistemesEz.com

SystemesEZ est une communauté de sysadmins où vous pouvez résoudre vos problèmes et vos doutes. Vous pouvez consulter les questions des autres sysadmins, poser vos propres questions ou résoudre celles des autres.

Powered by:

X