リンクプレビュー HTTP GET が仕様に違反する

長らく解決できていない問題に直面しており、まだ報告されていないようです。多くの要素が絡んでおり申し訳ありませんが、簡潔に説明させていただきます。

TL;DR: メッセージにリンクを貼り付けると、その URL に対して HTTP GET リクエストを送信して埋め込みデータを取得する Ruby Gem が、一部の HTTP プロキシから仕様違反とみなされるリクエストを送信してしまいます。これにより、場合によってはプレビューが機能しなくなります。

もう少し詳しく説明します。私たちはドキュメント用に Gitbook.io という便利なサービスを利用しています。Gitbook はホスト型ソリューションであり、サイト内のリダイレクトに Cloudflare ワーカーを使用しています。その Cloudflare ワーカーの一部には、Node Fetch API を用いて HTTP リクエストをプロキシする処理が含まれています。Node Fetch の開発者は仕様への厳格な遵守を非常に重視しており、HTTP ボディを持つ GET リクエストだけでなく、Content-Length ヘッダー(0 に設定されている場合でも)が含まれているリクエストを拒否します。

まさにそれが起こっています。HTTP リクエストを送信する Ruby Gem が以下のようなリクエストヘッダーを送信します。

Content-Length: 0

これにより、Node Fetch プロキシが非常に不満を持ち、最終的にリモートサーバーからリクエストが拒否されてしまいます。GET リクエストにボディを含めること、あるいは単に Content-Length ヘッダーを含めることが HTTP 仕様上有効かどうかについては、さまざまなフォーラムで議論が行われてきました。私自身は問題ないと考えていますが、Node Fetch の開発者がそのような仕様に同意するよう求めるすべての Issue をクローズし続けてきたことは変わりません。

私は残念ながらこの板挟み状態に陥っています。

  • Node Fetch プロジェクトは、これらの HTTP リクエストを有効とは見なすことを拒否しています refuses to consider these HTTP requests as valid
  • Cloudflare サポートは、問題となっている Node ベースのワーカーを制御できないため、私の助けにはなれません。
  • Gitbook のサポートは、Fetch の開発者の意見に同調しているため(そして本当に気にしていない可能性もあります)、私の助けにはなれません。
  • そして、HTTPrb ライブラリは ヘッダーの削除を拒否 しており、彼らにとっては完全に有効な仕様だと考えているためです。

そのため、ここで質問させていただきます。リンクプレビューのために送信される HTTP GET リクエストを制御または変更し、Node Fetch のような非常に厳格なライブラリを使用するプロキシがこれらのリクエストを拒否しないよう、許容される HTTP ヘッダーのセットを含めることは可能でしょうか?

もし試してみたい場合は、Gitbook のサーバーにホストされており、Node Fetch 駆動の Cloudflare ワーカーを使用している以下の URL を例にご覧ください。

「いいね!」 6

@jamie.wilson / @techAPJ、なぜリクエストに Content-Length を 0 で送信しているのか、理由がわかりますか?この動作を確認してもらえますか?HEAD リクエストであれば納得がいきますが、GET リクエストでもそうなのでしょうか?

「いいね!」 2

@sam さん、こんにちは。HTTP リクエストは、この挙動を持つ「httprb」という Ruby ライブラリによって行われているようです。「HTTPrb ライブラリは、ヘッダーを削除することを拒否している。それは彼らにとって完全に有効だと考えているため」という箇条書きのリンクを見ると、そのライブラリの開発者が、HTTP 仕様を破るのではなく、それを柔軟に解釈している理由を主張していることがわかります。

この件についてインターネット上で各方面に問い合わせて合意を得ようとしていたところ、httprb に対してこのプルリクエストを送信できる人が見つかりました。これで問題が解決する可能性があります。

私は Ruby 開発者ではないため、これをテストする方法さえわかりません。最終的には、この Gem が修正版をリリースし、その後 Discourse がそのバージョンを使用するように更新されると想定されます。動作するかどうかを確認できる方法をお持ちの方がいらっしゃれば、大変助かります。再現手順は非常にシンプルです。上記のリンクを私の Gitbook URL に貼り付け、プレビューが拒否されるかどうかを確認してください。

「いいね!」 1

以下の表示が確認されています(あなたの最初の投稿にある画像と一致します):

「Getting Started Guide」というテキストが表示されていることから、リクエストは成功していることがわかります。これは og:title メタタグからその文字列を取得しています:

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

説明が不足しているというエラー/警告も正しいものです。ページのコンテンツは以下の通りです:

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

画像の URL は og:image タグから取得されます。このタグは以下の通りです:

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

https://app.gitbook.com/share/space/thumbnail/-LA-UVvV3_TgzQyCXMWK.png をブラウザ(macOS 上の最近の Safari)に貼り付けると、以下のエラーが表示されます:

Error: could not handle the request

curl で同じリクエストを送信しても、同様の応答が返されます:

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

og:image の URL をブラウザに貼り付けた場合、画像が表示されますか?

まとめると、元の URL からの応答に基づくと、Onebox プレビューは期待通りに動作しているようです。

「いいね!」 3

@jamie.wilson ご多忙の中、調査いただきありがとうございます。ただし、上記のテストが前述のプルリクエストを含む最新の httprb gem バージョンで行われたものか、それとも以前のバージョンのライブラリで行われたものか、明確にしてください。

当初、Onebox プレビューで表示されていたエラーは、対象 URL が 500 ステータスコードを返すというものでした。私がこの投稿をする前のある時点で、Onebox プレビューは代わりに「Open Graph メタデータが不足している」という注意書きを表示するようになりました。Gitbook サポートに連絡する前の数ヶ月間、私はこの問題のトラブルシューティングを行ってきたため、その間に何らかの変更があった可能性があります。

