Anteprima link HTTP GET non rispetta le specifiche

Da tempo sto affrontando un problema che non sembra essere stato ancora segnalato. Mi scuso per la complessità della situazione, ma cercherò di descriverla in modo conciso.

In sintesi: quando incollo un link in un messaggio, il Ruby Gem che effettua la richiesta HTTP GET a quell’URL per recuperare i dati di embedding invia una richiesta che alcuni proxy HTTP considerano non valida secondo le specifiche. Questo impedisce il corretto funzionamento delle anteprime in alcuni casi:

La versione leggermente più dettagliata è questa. Utilizziamo un ottimo servizio chiamato Gitbook.io per la nostra documentazione. Gitbook è una soluzione ospitata e utilizza Cloudflare Workers per le reindirizzamenti interni sul proprio sito. Parte di questi Cloudflare Workers prevede l’uso della Node Fetch API per proxyficare le richieste HTTP. Gli sviluppatori di Node Fetch sono estremamente rigorosi nel rispetto delle specifiche e rifiutano qualsiasi richiesta GET che abbia un corpo HTTP o anche solo un header Content-Length, anche se impostato a 0.

Ed è esattamente ciò che sta accadendo. Il gem Ruby che effettua la richiesta HTTP invia un header

Content-Length: 0

che infastidisce moltissimo il proxy di Node Fetch, finendo per causare il rifiuto della richiesta da parte del server remoto. Su diversi forum c’è stato un ampio dibattito sulla validità, secondo le specifiche HTTP, di un corpo nella richiesta GET o anche solo della presenza di un header Content-Length. A me non crea problemi, ma questo non ha fermato gli sviluppatori di Node Fetch dal chiudere ogni issue aperta chiedendo loro di permettere tale semantica.

Purtroppo mi trovo nel mezzo di questa situazione:

  • Il progetto Node Fetch rifiuta di considerare valide queste richieste HTTP.
  • Il supporto di Cloudflare si rifiuta di aiutarmi perché non ho il controllo sui Worker basati su Node in questione.
  • Il supporto di Gitbook si rifiuta di aiutarmi perché sono d’accordo con gli sviluppatori di Fetch (e non sono sicuro che loro si preoccupino davvero).
  • E la libreria HTTPrb si rifiuta di rimuovere l’header perché ritengono che sia perfettamente valido.

Quindi mi trovo qui a chiedervi se esiste un modo per controllare o modificare le richieste HTTP GET effettuate per le anteprime dei link, in modo da includere un insieme accettabile di header HTTP tali che i proxy che utilizzano librerie estremamente rigorose come Node Fetch non rifiutino queste richieste.

Se volete provare, ecco un URL di esempio ospitato sui server di Gitbook che utilizza il loro Cloudflare Worker basato su Node Fetch.

6 Mi Piace

@jamie.wilson / @techAPJ avete idea del motivo per cui stiamo inviando Content-Length pari a 0 con le nostre richieste? Potete confermare questo comportamento? Immagino abbia senso per HEAD, ma per GET?

2 Mi Piace

Ciao @sam, le richieste HTTP sembrano essere effettuate da una libreria Ruby chiamata httprb, che ha questo comportamento. Se guardi il link nel punto “La libreria HTTPrb si rifiuta di rimuovere l’intestazione perché la ritiene perfettamente valida”, puoi vedere lo sviluppatore di quella libreria spiegare perché sta piegando la specifica HTTP senza violarla.

Mentre stavo cercando in giro per internet di trovare qualcuno con cui accordarmi, sono riuscito a ottenere che qualcuno inviasse questa pull request a httprb, che potrebbe risolvere il problema.

Non sono uno sviluppatore Ruby, quindi non saprei nemmeno come testare questa soluzione. Immagino che alla fine questo Gem rilascerà una versione con la correzione e, successivamente, Discourse aggiornerà il proprio codice per iniziare a utilizzarla. Sarebbe fantastico se qualcuno avesse un modo per verificare se funziona. Il caso di riproduzione è molto semplice: incolla semplicemente il link sopra al mio URL Gitbook e controlla se l’anteprima viene rifiutata.

