Prévia do link HTTP GET quebra a especificação

Tenho lutado com um problema há algum tempo que ainda não parece ter sido relatado. Peço desculpas pela quantidade estranha de partes envolvidas, mas tentarei descrevê-lo de forma sucinta.

O resumo é o seguinte: quando coloco um link em uma mensagem, o Ruby Gem que finalmente faz a requisição HTTP GET para aquela URL para buscar dados de incorporação envia uma requisição HTTP que, segundo alguns proxies HTTP, é considerada inválida conforme a especificação. Isso impede que as pré-visualizações funcionem em alguns casos:

A versão um pouco mais longa é esta: usamos um ótimo serviço chamado Gitbook.io para nossa documentação. O Gitbook é uma solução hospedada e eles utilizam Cloudflare workers para redirecionamentos internos em seu site. Parte desses Cloudflare workers envolve o uso da API Node Fetch para fazer proxy de requisições HTTP. Os desenvolvedores do Node Fetch são EXTREMAMENTE rigorosos em seguir a especificação e rejeitam qualquer requisição GET que tenha um corpo HTTP ou até mesmo um cabeçalho Content-Length, mesmo que esse cabeçalho esteja definido como 0.

E é exatamente isso que está acontecendo. O Ruby Gem que faz a requisição HTTP envia um cabeçalho de requisição:

Content-Length: 0

Isso irrita profundamente o proxy do Node Fetch e resulta na rejeição da requisição pelo servidor remoto. Houve muitos debates em diferentes fóruns sobre se um corpo de requisição em um GET ou apenas um cabeçalho de content-length é válido conforme a especificação HTTP. Eu não tenho problema com isso, mas isso não impediu os desenvolvedores do Node Fetch de fechar todas as issues já abertas pedindo que permitissem tal semântica.

Infelizmente, estou preso no meio disso tudo:

Portanto, sobra-me postar aqui perguntando se há alguma maneira de controlar ou alterar as requisições HTTP GET feitas para pré-visualizações de links, de modo a incluir um conjunto aceitável de cabeçalhos HTTP, de forma que proxies que utilizam bibliotecas extremamente rigorosas, como o Node Fetch, não rejeitem essas requisições?

Se quiserem tentar, aqui está uma URL de exemplo hospedada nos servidores do Gitbook que utiliza o Cloudflare worker alimentado pelo Node Fetch:

6 curtidas

@jamie.wilson / @techAPJ, alguma ideia do motivo pelo qual estamos enviando Content-Length de 0 com nossas requisições? Vocês podem confirmar esse comportamento? Acredito que isso faça sentido para HEAD, mas para GET?

2 curtidas

Olá @sam, as solicitações HTTP parecem ser feitas por uma biblioteca Ruby chamada httprb, que tem esse comportamento. Se você olhar o link no item “A biblioteca HTTPrb se recusa a remover o cabeçalho porque acha que ele é perfeitamente válido.”, verá o desenvolvedor dessa biblioteca apresentando argumentos sobre por que ele está flexibilizando a especificação HTTP, mas não a quebrando.

Enquanto eu explorava isso em toda a internet tentando chegar a um acordo, consegui que alguém enviasse este pull request para o httprb, o que pode resolver o problema.

Como não sou desenvolvedor Ruby, nem saberia como testar isso. Acredito que, eventualmente, esse Gem lançará uma versão com a correção e, mais tarde, o Discourse será atualizado para começar a usá-la. Seria ótimo se alguém tivesse como testar se funciona. O caso de reprodução é muito simples: basta colar o link acima para minha URL do Gitbook e ver se a prévia é rejeitada.

1 curtida

Estou vendo o seguinte (o que coincide com a imagem no seu primeiro post):

O texto ‘Getting Started Guide’ nos mostra que a solicitação foi bem-sucedida — ele está puxando essa string da tag meta og:title:

<meta property="og:title" content="Getting Started Guide" data-react-helmet="true">

O erro/aviso de que a descrição está ausente também está correto. O conteúdo da página é o seguinte:

<meta property="og:description" content="" data-react-helmet="true">

A URL da imagem vem da tag og:image, que é a seguinte:

<meta data-react-helmet="true" property="og:image" content="https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png">

Se eu copiar e colar https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png no meu navegador (Safari recente no MacOS), recebo um erro dizendo:

Error: could not handle the request

Fazendo a mesma solicitação via curl, obtenho a mesma resposta:

curl -v https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png
*   Trying 104.18.8.111...
* TCP_NODELAY set
* Connected to app.gitbook.com (104.18.8.111) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/cert.pem
  CApath: none
