昨日のこと。jszwedko/go-circleci というパッケージを使用してCircleCI EnterprizeのAPIを叩くという処理を実装していたのですが、どうにもうまくいかない。正直に言ってこのパッケージはドキュメントがしっかりしている、という訳ではないし、エラーメッセージを見ても何がだめなのか(そもそも現在使用しているCircleCI Enterpriseで使用できるかもよくわかっていなかった)わからない。
しかしまぁ、自分でHTTP requestを作ったりしてあれやこれややるのもまぁ面倒であるので、なんとかデバッグしたいと思ったのですが、外部のパッケージをフォークして変更を加えてデバッグする・・・という様なことはもちろんやりたくないわけです。
このパッケージは*http.Client
を指定できます。*http.Client
はインターフェースではなく構造体なので、別の実装に置き換えるということはできません。が、その実装はほぼほぼ後述するhttp.RoundTripper
なため、http.RoundTripper
をラップして、HTTP requestとHTTP responseをログに吐けばまぁ、何が問題かわかるだろう、と考えました。
そんなモノは誰かがすでに書いているだろう、というのはさておき、http.RoundTripper
を実際にいじってみるということはやったことがなかったので、勉強がてらnasa9084/go-logtransport
なるものを書きました。
書いていく途中で、考えたことなど、記録に残しておくのも良さそうと思ったため、本記事とします。
http.Clientとhttp.RoundTripper
Go言語でHTTPのリクエストを発行するには基本として*http.Client
というものを使用します。簡便のため、GET
、POST
、Head
についてはパッケージグローバルの関数も用意されてはいるのですが、これらも内部的にはパッケージグローバルで宣言されたDefaultClient
という*http.Client
が使用されています。
*http.Client
はゼロ値で使用できるようにまとめられた構造体で、DefaultClient
は*http.Client{}
と宣言されています。
普段はこの*http.Client
を使用してHTTPの通信を行うわけですが、実は*http.Client
はそれほど多くの機能は持っていません。実際、持っているフィールドはたったの4つ(Go1.13時点)しかないのです。*http.Client
はリダイレクトやクッキーなどの一部の処理だけを受け持っていて、実際のHTTP通信のほとんどはフィールドとして保持しているhttp.RoudTripper
が行います。
http.RoundTripper
はインターフェースとして定義されていて、自由に差し替えをすることができます。特に指定していない場合は*http.Transport
がデフォルトの実装として使用されます。
Goの他の標準パッケージの例に漏れず、http.RoundTripper
は非常にシンプルなインターフェースで、次の様に定義されています。
|
|
RoundTrip()
がHTTP requestを受け取り、HTTP responseを返します。つまり、requestのログをとり、子RoundTripper
のRoundTrip()
を実行し、Response
のログをとってそのまま返す、という様なラッパーを書けば良さそうです。
|
|
実際にrequestとresponseのログをとるには、net/http/httputil
パッケージのDump
系関数が使用できます。今回はクライアント側の実装なので、httputil.DumpRequestOut
とhttputil.DumpResponse
を使用します。
テスト
実装の詳細はそれほど難しい内容ではないのでさておき、テストをどう書くか、という話をしましょう。
HTTPに関連したテストを書くとき、Go言語ではnet/http/httptest
を使用すると便利です。
テストを書くにあたり、最初は子RoundTripper
をモックして、適当にResponseを返すモノをつくればよいか、と思ったのですが、いい感じにテスト用のResponseを作成するのは面倒そうでした。
そういえば今日、http.Transportにロガーを仕込むの書いてみて、テストを書くのにRequestとResponse生成したいな・・・って考えてhttptestにないか見て、なんでないんや!って一瞬おこだったけどhttptest.Server使えばええんや、とすぐに思い直したのでアレがそれでそんな感じでした(とりとめが無い
— nasa9084@某某某某(0x1a) (@nasa9084) October 31, 2019
しかし素直にhttptest.Server
を使えば、クライアント側では一切テスト用に特殊な実装を使用することなく、普通にリクエストをしてレスポンスを受けることができます(httptest.Server
は日常的に使っているのに、なぜ忘れていたのか・・・)。
これを使用して、シュッと適当なハンドラをサーブしてことなきをえました。
interface or struct
さて、今回得た教訓として、別に必ずしもすべてをinterfaceで定義する必要は無いと言うことです。
HTTPでいえばhttp.Client
は構造体として定義されていますが、これを使う側はいちいちinterfaceにして隠蔽せずとも、そのコアであるTransport
がRoundTripper
interfaceとして定義されているため、実装を差し替えることができます。
例えば、OAuthを使用した認証をしたい場合、golang.org/x/oauth2
を使用するのが簡単ですが、これもhttp.Client.Transport
に認証の設定を加えることで、http.Client
を使用するコードが意識することなく認証ができるように実装されています。
sql.DB
なども、実際にデータベースに接続するドライバ部のみがinterfaceとして定義されています。
とりとめが無くなってきたのでこの辺で。