はじめに
楽楽販売の開発チームに所属している kasuke18 です。担当領域はアプリケーションの運用周りです。 最近、アプリケーション開発・運用中に OSS のバグを発見し GitHub の Issue を登録しました。 この記事では、そのバグの内容や OSS への Issue の報告方法についてお伝えします。
今回バグを発見したOSSは Guzzle という、PHP ではメジャーなHTTPクライアントライブラリです。
バグの説明
バグの内容は Guzzle の Issue に登録していますので、経緯にご興味がなければそちらをご参照ください。
前提:楽楽販売について
楽楽販売にはファイルアップロード機能があり、アップロードされたファイルはCloudian HyperStore
というオブジェクトストレージで管理しています。
Cloudian HyperStore
は Amazon S3 とインタフェースの互換性がありますので、ファイルアップロードなどの操作は AWS SDK を利用することができます。
今回の主題である Guzzle は私達のアプリケーションが直接利用しているのではなく、AWS SDK for PHP の中でHTTPクライアントとして使用されています。
参考:このあたりに触れた過去記事がありますので、ご興味があればご参照ください。 tech-blog.rakus.co.jp
バグの発見経緯
開発・運用中に特定のファイルをアップロードしようとすると、エラーが発生し、アップロードができない問題に遭遇しました。エラーはPHPの処理でアップロードを試みた場合にのみ発生し、CLIのaws
コマンドでは同じファイルをアップロードできました。
特定のファイルは、中身が0
という文字だけのファイルでした(md5sum値はcfcd208495d565ef66e7dff9f98764da
)。
※PHPの開発者であれば、この時点で何となく原因を推測できるかもしれません。
また、エラーログには以下のような内容が出力されていました。
Error executing "PutObject" on "${アップロードURL}"; AWS HTTP error: Error creating resource: [message] fopen(${アップロードURL}): Failed to open stream: HTTP request failed! [file] /path/to/vendor/guzzlehttp/guzzle/src/Handler/StreamHandler.php [line] 324
バグの原因調査
エラーメッセージを見ると、PHPの関数fopen
の処理でエラーが発生したと書かれています。
AWS SDK for PHP では、ファイルアップロードを WebAPI(HTTP PUTリクエスト)で行っています。
最初はファイルアップロードなのに、なぜfopen
関数を使用してファイルを開いているのか疑問に思いました。しかし、調査を進めるうちに、実際にfopen
関数を使用して WebAPI を実行できることがわかりました。
したがって、fopen
関数の使用自体に問題はなく、使用方法に何らかの問題があることが分かりました。
以下は、fopen
関数を使用してPUTリクエストを送信するサンプルですが、重要なポイントは2つあります。
stream_context_create
関数でリクエストの内容を設定すること- その設定内容を
fopen
関数に渡すことで、WebAPIを実行できること
▶
fopen
関数で PUTリクエスト を送信するサンプル
<?php // 送信するデータ $data = "This is the data to be sent"; // リクエストのURL $url = "http://example.com/api/endpoint"; // リクエスト内容を設定 $options = array( 'http' => array( 'method' => 'PUT', // リクエストヘッダ 'header' => "Content-type: text/plain\r\n" . "Content-length: " . strlen($data) . "\r\n", // リクエストボディ 'content' => $data ) ); // リクエスト送信 $context = stream_context_create($options); $result = fopen($url, 'r', false, $context);
したがってstream_context_create
関数を呼び出している箇所のコードで何か問題が起こっていないかを確認するため、Guzzle のソースコードを調べました。その結果、リクエストボディを設定する部分で怪しい処理を見つけました。具体的には、リクエストボディに設定したい内容を文字列にキャストし、empty
関数で条件分岐していました。
デバッグを行いながら処理を追っていくと、ファイルの中身が0
だけの場合、Content-Length
は設定されるがリクエストボディが設定されない、ということが確認できました。
通常、サーバーはこのようなリクエストを受け入れることは考えられないため、これがfopen
関数でエラーが発生した原因であると判断しました。
バグ報告の手順
ここでは、バグを OSS の開発者に報告するために実施した手順について説明します。今回の場合、Guzzle のバグ報告は GitHub の Issue を通じて受け付けられているため、その内容と書き方について話をします。
Issue を作成する際に注意した点は、以下の2つです。
- OSS が提供しているバグ報告用のテンプレートに従うこと
- 英語で頑張って書くこと
バグ報告用のテンプレート に従う
多くの主要な OSS では、このようなバグ報告に使用するためのテンプレートが用意されています。これに従って記述することで、フォーマットに悩む必要がなくなりますし、テンプレートを使用していない場合は情報が不足しているとして拒否されることもあります。Guzzle のバグ報告テンプレートでは、以下の項目を可能な限り埋めるよう求められています。
その中で、今回は「再現するためのコード」に苦労しました。
まず最初の課題は、私たちのアプリケーションが直接 Guzzle を使用していないため、ほぼゼロからコードを作成する必要があったことです。Guzzle の使用方法に慣れていなかったため、再現コードの正確さに不安がありましたが、「バグだと思われるコードを通過する」ことを目的として作成し、その旨を Issue に記載するという対応をしました。
もう一つの課題は、Guzzle が HTTP クライアントライブラリであるため、動作確認にはモックサーバーや関連環境が必要になることです。再現コードの検証だけならば、ローカルにモックサーバーを立ち上げることで十分ですが、厳密に報告するならその手順を記載する必要があります。 とはいってもその手順を Guzzle の開発者に提供してもあまり意味がないため、「Guzzle の開発者なら手順が確立されているだろう」と仮定し、特に記述しませんでした。
英語で書く
一般的に、OSS 開発者に日本語の理解を要求することはできないため、英語で記述する必要があります。 しかし、私自身は英語が得意とは言えませんので、翻訳ツールに頼りました。
手順としては「①日本語で記述する」→「②DeepLなどを使用して英語に翻訳する」だけではなく、「③再度日本語に翻訳し直す」ことで、日本語で書いた際の意図が抜け落ちていないかを確認しています。
この手順は、社内のエンジニアがオフショア先のチームとコミュニケーションを取る際に行っている方法を聞いたことがあり、それを取り入れてみました。
こうすることで成果物を日本語にすることができました。つまり得意ではない英語ではなく、日本語でレビューできるということになり、この点が大きなメリットでした(レビューといっても、誰かに見てもらうわけではなく、セルフチェック程度ですが...)。
バグの解決
原因コードや修正方法を提供したおかげか、追加情報を求められることもなく、すぐに修正されました。7.5.2でリリースされています。
おわりに
OSSへのバグ報告は初めての経験でしたが、以下の点に従うことでスムーズに進めることができました。
- 提供されたIssueテンプレートに従う
- 提供する情報に過不足がないかを丁寧に確認する
今回のバグは限定的なケースでのみ発生する軽微なものでしたが、それでも報告することで、一人の利用者として OSS に貢献できたのではないかと思います。