Chrome S3 Cloudfront: nenhum cabeçalho 'Access-Control-Allow-Origin' na solicitação XHR inicial

30

Eu tenho uma página da Web ( https://smartystreets.com/contact ) que usa o jQuery para carregar alguns arquivos SVG do S3 através da CDN do CloudFront.

No Chrome, vou abrir uma janela de navegação anônima e o console. Então eu carregarei a página. À medida que a página é carregada, normalmente recebo de 6 a 8 mensagens no console com aparência semelhante a esta:

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 eu fizer uma recarga padrão da página, mesmo várias vezes, continuarei recebendo os mesmos erros. Se o fizer Command+Shift+R, a maioria e, às vezes, todas as imagens serão carregadas sem o XMLHttpRequesterro.

Às vezes, mesmo após o carregamento das imagens, eu atualizo e uma ou mais imagens não são carregadas e retornamos esse XMLHttpRequesterro novamente.

Verifiquei, alterei e verifiquei novamente as configurações do S3 e do Cloudfront. No S3, minha configuração do CORS se parece com isso:

<?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: inicialmente tinha apenas o <AllowedOrigin>*</AllowedOrigin>mesmo problema.)

Em CloudFront o comportamento de distribuição está definido para permitir que os métodos HTTP: GET, HEAD, OPTIONS. Métodos em cache são os mesmos. Encaminhar cabeçalhos é definido como "Lista de permissões" e essa lista de permissões inclui "Cabeçalhos de solicitação de controle de acesso, Método de solicitação de controle de acesso, Origem".

O fato de funcionar após uma recarga de navegador sem cache parece indicar que tudo está bem no lado do S3 / CloudFront; caso contrário, por que o conteúdo seria entregue. Mas então por que o conteúdo não seria entregue na visualização de página inicial?

Estou trabalhando no Google Chrome no macOS. O Firefox não tem problemas em obter os arquivos todas as vezes. O Opera NUNCA obtém os arquivos. O Safari seleciona as imagens após várias atualizações.

Usando curleu não tenho problemas:

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

Alguns sugeriram que eu exclua a distribuição do CloudFront e a recrie. Parece uma correção bastante dura e inconveniente.

O que está causando esse problema?

Atualizar:

Adicionando cabeçalhos de resposta de uma imagem que falhou ao carregar.

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
fonte
Você está certo - excluir e recriar é extremo e simplesmente nunca deve ser necessário. Você pode nos mostrar os cabeçalhos de solicitação e resposta do navegador para uma solicitação com falha? E talvez para uma solicitação bem-sucedida do mesmo objeto?
Michael - sqlbot
@ Michael-sqlbot, eu meio que esperava que você visitasse o URL ( smartystreets.com/contact ) e ver se a mesma coisa estava acontecendo em sua máquina. :) O interessante dos erros é que, além do erro no console, o navegador relata um status 200, alegando que está usando a imagem "(do cache do disco)", o que não deve ser possível com o Navegador Anônimo pensamento. Mesmo depois de limpar o cache local.
21717 SunSparc
1
Sim, as pessoas costumam "inventar" nomes de domínio (que se tornam sites reais, mas não o site em questão) que eu inicialmente não sabia que você havia fornecido o link correto e correto para o seu site. Obrigado por isso, você pode desconsiderar minha solicitação. Eu posso duplicar o problema. Parece um problema do lado do cliente. Estou perseguindo uma teoria.
Michael - sqlbot
Acho que você pode estar certo sobre se tratar de um problema do lado do cliente. As imagens são vinculadas às tags A no HTML e, em seguida, parece que elas são solicitadas novamente no jQuery. Talvez o erro seja de uma ligação e o 200 seja da outra.
SunSparc
1
É exatamente isso que acredito ser o caso. O Chrome e o S3 estão interagindo de uma maneira que quebra uma solicitação CORS que segue uma solicitação não-CORS para o mesmo objeto. Indiscutivelmente, os dois estão errados ... mas sem dúvida nenhum deles está errado. Acho que você não pode consertar isso sem armazenar duas cópias do objeto com chaves diferentes ... ou usar duas distribuições diferentes do CloudFront (nomes de host diferentes) para não fazer uma solicitação CORS e não-CORS. Escreverei com detalhes de como chego a essa conclusão, se quiser.
Michael - sqlbot

Respostas:

