Chrome S3 Cloudfront: в начальном запросе XHR отсутствует заголовок «Access-Control-Allow-Origin»

30

У меня есть веб-страница ( https://smartystreets.com/contact ), которая использует jQuery для загрузки некоторых файлов SVG из S3 через CloudFront CDN.

В Chrome я открою окно Incognito, а также консоль. Тогда я буду загружать страницу. По мере загрузки страницы я обычно получаю от 6 до 8 сообщений в консоли, которые выглядят примерно так:

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.

Если я делаю стандартную перезагрузку страницы, даже несколько раз, я продолжаю получать те же ошибки. Если я это сделаю, Command+Shift+Rто большинство, а иногда и все изображения будут загружаться без XMLHttpRequestошибок.

Иногда даже после загрузки изображений я обновляюсь, и одно или несколько изображений не загружаются и возвращают эту XMLHttpRequestошибку снова.

Я проверил, изменил и перепроверил настройки на S3 и Cloudfront. В S3 моя конфигурация CORS выглядит так:

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

(Примечание: изначально было только <AllowedOrigin>*</AllowedOrigin>, та же проблема.)

В CloudFront поведение распределения устанавливается , чтобы позволить методы HTTP: GET, HEAD, OPTIONS. Кэшированные методы одинаковы. Прямые заголовки установлены в «Белый список», и этот белый список включает в себя «Access-Control-Request-Headers, Access-Control-Request-Method, Origin».

Тот факт, что он работает после перезагрузки браузера без кеша, указывает на то, что все хорошо на стороне S3 / CloudFront, иначе зачем доставлять контент. Но тогда почему контент не будет доставлен при начальном просмотре страницы?

Я работаю в Google Chrome на MacOS. Firefox без проблем получает файлы каждый раз. Опера НИКОГДА не получает файлы. Safari подберет изображения после нескольких обновлений.

Использование curlу меня не вызывает никаких проблем:

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

Некоторые предлагают мне удалить дистрибутив CloudFront и воссоздать его. Похоже, довольно резкое и неудобное решение.

Что вызывает эту проблему?

Обновить:

Добавление заголовков ответов из изображения, которое не удалось загрузить.

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
источник
Вы правы - удалять и воссоздавать это экстремально и просто не должно быть необходимости. Можете ли вы показать нам заголовки запросов и ответов браузера для неудачного запроса? А может для успешного запроса точно такого же объекта?
Майкл - sqlbot
@ Michael-sqlbot, я надеялся, что вы зайдете на URL ( smartystreets.com/contact ) и узнаете , происходит ли то же самое на вашем компьютере. :) Интересная вещь об ошибках заключается в том, что кроме ошибки в консоли, браузер сообщает о состоянии 200, ссылаясь на то, что он использует изображение «(из дискового кэша)», что не должно быть возможно с Incognito, я думал. Даже после того, как я очищаю локальный кеш.
SunSparc
1
Да, люди так часто «составляют» доменные имена (которые оказываются реальными сайтами, но не самим сайтом), что я изначально не осознавал, что вы дали реальную, правильную ссылку на ваш сайт. Спасибо за это, вы можете игнорировать мою просьбу. Я могу продублировать проблему. Это похоже на проблему на стороне клиента. Я гонюсь за теорией.
Майкл - sqlbot
1
Именно так я и считаю. Chrome и S3 взаимодействуют таким образом, что нарушает запрос CORS, который следует за запросом не-CORS для того же объекта. Возможно, оба они не правы ... но, возможно, ни один из них не прав. Я не думаю, что вы можете это исправить, не сохраняя две копии объекта с разными ключами ... или используя два разных дистрибутива CloudFront (разные имена хостов), чтобы вы не делали запрос как CORS, так и не CORS. Я напишу это с подробностями того, как я прихожу к такому выводу, если хотите.
Майкл - sqlbot
1
Когда вы возитесь с заголовками и т. Д., Они кэшируются, чтобы вернуться к текущим настройкам, просто аннулируйте их в CloudFront
tarikakyol

