nasa9084
· 3 min read

net/http.ClientにHookをかける

net/http.ClientにHookをかける

昨日のこと。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というものを使用します。簡便のため、GETPOSTHeadについてはパッケージグローバルの関数も用意されてはいるのですが、これらも内部的にはパッケージグローバルで宣言された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は非常にシンプルなインターフェースで、次の様に定義されています。

type RoundTripper interface {
    RoundTrip(*Request) (*Response, error)
}

RoundTrip()がHTTP requestを受け取り、HTTP responseを返します。つまり、requestのログをとり、子RoundTripperRoundTrip()を実行し、Responseのログをとってそのまま返す、という様なラッパーを書けば良さそうです。

func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
    // Requestのログをとる
    
    resp, _ := t.Transport.RoundTrip(r)
    
    // responseのログをとる
    
    return resp, nil
}

実際にrequestとresponseのログをとるには、net/http/httputilパッケージのDump系関数が使用できます。今回はクライアント側の実装なので、httputil.DumpRequestOuthttputil.DumpResponseを使用します。

テスト

実装の詳細はそれほど難しい内容ではないのでさておき、テストをどう書くか、という話をしましょう。
HTTPに関連したテストを書くとき、Go言語ではnet/http/httptestを使用すると便利です。
テストを書くにあたり、最初は子RoundTripperをモックして、適当にResponseを返すモノをつくればよいか、と思ったのですが、いい感じにテスト用のResponseを作成するのは面倒そうでした。

しかし素直にhttptest.Serverを使えば、クライアント側では一切テスト用に特殊な実装を使用することなく、普通にリクエストをしてレスポンスを受けることができます(httptest.Serverは日常的に使っているのに、なぜ忘れていたのか・・・)。

これを使用して、シュッと適当なハンドラをサーブしてことなきをえました。

interface or struct

さて、今回得た教訓として、別に必ずしもすべてをinterfaceで定義する必要は無いと言うことです。
HTTPでいえばhttp.Clientは構造体として定義されていますが、これを使う側はいちいちinterfaceにして隠蔽せずとも、そのコアであるTransportRoundTripper interfaceとして定義されているため、実装を差し替えることができます。

例えば、OAuthを使用した認証をしたい場合、golang.org/x/oauth2を使用するのが簡単ですが、これもhttp.Client.Transportに認証の設定を加えることで、http.Clientを使用するコードが意識することなく認証ができるように実装されています。

sql.DBなども、実際にデータベースに接続するドライバ部のみがinterfaceとして定義されています。

とりとめが無くなってきたのでこの辺で。