57

Você está fazendo duas solicitações para o mesmo objeto, uma em HTML e uma em XHR. O segundo falha, porque o Chrome usa a resposta em cache da primeira solicitação, que não tem Access-Control-Allow-Origincabeçalho de resposta.

Por quê?

Bug do Chromium 409090 A solicitação de origem cruzada do cache que falha após a solicitação regular é armazenada em cache descreve esse problema e é um "não corrigido" - eles acreditam que seu comportamento está correto. O Chrome considera a resposta em cache utilizável, aparentemente porque a resposta não incluiu um Vary: Origincabeçalho.

Mas o S3 não retorna Vary: Originquando um objeto é solicitado sem um Origin:cabeçalho de solicitação, mesmo quando o CORS está configurado no bucket. Vary: Originé enviado apenas quando um Origincabeçalho está presente na solicitação.

E o CloudFront não adiciona Vary: Originmesmo quando Originestá na lista de permissões para encaminhamento, o que por definição significa que a variação do cabeçalho pode modificar a resposta - essa é a razão pela qual você encaminha e armazena em cache nos cabeçalhos de solicitação.

O CloudFront recebe uma aprovação, porque sua resposta estaria correta se os S3 estivessem mais corretos, pois o CloudFront retorna isso quando é fornecido pelo S3.

S3, um pouco mais confuso. Não é errado retornar Vary: Some-Headerquando não havia Some-Headerna solicitação.

Por exemplo, uma resposta que contém

Vary: accept-encoding, accept-language

indica que o servidor de origem pode ter usado as solicitações Accept-Encodinge os Accept-Languagecampos (ou a falta deles) como fatores determinantes ao escolher o conteúdo para esta resposta. (enfase adicionada)

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

Claramente, Vary: Some-Absent-Headeré válido, portanto, o S3 estaria correto se adicionado Vary: Originà sua resposta se o CORS estiver configurado, pois isso poderia variar a resposta.

E, aparentemente, isso faria o Chrome fazer a coisa certa. Ou, se não fizer a coisa certa nesse caso, estaria violando a MUST NOT. Da mesma seção:

Um servidor de origem pode enviar Varycom uma lista de campos para dois propósitos:

  1. Para informar aos destinatários do cache que eles MUST NOTusam essa resposta para atender a uma solicitação posterior, a menos que a solicitação posterior tenha os mesmos valores para os campos listados que a solicitação original (Seção 4.1 de [RFC7234]). Em outras palavras, o Vary expande a chave de cache necessária para corresponder uma nova solicitação à entrada de cache armazenada.

...

Portanto, o S3 realmente SHOULDretornará Vary: Originquando o CORS estiver configurado no bucket, se Originestiver ausente da solicitação, mas não.

Ainda assim, o S3 não está estritamente errado por não retornar o cabeçalho, porque é apenas um SHOULD, não um MUST. Novamente, da mesma seção da RFC-7231:

Um servidor de origem SHOULDenvia um campo de cabeçalho Vary quando seu algoritmo para selecionar uma representação varia com base em aspectos da mensagem de solicitação que não sejam o método e o destino da solicitação, ...

Por outro lado, pode-se argumentar que o Chrome deveria saber implicitamente que a variação do Origincabeçalho deve ser uma chave de cache, pois pode alterar a resposta da mesma maneira que Authorizationpode alterar a resposta.

... a menos que a variação não possa ser cruzada ou o servidor de origem tenha sido deliberadamente configurado para impedir a transparência do cache. Por exemplo, não há necessidade de enviar o Authorizationnome do campo Varyporque a reutilização entre usuários é restringida pela definição do campo.

Da mesma forma, a reutilização através das origens é indiscutivelmente restrita pela natureza, Originmas esse argumento não é forte.


tl; dr: Você aparentemente não pode buscar com êxito um objeto do HTML e, em seguida, buscá-lo novamente com uma solicitação CORS no Chrome e S3 (com ou sem CloudFront), devido a peculiaridades nas implementações.


Solução alternativa:

Esse comportamento pode ser contornado com o CloudFront e o Lambda @ Edge, usando o código a seguir como um gatilho de resposta à origem.

Isso adiciona Vary: Access-Control-Request-Headers, Access-Control-Request-Method, Originqualquer resposta do S3 que não tenha Varycabeçalho. Caso contrário, o Varycabeçalho na resposta não será modificado.

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