もし Gitbook の URL が実際に読み込まれており、単にメタデータや画像が不足しているだけなら、それはリクエストが拒否されている場合とは異なります。しかし、私が送信する Content-Length: 0 という HTTP リクエストヘッダーを含むリクエストは、リモートサーバー上の CloudFlare ワーカーによって拒否されることは確実です。もしかすると、Discourse でリクエストを送信するために使用される HTTP クライアントが変更されたのでしょうか?Discourse のソースコードについては何も知りませんし、httprb ライブラリが実際に HTTP リクエストの発生源であるかどうかさえ、100% 確信していません。

httprb ガムは全く使用していないはずです。Oneboxing プロセス(リンクプレビューを生成する仕組み)は、Ruby の標準ライブラリにある Net::HTTP と、フローの一部として Excon ガムを使用しています。

もう少し深く調査したところ、Content-Length: 0 ヘッダーを含むリクエストを生成することがあることが分かりました。ただし、少なくとも提供された URL の場合、これが Onebox の生成を妨げているわけではありません。

マイナーバージョンのアップグレードがあった可能性はありますが、リクエストの仕組みや使用するライブラリを根本から再構築するような大きな変更はありませんでした。

Oneboxing をより堅牢にするためのいくつかの変更があり、以前は 500 エラーを返していた URL が正常に Onebox できるようになった理由を説明している可能性があります。

現在 Oneboxing 中にエラーを返している、または Discourse の他の部分で期待通りに動作していない URL を共有できる場合は、ぜひお知らせください!

「いいね!」 3

ああ、これは非常に有益な情報ですね。Gitbook チーム(CloudFlare プロキシをメンテナンスしているチーム)からはほとんど助けを得られなかったため、この時点でどのライブラリが関与しているのか、かなり推測に頼らざるを得ませんでした。

わかりました。上記で共有したかどうかは覚えていませんが、Gitbook から得られた唯一の情報は、Discourse からのプレビューリクエストを拒否していた CloudFlare エラーログのエラー内容が以下の通りだったということです。

GET または HEAD メソッドのリクエストには、ボディを含めることはできません。

不明なのは、Discourse からの GET リクエストが実際にボディを 含んでいた のか、それとも単に Content-Length: 0 ヘッダーを持っていたのかという点です。いずれにせよ、これは一部の人間(CloudFlare の担当者を含む)によれば、Fetch 仕様違反となります。

はい、ある時点で Onebox エラーが一般的な 500 から、何らかのデータを含むものに変更されたように見えます。どのライブラリがアップグレードされたのかはわかりません(この間に Discourse も更新しました)。Discourse から送信されているヘッダーを正確にキャプチャする方法があればいいのですが、http://httpbin.org/get のような URL にアクセスしても、結果が Onebox によって完全に消費され、私の知る限りどこにもログに残らないため、「確認」する方法がありません。

もし空の Content-Length ヘッダーがもう存在しないのであれば、少なくとも Gitbook と連携して埋め込み機能を修正する作業を進められます(彼らは現在アプリ全体をゼロから書き直しており、既存のバグに対処することを拒否しているため、それは実現しないでしょう :confused: ですが、少なくともそれは Discourse の問題ではありません)。

まず、上記の多くの内容は私には難しすぎることをお断りしておきますが、今、必死に手探りで探っています。もし私が誤ったトピックにコメントしているなら、私が無知であることを教えていただいても全く構いません。

私たちはこれを頻繁に目にしており、コミュニティ内でヘルプセンター(ナレッジベース)へのリンクを頻繁に投稿しているためです。

Oneboxingに失敗するリンクの例をいくつか挙げます:

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

入力中のプレビューパネルから:

「いいね!」 1

さらに詳しく調査した結果、GET リクエスト以外では Excon gem が Content-Length: 0 を追加していることが判明しました。

ただし、そのコードは 8〜9 年前から存在しているため、おそらく問題の原因ではありません。

Gemfile.lock ファイルには、Discourse コアで使用されている gem が一覧表示されます。

「いいね!」 2

このサイトは Cloudflare の CAPTCHA の背後にあり、Discourse が情報を取得するのをブロックしています :slightly_frowning_face:

「いいね!」 2

ブラウザで表示すると、このページには Onebox を構築するために必要なメタタグが含まれています。しかし、その URL を取得しようとするとエラーが発生しているようです!

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

これは、「Discourse Forum Onebox v2.8.0.beta4」という User-Agent でその URL を要求しましたが、リモート Web サーバーが 403 ステータスコード を返したことを意味します。

同様に、コマンドラインツール 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

これも同じことを示しています。有効なリクエストを送信していますが、リモート Web サーバーは結果を返すことを拒否しています。help.republicwireless.com の担当者には、これらの有効なリクエストをブロック解除することは可能でしょうか?

この 2 つのサイトには OpenGraph のタイトル/説明タグが含まれていませんが、Onebox がフォールバックとして使用できる他のタイトル/説明は存在します。これは修正を検討すべき課題です。

「いいね!」 2

:confused: でも、それは何年も機能していました。

同じサイトからのリンクが有効な Onebox を表示する例を以下に示します:https://forums.republicwireless.com/t/4-digit-pin-which-i-have-forgot/37655/2

これは https://help.republicwireless.com/hc/en-us/articles/115012101188-Can-t-Get-Past-the-Screen-Lock-on-the-Phone にリンクしています

「いいね!」 1

Cloudflare はロボット検出アルゴリズムを頻繁に変更しています。Discourse がブロックされないようにしたい場合は、Cloudflare のサポートに連絡し、リクエストがブロックされる理由を確認することをお勧めします。

「いいね!」 5

申し訳ありませんが、これは古い問題として閉じます。問題を開くには、新しい再現手順が必要です。