1 Mi Piace

Vedo quanto segue (che corrisponde all’immagine nel tuo primo post):

Il testo ‘Getting Started Guide’ ci indica che la richiesta è andata a buon fine: sta estraendo quella stringa dal tag meta og:title:

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

L’errore/avviso che la descrizione manca è anch’esso corretto. Il contenuto della pagina è il seguente:

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

L’URL dell’immagine proviene dal tag og:image, che è così definito:

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

Se copio e incollo https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png nel mio browser (Safari recente su MacOS), ricevo un errore che dice:

Error: could not handle the request

Effettuando la stessa richiesta tramite curl ottengo la stessa risposta:

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

Riesci a vedere un’immagine se copi e incoll l’URL og:image nel tuo browser?

In sintesi: l’anteprima Onebox sembra comportarsi come previsto, in base alla risposta ottenuta dall’URL originale.

3 Mi Piace

@jamie.wilson Grazie per il tempo dedicato a esaminare la questione. Potresti però chiarire se il tuo test sopra menzionato è stato eseguito con la versione più recente del gem httprb, che include la pull request citata, oppure con una versione precedente della libreria?

L’errore originale che vedevo nell’anteprima di Onebox era che l’URL di destinazione restituiva un codice di stato 500. In un certo momento prima di pubblicare questo messaggio, l’anteprima di Onebox ha iniziato invece a mostrare la nota relativa ai metadati Open Graph mancanti. Dato che ho risolto questo problema per mesi prima di pubblicare con il supporto di Gitbook, è possibile che nel frattempo qualcosa sia cambiato.

Se l’URL di Gitbook viene effettivamente caricato ma mancano solo alcuni metadati o alcune immagini, allora la situazione è diversa rispetto al rifiuto della richiesta. Tuttavia, sono certo che qualsiasi richiesta che invio io stesso contenente l’intestazione HTTP Content-Length: 0 venga rifiutata dai worker di CloudFlare sul server remoto. Forse il client HTTP utilizzato per effettuare le richieste in Discourse è cambiato? Non conosco il codice sorgente di Discourse e non sono nemmeno certo al 100% che la libreria httprb sia la vera origine delle richieste HTTP.

Non credo che utilizziamo affatto il gem httprb. Il processo di Oneboxing (cioè ciò che genera le anteprime dei link) utilizza Net::HTTP dalla libreria standard di Ruby e anche il gem Excon come parte del flusso.

Approfondendo un po’, vedo che a volte generiamo richieste con un’intestazione Content-Length: 0. Tuttavia, nel caso dell’URL fornito, almeno, questo non interferisce con la generazione dell’Onebox.

Potrebbe esserci stato un aggiornamento di versione minore, ma nulla di significativo come una riprogettazione di come effettuiamo le richieste o delle librerie che utilizziamo per farlo.

Ci sono stati alcuni cambiamenti per rendere l’Oneboxing più robusto in generale, il che potrebbe spiegare perché gli URL che in precedenza restituivano errori 500 ora generano correttamente l’Onebox.

Se hai URL che puoi condividere e che attualmente restituiscono errori durante l’Oneboxing (o non funzionano come previsto in qualche altra parte di Discourse), sentiti libero di inviarmeli!

3 Mi Piace

Ahh, quindi questa è un’ottima informazione. Ho dovuto fare congetture selvagge su quali librerie fossero coinvolte fino a questo punto, in gran parte a causa della scarsa assistenza ricevuta dal team di Gitbook che mantiene i proxy CloudFlare.

Capito. Non credo di averlo condiviso sopra, ma l’unico pezzo di informazione che sono riuscito a ottenere da Gitbook è che l’errore nei loro log di errore CloudFlare, che stava rifiutando le richieste di anteprima da Discourse, era questo:

Una richiesta con metodo GET o HEAD non può avere un corpo.

Non è chiaro se la richiesta GET da Discourse abbia effettivamente contenuto un corpo o solo l’intestazione Content-Length: 0. In ogni caso, ciò viola la specifica Fetch secondo alcune persone (inclusi quelli di Cloudflare)

Sì, a un certo punto sembra che l’errore dell’Onebox sia cambiato da un generico 500 a uno che ora include alcuni dati. Non si può sapere quali librerie siano state aggiornate (e nel frattempo ho aggiornato Discourse). Vorrei avere un modo per catturare esattamente quali intestazioni vengono inviate da Discourse, ma anche se colpisco un URL come http://httpbin.org/get, non ho modo di “vedere” cosa viene restituito, poiché i risultati sono consumati interamente dall’Onebox e non vengono registrati da nessuna parte, per quanto ne sappia.

Se l’intestazione Content-Length vuota è ora scomparsa, posso almeno lavorare con Gitbook per risolvere il loro sistema di embedding (cosa che non accadrà, dato che stanno attualmente riscrivendo l’intera applicazione da zero e rifiutandosi di affrontare qualsiasi bug esistente :confused: ma almeno questo non è un problema di Discourse).

Vorrei prima precisare che gran parte di quanto scritto sopra è al di sopra delle mie capacità, ma sto cercando di fare del mio meglio. Non c’è alcun problema nel farmi sapere che ho torto se sto intervenendo in un argomento sbagliato.

Questo capita abbastanza spesso, perché pubblichiamo frequentemente link al nostro Centro Assistenza (knowledge base) nella nostra Community.

Ecco alcuni esempi di link che non riescono a generare il Onebox:

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

Dal pannello di anteprima mentre scrivo:

1 Mi Piace

Dopo un’analisi più approfondita, è il gem Excon ad aggiungere Content-Length: 0, ma non sulle richieste GET.

Tuttavia, quel codice è presente da 8 o 9 anni, quindi probabilmente non era il problema.

Il file Gemfile.lock ti mostrerà i gem utilizzati dal Discourse core.

2 Mi Piace

Questo sito è protetto da un captcha di Cloudflare che impedisce a Discourse di recuperare qualsiasi informazione da esso :slightly_frowning_face:

2 Mi Piace

Quando visualizzata in un browser, questa pagina contiene effettivamente i meta tag necessari per costruire un Onebox. Tuttavia, tentando di recuperare l’URL, sembra che si verifichi un errore!

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

Ciò significa che abbiamo richiesto quell’URL con un User Agent “Discourse Forum Onebox v2.8.0.beta4”, ma il server web remoto ha restituito un codice di stato 403.

Analogamente, utilizzando lo strumento da riga di 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

Ciò conferma la stessa cosa: stiamo inviando una richiesta valida, ma il server web remoto si rifiuta di restituire un risultato. È possibile che i responsabili di help.republicwireless.com sblocchino queste richieste valide?

Questi due siti non dispongono dei tag OpenGraph per titolo e descrizione; tuttavia, possiedono altri titoli e descrizioni a cui il Onebox dovrebbe fare da fallback. Dovremmo esaminare come risolvere questo problema.

2 Mi Piace

:confused: Tuttavia, ha funzionato per anni.

Ecco un esempio in cui un link dallo stesso sito genera una Onebox valida: https://forums.republicwireless.com/t/4-digit-pin-which-i-have-forgot/37655/2

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

1 Mi Piace

Cloudflare modifica costantemente il proprio algoritmo di rilevamento dei robot, quindi se desideri evitare che Discourse venga bloccato, potresti contattare il loro supporto e chiedere perché la richiesta viene bloccata.

5 Mi Piace

Mi dispiace, chiudo questo come obsoleto, avremo bisogno di una nuova riproduzione dei problemi da aprire.