* TLSv1.2 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-ECDSA-CHACHA20-POLY1305
* ALPN, server accepted to use h2
* Server certificate:
*  subject: C=US; ST=California; L=San Francisco; O=Cloudflare, Inc.; CN=sni.cloudflaressl.com
*  start date: Jun 16 00:00:00 2021 GMT
*  expire date: Jun 15 23:59:59 2022 GMT
*  subjectAltName: host "app.gitbook.com" matched cert's "*.gitbook.com"
*  issuer: C=US; O=Cloudflare, Inc.; CN=Cloudflare Inc ECC CA-3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0x142809200)
> GET /share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png HTTP/2
> Host: app.gitbook.com
> User-Agent: curl/7.64.1
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 256)!
< HTTP/2 500
< date: Mon, 23 Aug 2021 17:40:04 GMT
< content-type: text/plain; charset=utf-8
< content-length: 36
< cf-ray: 68361fb8ea9b4009-YYZ
< age: 432
< vary: Accept-Encoding
< via: magic cache
< cf-cache-status: HIT
< expect-ct: max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"
< x-cache: HIT
< x-cloud-trace-context: 9d4cbd24a15138451c88b2ced35a32f1;o=1
< x-content-type-options: nosniff
< x-magic-hash: f46ac4bf6b6dc125a68e9ad566b48481631bb27eec2165532a7c0f538e93c4f6
< x-release: gitbook-28427-6.25.14
< server: cloudflare
<
Error: could not handle the request
* Connection #0 to host app.gitbook.com left intact
* Closing connection 0

Você consegue ver uma imagem se copiar e colar a URL de og:image no seu navegador?

Em resumo: a pré-visualização Onebox parece estar funcionando conforme o esperado, com base na resposta da URL original.

3 curtidas

@jamie.wilson Obrigado pelo seu tempo para investigar isso. Você poderia esclarecer, no entanto, se o seu teste acima foi feito com a versão mais recente da gem httprb que contém o pull request mencionado ou se foi com uma versão mais antiga da biblioteca?

O erro original que eu via na prévia do Onebox era que a URL de destino retornava um código de status 500. Em algum momento antes de eu postar isso, a prévia do Onebox passou a mostrar a nota sobre os metadados opengraph ausentes. Como tenho solucionado esse problema há meses antes de postar com o suporte do Gitbook, é possível que algo tenha mudado nesse meio tempo.

Se a URL do Gitbook estiver realmente carregando e apenas faltando alguns metadados ou imagens, isso é diferente de a solicitação ser rejeitada. No entanto, tenho certeza de que qualquer solicitação que eu envio contendo um cabeçalho de solicitação HTTP Content-Length: 0 é rejeitada pelos workers do CloudFlare no servidor remoto. Talvez o cliente HTTP usado para fazer as solicitações no Discourse tenha mudado? Não sei nada sobre o código-fonte do Discourse e nem tenho 100% de certeza de que a biblioteca httprb é a fonte real das solicitações HTTP.

Não acredito que utilizemos o gem httprb de forma alguma. O processo de Oneboxing (ou seja, a funcionalidade que gera as pré-visualizações de links) utiliza o Net::HTTP da biblioteca padrão do Ruby, além do gem Excon como parte do fluxo.

Investigando um pouco mais, vejo que às vezes geramos requisições com um cabeçalho Content-Length: 0. No entanto, no caso da URL fornecida, pelo menos, isso não está interferindo na geração do Onebox.

Pode ter havido um incremento de versão menor, mas nada significativo, como uma reestruturação de como fazemos as requisições ou quais bibliotecas utilizamos para isso.

Houve algumas alterações para tornar o Oneboxing mais robusto em geral, o que pode explicar por que URLs que anteriormente retornavam erro 500 agora estão gerando Onebox com sucesso.

Se você tiver URLs que possa compartilhar e que atualmente estejam retornando erros durante o Oneboxing (ou não funcionando conforme o esperado em outra parte do Discourse), sinta-se à vontade para enviá-las para mim!

3 curtidas

Ah, então essa é uma informação muito boa. Tenho tido que fazer suposições ousadas sobre quais bibliotecas estavam envolvidas até agora, em grande parte devido à quase nenhuma ajuda recebida da equipe do Gitbook, que mantém os proxies da CloudFlare.

Entendido. Acredito que não compartilhei acima, mas a única informação que consegui obter do Gitbook foi que o erro nos logs de erro da CloudFlare, que estava rejeitando as requisições de prévia do Discourse, era o seguinte:

Requisição com método GET ou HEAD não pode ter corpo.

O que não está claro é se a requisição GET do Discourse realmente conteria um corpo ou apenas o cabeçalho Content-Length: 0. De qualquer forma, isso viola a especificação Fetch, segundo algumas pessoas (incluindo as da Cloudflare).

Sim, em algum momento parece que o erro do Onebox mudou de um genérico 500 para agora conter alguns dados. Não há como saber quais bibliotecas podem ter sido atualizadas (e eu atualizei o Discourse nesse período). Gostaria de ter uma maneira de capturar exatamente quais cabeçalhos estão sendo enviados pelo Discourse, mas mesmo acessando uma URL como http://httpbin.org/get, não tenho como “ver” o que é retornado, já que os resultados são totalmente consumidos pelo Onebox e não são registrados em nenhum lugar que eu saiba.

Se o cabeçalho content-length vazio agora desapareceu, então posso, pelo menos, trabalhar com o Gitbook para corrigir o código de incorporação deles (o que não vai acontecer, pois eles estão atualmente reescrevendo todo o aplicativo do zero e se recusando a corrigir bugs existentes :/, mas isso, pelo menos, não é problema do Discourse).

Deixe-me dizer primeiro que muito do que foi escrito acima está bem além da minha compreensão, mas estou tentando o meu melhor aqui. É perfeitamente aceitável me avisar se eu estiver parecendo um tolo ao me intrometer no tópico errado.

Estamos vendo isso com bastante frequência, pois postamos frequentemente links para nosso Centro de Ajuda (base de conhecimento) em nossa Comunidade.

Alguns exemplos de links que falham ao Onebox:

https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address

Do painel de pré-visualização enquanto digito:

1 curtida

Após investigar um pouco mais, é o gem Excon que adiciona Content-Length: 0, mas não em requisições GET.

Mas esse código está lá há 8 ou 9 anos, então provavelmente não era o problema.

O arquivo Gemfile.lock mostrará os gems usados pelo núcleo do Discourse.

2 curtidas

Este site está atrás de um captcha do Cloudflare e está bloqueando o Discourse de obter qualquer informação nele :slightly_frowning_face:

2 curtidas

Ao visualizar esta página em um navegador, ela contém as meta tags necessárias para construir um Onebox. No entanto, ao tentar buscar a URL, parece que estamos recebendo um erro!

oneboxer preview url: https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
headers: {"User-Agent"=>"Discourse Forum Onebox v2.8.0.beta4"}
helpers response code: 403

Isso significa que solicitamos essa URL com um User Agent de “Discourse Forum Onebox v2.8.0.beta4”, mas o servidor web remoto retornou um código de status 403.

Da mesma forma, usando a ferramenta de linha de comando wget:

wget https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
--2021-08-23 17:38:30--  https://help.republicwireless.com/hc/en-us/articles/115014150828--How-to-Add-an-E911-Address
Resolving help.republicwireless.com (help.republicwireless.com)... 104.16.53.111, 104.16.51.111
Connecting to help.republicwireless.com (help.republicwireless.com)|104.16.53.111|:443... connected.
HTTP request sent, awaiting response... 403 Forbidden

O que diz a mesma coisa… Estamos enviando uma solicitação válida, mas o servidor web remoto está se recusando a retornar um resultado. É possível que as pessoas responsáveis por help.republicwireless.com desbloqueiem essas solicitações válidas?

Esses dois sites não possuem as tags de título/descrição do OpenGraph, no entanto, eles têm outros títulos/descrições aos quais o Onebox deveria recorrer. Isso é algo que devemos investigar para corrigir.

2 curtidas

:confused: No entanto, tem funcionado há anos.

Aqui está um exemplo em que um link do mesmo site apresenta um Onebox válido: https://forums.republicwireless.com/t/4-digit-pin-which-i-have-forgot/37655/2

Que redireciona para https://help.republicwireless.com/hc/en-us/articles/115012101188-Can-t-Get-Past-the-Screen-Lock-on-the-Phone

1 curtida

O Cloudflare altera constantemente seu algoritmo de detecção de robôs. Portanto, se você deseja que o Discourse não seja bloqueado, talvez seja necessário entrar em contato com o suporte deles e perguntar por que a solicitação está sendo bloqueada.

5 curtidas

Desculpem pessoal, estou fechando isso como obsoleto, precisaremos de uma nova reprodução dos problemas para abrir.