Atribuição: também sou o autor da postagem original nos fóruns de suporte da AWS em que esse código foi compartilhado inicialmente.


A solução Lambda @ Edge acima resulta em um comportamento totalmente correto, mas aqui estão duas alternativas que você pode achar úteis, dependendo de suas necessidades específicas:

Alternativa / Hackaround # 1: forjar os cabeçalhos CORS no CloudFront.

O CloudFront suporta cabeçalhos personalizados que são adicionados a cada solicitação. Se você definir Origin:todas as solicitações, mesmo aquelas que não são de origem cruzada, isso permitirá o comportamento correto no S3. A opção de configuração é chamada de Cabeçalhos de origem personalizados, com a palavra "Origem" significando algo totalmente diferente do que significa no CORS. A configuração de um cabeçalho personalizado como este no CloudFront substitui o que é enviado na solicitação pelo valor especificado ou o adiciona se ausente. Se você tem exatamente uma origem acessando seu conteúdo por XHR, por exemplo https://example.com, você pode adicioná-lo. O uso *é duvidoso, mas pode funcionar para outros cenários. Considere as implicações cuidadosamente.

Alternativa / Hackaround # 2: Use um parâmetro de seqüência de caracteres de consulta "fictício" que difere de HTML e XHR ou esteja ausente de um ou de outro. Esses parâmetros geralmente são nomeados, x-*mas não devem ser x-amz-*.

Digamos que você invente o nome x-request. Então <img src="https://dzczcexample.cloudfront.net/image.png?x-request=html">. Ao acessar o objeto a partir de JS, não adicione o parâmetro de consulta. O CloudFront já está fazendo a coisa certa, armazenando em cache diferentes versões dos objetos usando o Origincabeçalho ou a ausência dele como parte da chave do cache, porque você encaminhou esse cabeçalho no comportamento do cache. O problema é que seu navegador não sabe disso. Isso convence o navegador de que este é realmente um objeto separado que precisa ser solicitado novamente, em um contexto CORS.

Se você usar essas sugestões alternativas, use uma ou outra - não as duas.

Michael - sqlbot
fonte
5
Sua resposta é um salva-vidas, ótima resposta. Você me salvou um tempo sério.
Mthurt #
Oi, eu não uso cloudfront para o meu s3, portanto, esta solução alternativa não está ajudando, há mais alguma coisa que eu possa fazer?
Jeffin
1
@ Jeffin, a alternativa nº 2 acima funcionará apenas para o S3, sem o CloudFront. Adicionar um ?x-some-key=some-valueparâmetro de sequência de consulta arbitrária convencerá o navegador de que a solicitação é diferente.
Michael - sqlbot
1
@ Michael-sqlbot: Sim, funcionou como um encanto
Jeffin
1
@ Lionel sim, isso parece correto.
Michael - sqlbot
1

Não sei por que você obteria resultados tão diferentes em vários navegadores, mas:

X-Amz-Cf-Id: wxn_m9meR6yPoyyvj1R7x83pBDPJy1nT7kdMv1aMwXVtHCunT9OC9g ==

Nessa linha, existe o que (se você conseguir chamar a atenção deles) que um engenheiro do CloudFront ou de suporte usará para seguir uma de suas solicitações com falha. Se a solicitação estiver chegando a um servidor CloudFront, ele deverá ter esse cabeçalho na resposta. Se esse cabeçalho não estiver lá, é provável que a solicitação falhe em algum lugar antes de chegar ao CloudFront.

unixguy
fonte
Obrigado, vou ver se consigo obter respostas nos fóruns da AWS.
21717 SunSparc
1
Pode ser necessário pagar os US $ 29 pelo suporte ao desenvolvedor. É uma quantia trivial de dinheiro para qualquer empresa, considerando o custo de uma pessoa.
Tim
1
@ Tim, observe que o suporte ao desenvolvedor não é simplesmente US $ 29. Esse é o preço base. Se 3% da sua fatura mensal da AWS for> = $ 29, você pagará 3% em vez da base.
Michael - sqlbot
Obrigado @ Michael-sqlbot, eu não percebi isso. Sei que o preço do suporte pode aumentar rapidamente quando você tem coisas como instâncias reservadas, mas nunca observei o preço do desenvolvedor quando você tem muitos recursos.
Tim