Ответы:

57

Вы делаете два запроса на один и тот же объект, один из HTML, другой из XHR. Второй сбой, потому что Chrome использует кэшированный ответ от первого запроса, который не имеет Access-Control-Allow-Originзаголовка ответа.

Зачем?

Ошибка Chromium 409090 Не удается выполнить перекрестный запрос из кэша после кэширования обычного запроса , и эта проблема «не будет устранена» - они считают, что их поведение правильное. Chrome считает, что кэшированный ответ можно использовать, очевидно, потому что в ответ не был включен Vary: Originзаголовок.

Но S3 не возвращается, Vary: Originкогда объект запрашивается без Origin:заголовка запроса, даже когда CORS настроен в корзине. Vary: Originотправляется только тогда, когда Originв запросе присутствует заголовок.

А CloudFront не добавляется Vary: Originдаже Originв белый список для пересылки, что по определению должно означать, что изменение заголовка может изменить ответ - вот почему вы пересылаете и кэшируете по заголовкам запросов.

CloudFront получает пропуск, потому что его ответ был бы правильным, если бы S3 был более правильным, поскольку CloudFront действительно возвращает его, когда он предоставляется S3.

S3, немного пушистый. Это не так, чтобы вернуться, Vary: Some-Headerкогда не было Some-Headerв запросе.

Например, ответ, который содержит

Vary: accept-encoding, accept-language

указывает, что исходный сервер мог использовать запрос Accept-Encodingи Accept-Languageполя (или их отсутствие) в качестве определяющих факторов при выборе содержимого для этого ответа. (выделение добавлено)

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

Понятно, что Vary: Some-Absent-Headerон действителен, поэтому S3 будет корректным, если он добавит Vary: Originк своему ответу, если настроен CORS, поскольку это действительно может изменить ответ.

И, видимо, это заставило бы Chrome делать правильные вещи. Или, если в этом случае это не так, это будет нарушением MUST NOT. Из того же раздела:

Исходный сервер может отправить Varyсписок полей для двух целей:

  1. Информировать получателей кэша, что они MUST NOTиспользуют этот ответ для удовлетворения более позднего запроса, если только у более позднего запроса нет тех же значений для перечисленных полей, что и у исходного запроса (Раздел 4.1 [RFC7234]). Другими словами, Vary расширяет ключ кеша, необходимый для сопоставления нового запроса с сохраненной записью кеша.

...

Таким образом, S3 действительно SHOULDвозвращается, Vary: Originкогда CORS настроен в сегменте, если Originотсутствует в запросе, но это не так.

Тем не менее, S3 не является строго неправильным для того, чтобы не возвращать заголовок, потому что это только a SHOULD, а не a MUST. Опять же из того же раздела RFC-7231:

Исходный сервер SHOULDотправляет поле заголовка Vary, когда его алгоритм выбора представления изменяется на основе аспектов сообщения запроса, отличных от метода и цели запроса, ...

С другой стороны, можно утверждать, что Chrome должен неявно знать, что изменение Originзаголовка должно быть ключом кеша, поскольку он может изменить ответ таким же образом, что Authorizationможет изменить ответ.

... если дисперсия не может быть пересечена или исходный сервер не был специально настроен для предотвращения прозрачности кэша. Например, нет необходимости отправлять Authorizationимя поля, Varyпотому что повторное использование между пользователями ограничено определением поля [...]

Точно так же повторное использование в разных источниках возможно ограничено природой, Originно этот аргумент не является сильным.


tl; dr: Вы, очевидно, не можете успешно извлечь объект из HTML, а затем успешно извлечь его снова, как запрос CORS с Chrome и S3 (с или без CloudFront), из-за особенностей реализации.


Временное решение:

Это поведение можно обойти с помощью CloudFront и Lambda @ Edge, используя следующий код в качестве триггера Origin Response.

Это добавляет Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Originк любому ответу от S3, который не имеет Varyзаголовка. В противном случае Varyзаголовок в ответе не изменяется.

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

