La vista previa del enlace HTTP GET rompe la especificación

Llevo un tiempo lidiando con un problema que, por lo que parece, aún no ha sido reportado. Me disculpo por la cantidad inusual de elementos involucrados, pero intentaré describirlo de manera concisa.

El TL;DR es que cuando pego un enlace en un mensaje, la Gem de Ruby que finalmente realiza la solicitud HTTP GET a esa URL para buscar datos de incrustación envía una solicitud HTTP que algunos proxies HTTP consideran inválida según la especificación. Esto impide que las vistas previas funcionen en algunos casos:

La versión un poco más larga es la siguiente. Utilizamos un servicio muy bueno llamado Gitbook.io para nuestra documentación. Gitbook es una solución alojada y utilizan Cloudflare workers para redirecciones internas en su sitio. Parte de esos workers de Cloudflare implica el uso de la API Node Fetch para proxy de solicitudes HTTP. Los desarrolladores de Node Fetch son EXTREMADAMENTE pedantes al seguir la especificación y rechazarán cualquier solicitud GET que tenga un cuerpo HTTP o incluso una cabecera Content-Length, aunque esa cabecera esté establecida en 0.
Y eso es exactamente lo que está ocurriendo. La Gem de Ruby que realiza la solicitud HTTP envía una cabecera de solicitud

Content-Length: 0

y esto enfurece enormemente al proxy de Node Fetch, lo que termina provocando que la solicitud sea rechazada por el servidor remoto. Ha habido mucho debate en diferentes foros sobre si un cuerpo de solicitud en una GET o simplemente una cabecera de longitud de contenido es válido según la especificación HTTP. A mí no me importa, pero eso no ha impedido que los desarrolladores de Node Fetch cierren cada problema que se ha abierto rogándoles que permitan tal semántica.

Desafortunadamente, me encuentro atrapado en medio de esto.

  • El proyecto Node Fetch se niega a considerar estas solicitudes HTTP como válidas.
  • El soporte de Cloudflare se niega a ayudarme porque no tengo control sobre los workers basados en Node en cuestión.
  • El soporte de Gitbook se niega a ayudarme porque están de acuerdo con los desarrolladores de Fetch (y no estoy seguro de que realmente les importe).
  • Y la biblioteca HTTPrb se niega a eliminar la cabecera porque consideran que es perfectamente válida.

Así que me queda publicar aquí preguntando si hay alguna forma de controlar o modificar las solicitudes HTTP GET realizadas para las vistas previas de enlaces, de modo que incluyan un conjunto aceptable de cabeceras HTTP para que los proxies que utilizan bibliotecas increíblemente pedantes como Node Fetch no rechacen estas solicitudes.

Si quieren intentarlo, aquí hay una URL de ejemplo alojada en los servidores de Gitbook que utiliza su worker de Cloudflare impulsado por Node Fetch.

6 Me gusta

@jamie.wilson / @techAPJ ¿tienen alguna idea de por qué estamos enviando Content-Length de 0 con nuestras solicitudes? ¿Pueden confirmar este comportamiento? Supongo que esto tiene sentido para HEAD, pero ¿para GET?

2 Me gusta

Hola @sam: las solicitudes HTTP parecen ser realizadas por una biblioteca de Ruby llamada httprb, que tiene este comportamiento. Si observas el enlace en el punto “La biblioteca HTTPrb se niega a eliminar la cabecera porque considera que es perfectamente válida”, podrás ver al desarrollador de esa biblioteca argumentando por qué está doblando la especificación HTTP sin romperla.

Mientras he estado investigando esto por toda la internet tratando de que alguien llegara a un acuerdo, logré que alguien enviara esta pull request a httprb, lo cual podría resolver el problema.

No soy desarrollador de Ruby, así que ni siquiera sabría cómo probar esto. Supongo que eventualmente esta Gem lanzará una versión con la corrección y, más adelante, Discourse se actualizará para empezar a usarla. Sería genial que alguien tuviera una forma de probar si funciona. El caso de reproducción es muy sencillo: simplemente pega el enlace anterior a mi URL de Gitbook y comprueba si se rechaza la vista previa.

1 me gusta

Veo lo siguiente (que coincide con la imagen de tu primer mensaje):

El texto “Getting Started Guide” nos indica que la solicitud se ha realizado con éxito: está extrayendo esa cadena de la etiqueta meta og:title:

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

El error/advertencia de que falta la descripción también es correcto. El contenido de la página es el siguiente:

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

La URL de la imagen proviene de la etiqueta og:image, que es la siguiente:

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

Si copio y pego https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png en mi navegador (Safari reciente en macOS), recibo un error que dice:

Error: could not handle the request

Realizar la misma solicitud mediante curl da la misma respuesta:

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

¿Puedes ver una imagen si copias y pegas la URL de og:image en tu navegador?

En resumen: la vista previa de Onebox parece estar funcionando como se espera, basándose en la respuesta de la URL original.

3 Me gusta

@jamie.wilson Gracias por tu tiempo investigando esto. Sin embargo, ¿podrías aclarar si tu prueba anterior se realizó con la versión más reciente de la gem httprb que incluye la solicitud de extracción mencionada, o si se trata de una versión anterior de la biblioteca?

El error original que solía ver en la vista previa de Onebox era que la URL de destino devolvía un código de estado 500. En algún momento antes de publicar esto, la vista previa de Onebox comenzó a mostrar en su lugar la nota sobre los metadatos opengraph faltantes. Dado que he estado solucionando este problema durante meses antes de contactar con el soporte de Gitbook, es posible que algo haya cambiado en ese intervalo.

