プログラミング
hyper HTTPライブラリのバグを発見
We found a bug in the hyper HTTP library (blog.cloudflare.com)
要約
Cloudflareのチームは、Workers上でRustで構築されたImagesサービスにおいて、hyper HTTPライブラリに潜在する特定条件下でのみ発生する競合状態のバグを発見しました。このバグは、大規模な画像の変換リクエストにおいて、データが途中で切断されるにもかかわらず200 OKが返されるという事象を引き起こしました。6週間にわたる調査の結果、わずか4行のコードでこのバグを修正しました。
全文翻訳
hyper HTTPライブラリのバグをいかにして発見したか
2026年6月22日 Deanna Lam, Diretnan Domnan, Matt Lewis
12分読了
Cloudflareのエッジネットワーク上のすべてのマシンで稼働する、Workers上にRustで構築されたImagesサービスは、クライアント接続を処理するために、Rust用のオープンソースHTTPライブラリであるhyperを使用しています。
昨年、Workersでリモート画像の処理のためのカスタムでプログラム可能なワークフローを可能にするImagesバインディングを導入しました。2025年末には、WorkersランタイムとImagesサービスの間でより直接的なローカル接続を提供するために、バインディングを再構築しました。
ロールアウト直後、バインディングからの変換リクエストが失敗するという報告を受けました。ただし、それは断続的であり、しかも大規模な画像に限られていました。さらに奇妙なことに、これらのリクエストに対する応答は、エラーがログに記録されていないにもかかわらず200ステータスを返していました。画像データは単純に途中で切断されていました。2メガバイトあるべき応答が、数百キロバイトで届くといった具合です。
私たちは6週間を費やし、hyperライブラリに潜在するほとんど目に見えないバグ、つまり特定の条件下でのみ発生する競合状態を追跡しました。このバグは、Imagesバインディングが処理された画像データをクライアントに返す方法に影響を与えていました。最終的に、修正にはわずか4行のコードしか必要ありませんでした。
ホップ、ハンドオフ、そしてhyper
開発者がCloudflare上で構築する際、彼らはバインディングを通じてWorkersからアクセス可能な一連のプラットフォームサービスからフルスタックアプリケーションを構成します。バインディングは、コンピューティング、ストレージ、AI推論、メディア処理など、開発者プラットフォーム上のリソースへの直接的なAPIを提供します。
Imagesバインディングは、画像の最適化と配信を分離します。出力をHTTP応答として返す必要なく、画像をトランスコード、合成、または操作できます。また、URLインターフェースによって課せられる固定シーケンスに従うことなく、任意の順序で最適化パラメータを適用できます。ここで、Workerは画像データをImages APIに直接渡し、操作を連結し、処理された結果をストリームとして受け取ることができます。
```javascript
const result = await env.IMAGES
.input(image)
.transform({ width: 800, rotate: 90 })
.output({ format: "image/avif" });
return result.response();
```
大まかに言えば、画像データは私たちの様々なサービスを通じてこのように移動します。パイプは、中間サーバーとImagesサービス間のソケット接続を表し、データはカーネルのバッファを通じてあるプロセスから次のプロセスへとハンドオフされます。
バインディングは、Workersランタイムが管理するソケット接続を通じてImagesと通信します。ソケット接続は、2つのプロセス間の通信チャネルです。ソケットの両端には、オペレーティングシステムのカーネルによって管理されるバッファがあります。これらのバッファは、一方がデータを書き込んだ後、もう一方がそれを読み取るまでの間、データが一時的に保持される領域です。
HyperはImagesサービス側で接続を管理し、ソケットから受信リクエストを読み取り、応答をソケットに書き戻します。
リクエストがImagesバインディングを使用すると、Imagesサービスは入力を読み取り、要求された最適化操作を実行し、結果をエンコードします。その後、エンコードされた画像全体を単一のメモリ内ブロックとしてhyperに渡します。Hyperはこの応答データを自身の内部バッファに書き込みます。この時点で、hyperは送信に必要なすべてのバイトを持っているため、エンコード作業が完了したと見なします。次のステップは、その内部バッファをソケットの送信バッファにフラッシュし、データをImagesサービスから反対側の中間サーバーに移動させることです。
反対側の読み取り側が速い場合、hyperは一度にすべてをフラッシュできます。読み取り側がデータが到着するのと同じくらい速くデータを消費しているため、送信バッファには空きがあります。すべてのデータが送信されると、hyperはソケット上でシャットダウンを発行し、接続が完了し、これ以上データが書き込まれないことを通知します。しかし、読み取り側が遅い場合(数ミリ秒であっても)、送信バッファは満杯になり、hyperは書き込みを継続するための空きができるまで待つ必要があります。
ローカルを取る
Cloudflareのネットワーク上のすべての受信トラフィックは、セキュリティおよびパフォーマンス機能を実行し、リクエストを適切なバックエンドにルーティングする内部中間サービスであるFLを通過します。最初にバインディングを立ち上げたとき、画像データはWorkersランタイムからFLを介してImagesサービスに流れました。
このパスは、初期リリースに自然に適合し、URLインターフェースと同じアーキテクチャに従っていました。しかし、時間の経過とともに、FLとのこの結合が制約となりました。バインディングへのすべての変更は、FLのリリースサイクルに従わなければなりませんでした。
2025年12月、ImagesチームはFLを新しい中間サービス、つまり同じマシン上で動作する内部ワーカーバインディングに置き換えました。元のアーキテクチャでは、データはネットワークソケットを介してFLを通過していました。このパスは、DNSルックアップやルーティングなど、FLの完全な処理パイプラインのオーバーヘッドを伴いました。
内部バインディングは、これらのオーバーヘッドをUnixソケットに置き換え、同じマシン上のサービスを直接接続し、FLとネットワークスタックのオーバーヘッドをバイパスしました。これにより、Imagesへのリクエストパスが高速化され、チームはバインディングのリリースを独立して制御できるようになりました。
ロールアウトから数日以内に、最初の顧客からの報告を受けました。
200 OK (OKではない)
最初の問題の兆候は、非標準のセットアップを持つ顧客から来ました。つまり、1つのパイプラインが別のパイプライン内にネストされた2層の画像処理でした。
まず、彼らのWorkerはImagesバインディングを使用して、R2からの複数の大きなソース画像(JPEG背景とPNGオーバーレイ層)を単一の結合されたJPEGに合成しました。次に、URLインターフェースを通じて結果をさらに圧縮、トランスコード、およびサイズ変更しました。バグは、内部パイプラインの戻りパスで発生し、応答が外部パイプラインに到達する前に切り詰められました。
内部パイプライン(変換バインディング)は合成を処理しました。外部パイプライン(変換URL)は、スケーリングやフォーマット変換などの配信最適化を処理しました。このレイヤー化されたアプローチは、内部パイプラインがサイレントに切り詰められた応答を返した場合、唯一の目に見えるエラーが1レベル上に現れることを意味しました。
error reading a body from connection: end of file before message length reached
外部パイプラインは内部パイプラインからHTTP 200を受信し、Content-Lengthヘッダーは数メガバイトを約束していました。実際の本文はそのごく一部にすぎませんでした。あるリクエストでは、予想される3.3 MBのうち、約200 KBしか届きませんでした。エラーは外部パイプラインで表面化しましたが、切り詰めはバインディング、中間サービス、Imagesサービス、またはその間のどこかで発生した可能性があります。
ブラウザが切り詰められた画像を受信すると、その結果は目に見えます。フォーマットによっては、画像は部分的にレンダリングされるか(例:下半分が欠落しているか灰色になっている)、または完全にデコードに失敗し、代わりに壊れた画像が表示されます。
闇の中でのデバッグ
ここから、リクエストパスを内側に向かって作業し、各レイヤーをテストして切り詰めがどこで発生しているかを特定しました。これらの努力の中には行き詰まったものもありました。他のものは、検索範囲を狭めるヒントを残しました。
再現の構築。私たちは顧客のネストされたセットアップを模倣したWorkerを構築し、バインディング単独でバグをトリガーできるまでレイヤーを剥ぎ取りました。小さなスクリプトで、バッチでリクエストを発行できました。ある初期の実行では、25件のリクエストのうち19件が失敗しました。到着したデータの量(約200 KB)は、本番環境のソケットバッファのサイズに疑わしいほど近かったのです。これは、問題が顧客の構成に関連していないことを確認し、オンデマンドでバグをトリガーする信頼できる方法を与えてくれました。
タイムアウトの調査。当初、切り詰めはタイムアウト動作(つまり、時間制限後に接続が閉じられた)に関連しているのではないかと疑っていました。この理論は成り立ちませんでした。切り詰めはリクエストの期間とは相関がありませんでした。
hyperバージョンの更新。バグが最初に報告されたとき、私たちは0.14.xを実行していましたが、最新のhyperバージョンは約1.8.xでした。最も明白な答えが正しい(そして最も簡単な)場合に備えて、hyperバージョン0.14、1.7、1.8でテストしました。しかし、バグは各バージョンで現れたため、上流での修正はありませんでした。
ローカルでの再現。W