Атрибуция. Я также являюсь автором исходного поста на форумах поддержки AWS, где этот код был опубликован изначально.


Решение Lambda @ Edge, приведенное выше, приводит к полностью корректному поведению, но вот две альтернативы, которые вы можете найти полезными, в зависимости от ваших конкретных потребностей:

Альтернатива / Hackaround # 1: Подделать заголовки CORS в CloudFront.

CloudFront поддерживает пользовательские заголовки, которые добавляются к каждому запросу. Если вы установите Origin:для каждого запроса, даже те, которые не являются кросс-источником, это включит правильное поведение в S3. Параметр конфигурации называется Custom Origin Headers, а слово «Origin» означает нечто совершенно иное, чем в CORS. Конфигурирование пользовательского заголовка, подобного этому, в CloudFront перезаписывает то, что отправлено в запросе, с указанным значением, или добавляет его, если оно отсутствует. Если у вас есть только один источник доступа к вашему контенту через XHR, например https://example.com, вы можете добавить это. Использование *сомнительно, но может работать для других сценариев. Тщательно обдумайте последствия.

Альтернатива / Hackaround # 2: Используйте «пустой» параметр строки запроса, который отличается для HTML и XHR или отсутствует у одного или другого. Эти параметры обычно называются, x-*но не должны быть x-amz-*.

Допустим, вы придумали имя x-request. Так <img src="https://dzczcexample.cloudfront.net/image.png?x-request=html">. При доступе к объекту из JS не добавляйте параметр запроса. CloudFront уже делает правильные вещи, кэшируя разные версии объектов, используя Originзаголовок или его отсутствие как часть ключа кэша, потому что вы перенаправили этот заголовок в свое поведение кэша. Проблема в том, что ваш браузер этого не знает. Это убеждает браузер в том, что на самом деле это отдельный объект, который необходимо запросить снова, в контексте CORS.

Если вы используете эти альтернативные предложения, используйте одно или другое, а не оба.

Майкл - sqlbot
источник
5
Ваш ответ - спасатель, отличный ответ. Вы сэкономили мне серьезное время.
мтюрт
Привет, я не использую cloudfront для своего s3, так что этот обходной путь не помогает, есть ли что-нибудь еще, что я могу сделать?
Джеффин
1
@Jeffin, альтернатива # 2 выше будет работать только для S3, без CloudFront. Добавление произвольного ?x-some-key=some-valueпараметра строки запроса убедит браузер, что запрос отличается.
Майкл - sqlbot
1
@ Майкл-sqlbot: Да, работал как шарм
Джеффин
1
@ Лайонел, да, это выглядит правильно.
Майкл - sqlbot
1

Я не знаю, почему вы получаете такие разные результаты из разных браузеров, но:

X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==

Эта строка прямо здесь - это то, что (если вы можете привлечь их внимание) инженер CloudFront или Служба поддержки будет использовать для выполнения одного из ваших неудавшихся запросов. Если запрос поступает на сервер CloudFront, он должен иметь этот заголовок в ответе. Если этот заголовок отсутствует, то, скорее всего, запрос где-то завершится неудачей, прежде чем он попадет в CloudFront.

unixguy
источник
Спасибо, я посмотрю, смогу ли я получить ответы на форумах AWS.
SunSparc
1
Возможно, вам придется заплатить 29 долларов за поддержку разработчиков. Это тривиальная сумма денег для любого бизнеса, учитывая, сколько человек тратит время.
Тим
1
@ Тим, обратите внимание, что поддержка разработчиков не просто 29 долларов. Это базовая цена. Если 3% вашего ежемесячного счета AWS> = $ 29, вы платите 3% вместо базовой.
Майкл - sqlbot
Спасибо @ Michael-sqlbot, я этого не осознавал. Я знаю, что цена поддержки может быстро возрасти, когда у вас есть такие вещи, как зарезервированные экземпляры, но я никогда не смотрел на цены для разработчиков, когда у вас много ресурсов.
Тим