Si la URL de Gitbook se está cargando realmente y solo faltan algunos metadatos o hay imágenes faltantes, eso es diferente a que la solicitud sea rechazada. Sin embargo, sé con certeza que cualquier solicitud que yo envíe que incluya un encabezado HTTP Content-Length: 0 es rechazada por los workers de CloudFlare en el servidor remoto. ¿Quizás el cliente HTTP utilizado para realizar las solicitudes en Discourse ha cambiado? No sé nada sobre el código fuente de Discourse y ni siquiera estoy 100 % seguro de que la biblioteca httprb sea la fuente real de las solicitudes HTTP.

No creo que utilicemos la gem httprb en absoluto. El proceso de Oneboxing (es decir, lo que genera las vistas previas de los enlaces) utiliza Net::HTTP de la librería estándar de Ruby, así como la gem Excon como parte del flujo.

Al investigar un poco más, puedo ver que a veces generamos solicitudes con un encabezado Content-Length: 0. Sin embargo, en el caso de la URL proporcionada al menos, esto no está interfiriendo con la generación del Onebox.

Puede haber habido un incremento menor de versión, pero nada mayor como una reestructuración de cómo hacemos las solicitudes o qué bibliotecas utilizamos para ello.

Se han realizado algunos cambios para hacer que Oneboxing sea más robusto en general, lo cual podría explicar por qué las URLs que antes devolvían un error 500 ahora generan Onebox correctamente.

Si tienes URLs que puedas compartir y que actualmente estén devolviendo errores durante el Oneboxing (o que no funcionen como se espera en alguna otra parte de Discourse), ¡no dudes en enviármelas!

3 Me gusta

Ah, así que esta es información muy valiosa. He estado teniendo que hacer suposiciones arriesgadas sobre qué bibliotecas estaban involucradas en este punto, en gran parte debido a que apenas obtuve ayuda del equipo de Gitbook, que mantiene los proxies de CloudFlare.

Entendido. Creo que no lo compartí arriba, pero lo único que pude obtener de Gitbook fue que el error en sus registros de errores de CloudFlare, que estaba rechazando las solicitudes de vista previa de Discourse, fue este:

Una solicitud con el método GET o HEAD no puede tener un cuerpo.

Lo que no está claro es si la solicitud GET de Discourse realmente contenía un cuerpo o solo el encabezado Content-Length: 0. De cualquier manera, eso viola la especificación Fetch según algunas personas (incluidos los de Cloudflare).

Sí, en algún momento parece que el error del Onebox cambió de un genérico 500 a ahora tener algunos datos. No hay forma de saber qué bibliotecas pudieron haber sido actualizadas (y yo he actualizado Discourse durante este tiempo). Ojalá tuviera una forma de capturar exactamente qué encabezados se están enviando desde Discourse, pero incluso si accedo a una URL como http://httpbin.org/get, no tengo forma de “ver” qué se devuelve, ya que los resultados son consumidos completamente por Onebox y no se registran en ningún lugar que yo sepa.

Si el encabezado de contenido vacío ya no existe, entonces al menos puedo trabajar con Gitbook para arreglar su sistema de incrustación (lo cual no sucederá, ya que actualmente están reescribiendo toda su aplicación desde cero y se niegan a abordar errores existentes :/, pero eso al menos no es problema de Discourse).

Déjame aclarar primero que gran parte de lo que se ha escrito arriba está muy por encima de mis posibilidades, pero estoy agarrándome de pajas aquí. Es perfectamente válido que me digan que soy un idiota si estoy interviniendo en el tema incorrecto.

Estamos viendo esto bastante a menudo, ya que frecuentemente publicamos enlaces a nuestro Centro de Ayuda (base de conocimientos) en nuestra Comunidad.

Algunos ejemplos de enlaces que no generan la previsualización (Onebox):

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

Desde el panel de vista previa mientras escribo:

1 me gusta

Tras investigar un poco más, resulta que es la gema Excon la que añade Content-Length: 0, pero no en las solicitudes GET.

Sin embargo, ese código lleva ahí 8 o 9 años, así que probablemente no sea el problema.

El archivo Gemfile.lock te mostrará las gemas utilizadas por Discourse core.

2 Me gusta

Este sitio está protegido por un captcha de Cloudflare y bloquea a Discourse para que no pueda extraer ninguna información de él :slightly_frowning_face:

2 Me gusta

Al visualizar esta página en un navegador, sí contiene las etiquetas meta necesarias para construir un Onebox. Sin embargo, al intentar obtener la URL, parece que estamos recibiendo un error.

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

Esto significa que solicitamos esa URL con un agente de usuario (“User-Agent”) de “Discourse Forum Onebox v2.8.0.beta4”, pero el servidor web remoto devolvió un código de estado 403.

De manera similar, usando la herramienta de línea de comandos 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

Esto dice exactamente lo mismo: estamos enviando una solicitud válida, pero el servidor web remoto se niega a devolver un resultado. ¿Sería posible que las personas responsables de help.republicwireless.com desbloqueen estas solicitudes válidas?

Estos dos sitios no tienen las etiquetas de título/descripción OpenGraph; sin embargo, sí cuentan con otros títulos/descripciones a los que el Onebox debería recurrir como alternativa. Esto es algo en lo que deberíamos investigar para solucionarlo.

2 Me gusta

:confused: Sin embargo, ha estado funcionando durante años.

Aquí tienes un ejemplo donde un enlace del mismo sitio muestra un Onebox válido: https://forums.republicwireless.com/t/4-digit-pin-which-i-have-forgot/37655/2

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

1 me gusta

Cloudflare cambia constantemente su algoritmo de detección de bots, por lo que si deseas evitar que Discourse sea bloqueado, es posible que debas contactar a su soporte y preguntar por qué se está bloqueando la solicitud.

5 Me gusta

Lo siento, cerramos esto por estar obsoleto. Necesitaremos una nueva reproducción de los problemas para abrir.