net/http.ClientにHookをかける

昨日のこと。jszwedko/go-circleci というパッケージを使用してCircleCI EnterprizeのAPIを叩くという処理を実装していたのですが、どうにもうまくいかない。正直に言ってこのパッケージはドキュメントがしっかりしている、という訳ではないし、エラーメッセージを見ても何がだめなのか(そもそも現在使用しているCircleCI Enterpriseで使用できるかもよくわかっていなかった)わからない。 しかしまぁ、自分でHTTP requestを作ったりしてあれやこれややるのもまぁ面倒であるので、なんとかデバッグしたいと思ったのですが、外部のパッケージをフォークして変更を加えてデバッグする・・・という様なことはもちろんやりたくないわけです。 このパッケージは<code>*http.Client</code> を指定できます。*http.Clientはインターフェースではなく構造体なので、別の実装に置き換えるということはできません。が、その実装はほぼほぼ後述するhttp.RoundTripperなため、http.RoundTripperをラップして、HTTP requestとHTTP responseをログに吐けばまぁ、何が問題かわかるだろう、と考えました。 そんなモノは誰かがすでに書いているだろう、というのはさておき、http.RoundTripperを実際にいじってみるということはやったことがなかったので、勉強がてらnasa9084/go-logtransport なるものを書きました。 書いていく途中で、考えたことなど、記録に残しておくのも良さそうと思ったため、本記事とします。 http.Clientとhttp.RoundTripper Go言語でHTTPのリクエストを発行するには基本として<code>*http.Client</code> というものを使用します。簡便のため、GET 、POST 、Head についてはパッケージグローバルの関数も用意されてはいるのですが、これらも内部的にはパッケージグローバルで宣言された<code>DefaultClient</code> という<code>*http.Client</code> が使用されています。 *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のログをとり、子RoundTripperのRoundTrip()を実行し、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.DumpRequestOutとhttputil.DumpResponseを使用します。 テスト 実装の詳細はそれほど難しい内容ではないのでさておき、テストをどう書くか、という話をしましょう。 HTTPに関連したテストを書くとき、Go言語ではnet/http/httptestを使用すると便利です。 テストを書くにあたり、最初は子RoundTripperをモックして、適当にResponseを返すモノをつくればよいか、と思ったのですが、いい感じにテスト用のResponseを作成するのは面倒そうでした。...

2019-11-01 · nasa9084

きちんとやるnet/http

皆さん、net/httpパッケージは使っていますか? Go言語の標準パッケージであるnet/httpはPythonなどの標準HTTPパッケージに比べ、人間にとっても取り扱いがしやすいため、そのまま使用している方が多いかと思います。 しかし、このnet/httpパッケージ、簡単に使えるように見えて結構落とし穴が多いのです。 1. Response Bodyはクローズする必要がある 次のコードを見てみましょう。 resp, err := http.Get("https://example.com/api") if err != nil { return nil, err } var t T if err := json.NewDecoder(resp.Body).Decode(&t); err != nil { return nil, err } return &t, nil クライアントライブラリなどでよく書きそうな処理ですね。何も問題ないと思いましたか? 公式ドキュメント を見てみましょう。 It is the caller’s responsibility to close Body. Bodyをクローズするのは関数を呼んだ人の責任、とあります。そうです。Response.Bodyは Close()しなければならないのです。ちゃんとクローズされていない場合、次のリクエストでkeepaliveコネクションの再利用がされず、パフォーマンスの悪化やコネクションリークを起こす可能性があります。 2. Response Bodyを最後まで読む Response Bodyをきちんとクローズするように修正したコードが次のようなコードです。 resp, err := http.Get("https://example.com/api") if err != nil { return nil, err } defer resp....

2019-01-08 · nasa9084

sygを使用したgraceful shutdown serverパターン

<code>github.com/nasa9084/syg</code> を使用すると、手軽にシグナルとコールバック関数のマッピングを行うことができます1 これを使用し、SIGINTを受けてgraceful shutdownできるHTTPサーバを実装してみます。 package app import ( "context" "net/http" "os" "time" "github.com/nasa9084/syg" ) type Server struct { server *http.Server closed chan struct{} } func NewServer() *Server { http.HandleFunc("/", longlongHandler) return &Server{ server: &http.Server{ Addr: ":8080", }, closed: make(chan struct{}), } } func (s *Server) Run() error { // os.Interrupt = syscall....

2018-03-10 · nasa9084

Application Specific Context

元ネタは@lestrrat さんの「Abusing type aliases to augment context.Context 」。 golangを用いてHTTPサーバを作る場合、ルーティングを定義するのに以下の様な関数を用います。 http.HandleFunc(path string, handler func(w http.ResponseWriter, r *http.Request) もちろん、http.Handleを用いる場合もありますし、gorilla/mux などのライブラリを用いることもあると思います。 ここで重要なのは、func(w http.ResponseWriter, r *http.Request)という引数の方です。 多くの場合、アプリケーションのハンドラ内ではデータベースなどの外部アプリケーション・ミドルウェアを用いることになります。 しかし、golangのHTTPアプリケーションでは、ハンドラ関数の形式がfunc (w http.ResponseWriter, r *http.Request)と決まっています。引数の追加はできないため、引数以外の方法でDB接続情報などを渡す必要があります。 これまで、golangでWebアプリケーション開発を行う場合によく用いられていたデータベースコネクションの保持方法は、dbパッケージを作成し、そこにパッケージ変数として持つ方法かと思います。が、グローバルな変数はできるだけ持ちたくない ですよね。 そこで、Go 1.8から追加された<code>context</code> を使うことができます。<code>http.Request</code> にはcontext.Contextが入っていて、<code>Request.Context()</code> でget、<code>Request.WithContext()</code> でsetできます。 context.Contextに値を持たせる方法で最初に思いつくのは<code>Context.WithValue()</code> を用いる方法ですが、これは値を取得する度にtype assertionをする必要があり、あまりよくありません 。 これを解消するため、自分で型を定義するのがよいでしょう。 package context // internal context subpackage import ( "context" "errors" ) type withSomethingContext struct { context.Context something *Something } func WithSomething(ctx context.Context, something *Something) context....

2017-11-21 · nasa9084

golang: net/httpでBASIC認証

golangでベーシック認証するのはどうしたら良いのかなー。って思ってたら、net/httpでhandlerに渡されるhttp.RequestにBasicAuth()というメソッドが生えてました。 これはBASIC認証用のユーザ名、パスワード、ヘッダ解析のフラグという値を返してくれます。 なので、 func handler(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() if !ok { return } if username == "hogehogeuser" && password == "fugafugapasswd" { // something } } とすることで認証することができます。簡単、簡単。 なお残念ながらダイジェスト認証はサポートされていない様子。

2017-05-02 · nasa9084