HTTP Middleware の作り方と使い方
こちらは Making and Using HTTP Middleware の日本語訳です。
HTTP Middleware の作り方と使い方
ウェブアプリケーションを構築しているときに、多くの(あるいはすべての)HTTPリクエストに対して実行したい共通機能があるかもしれません。すべてのリクエストをログに記録したり、すべてのレスポンスを gzip したり、重い処理を行う前にキャッシュをチェックしたりしたいと思うかもしれません。
このような共通機能を整理する一つの方法として、ミドルウェアを設定することがあります。Go では、HTTP リクエストの制御の流れが以下のようになるように、ServeMux とアプリケーションハンドラの間でミドルウェアを使用するのが一般的です。
ServeMux => Middleware Handler => Application Handler
今回は、このパターンで動作するカスタムミドルウェアの作り方と、サードパーティ製のミドルウェアパッケージを使った具体的な例を紹介します。
基本原則
Goでのミドルウェアの作成と使用は基本的にシンプルです。以下のようにしたいです。
その方法を説明します。
ハンドラを構築するための以下の方法をすでに理解していることを願っています (そうでない場合は、先に進む前にこの入門編を読んでおくのがベストでしょう)。
func messageHandler(message string) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(message) }) }
このハンドラでは、匿名関数の中にロジック (シンプルな w.Write という書き込みです) を配置し、メッセージ変数の上でクロージャを形成しています。そして、このクロージャを http.HandlerFunc アダプタを使用してハンドラに変換し、ハンドラを返します。
同じアプローチを使用して、ハンドラのチェーンを作ることもできます。(上記のように) クロージャに文字列を渡すのではなく、 チェーンの中で次のハンドラを変数として渡し、その次のハンドラの ServeHTTP() メソッドを呼び出することで制御を次のハンドラに移すことができます。
これにより、ミドルウェアを構築するための完全なパターンが得られます。
func exampleMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Our middleware logic goes here... next.ServeHTTP(w, r) }) }
このミドルウェアの関数は func(http.Handler) http.Handler シグネチャを持っていることに気づくでしょう。これはハンドラをパラメータとして受け取り、ハンドラを返します。これは二つの理由で便利です。
- これは http.Handler を返すので、ミドルウェアの関数を net/http パッケージで提供されている標準の ServeMux に直接登録することができます。
- ミドルウェア関数を互いに入れ子にすることで、任意の長いハンドラチェーンを作ることができます。例えば、以下のようになります。
http.Handle("/", middlewareOne(middlewareTwo(finalHandler)))
制御の流れの説明
ログメッセージを標準出力に書き込むだけのミドルウェアの例を見てみましょう。
main.go
package main import ( "log" "net/http" ) func middlewareOne(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Executing middlewareOne") next.ServeHTTP(w, r) log.Println("Executing middlewareOne again") }) } func middlewareTwo(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Println("Executing middlewareTwo") if r.URL.Path != "/" { return } next.ServeHTTP(w, r) log.Println("Executing middlewareTwo again") }) } func final(w http.ResponseWriter, r *http.Request) { log.Println("Executing finalHandler") w.Write([]byte("OK")) } func main() { finalHandler := http.HandlerFunc(final) http.Handle("/", middlewareOne(middlewareTwo(finalHandler))) http.ListenAndServe(":3000", nil) }
このアプリケーションを実行して http://localhost:3000 にリクエストします。以下のようなログ出力が得られるはずです。
$ go run main.go 2014/10/13 20:27:36 Executing middlewareOne 2014/10/13 20:27:36 Executing middlewareTwo 2014/10/13 20:27:36 Executing finalHandler 2014/10/13 20:27:36 Executing middlewareTwo again 2014/10/13 20:27:36 Executing middlewareOne again
制御がどのようにして、ネストした順序のハンドラに渡され、また逆順に戻ってくるのかよくわかります。
ミドルウェアハンドラから return を発行することで、チェーンを介して伝播する制御を任意の時点で停止することができます。
上の例では、middlewareTwo 関数の中に条件付きの return を入れています。http://localhost:3000/foo にアクセスして、もう一度ログをチェックしてみてください。今回のリクエストは、チェーンを遡る前に middlewareTwo を超えていないことがわかるでしょう。
基本を理解しました。適切な例はどのようなものでしょうか?
さて、XML ボディを含むリクエストを処理するサービスを構築しているとしましょう。a) リクエストボディの存在をチェックし、b) ボディが XML であることを確認するためにスニッフィングを行う ミドルウェアを作成したいとします。これらのチェックのいずれかが失敗した場合、ミドルウェアがエラーメッセージを書き込み、アプリケーションハンドラにリクエストが到達しないようにしたいと考えています。
main.go
package main import ( "bytes" "net/http" ) func enforceXMLHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Check for a request body if r.ContentLength == 0 { http.Error(w, http.StatusText(400), 400) return } // Check its MIME type buf := new(bytes.Buffer) buf.ReadFrom(r.Body) if http.DetectContentType(buf.Bytes()) != "text/xml; charset=utf-8" { http.Error(w, http.StatusText(415), 415) return } next.ServeHTTP(w, r) }) } func main() { finalHandler := http.HandlerFunc(final) http.Handle("/", enforceXMLHandler(finalHandler)) http.ListenAndServe(":3000", nil) } func final(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
これは良さそうですね。簡単な XML ファイルを作成してテストしてみましょう。
$ cat > books.xml <?xml version="1.0"?> <books> <book> <author>H. G. Wells</author> <title>The Time Machine</title> <price>8.50</price> </book> </books>
$ curl -i localhost:3000 HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 Content-Length: 12 Bad Request $ curl -i -d "This is not XML" localhost:3000 HTTP/1.1 415 Unsupported Media Type Content-Type: text/plain; charset=utf-8 Content-Length: 23 Unsupported Media Type $ curl -i -d @books.xml localhost:3000 HTTP/1.1 200 OK Date: Fri, 17 Oct 2014 13:42:10 GMT Content-Length: 2 Content-Type: text/plain; charset=utf-8 OK
サードパーティ製のミドルウェアの使い方
独自のミドルウェアを常に展開するよりも、サードパーティのパッケージを使う方がいいかもしれません。ここでは、goji/httpauth と Gorilla's LoggingHandler を見てみましょう。
goji/httpauth パッケージは HTTP Basic 認証機能を提供します。これには SimpleBasicAuth ヘルパーがあり、func(http.Handler) http.Handler というシグネチャを持つ関数を返します。これは、独自に構築されたミドルウェアと全く同じ方法で使用できることを意味します。
$ go get github.com/goji/httpauth
main.go
package main import ( "github.com/goji/httpauth" "net/http" ) func main() { finalHandler := http.HandlerFunc(final) authHandler := httpauth.SimpleBasicAuth("username", "password") http.Handle("/", authHandler(finalHandler)) http.ListenAndServe(":3000", nil) } func final(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
この例を実行すると、有効なクレデンシャルと無効なクレデンシャルについて期待されるレスポンスが得られるはずです。
$ curl -i username:password@localhost:3000 HTTP/1.1 200 OK Content-Length: 2 Content-Type: text/plain; charset=utf-8 OK $ curl -i username:wrongpassword@localhost:3000 HTTP/1.1 401 Unauthorized Content-Type: text/plain; charset=utf-8 Www-Authenticate: Basic realm=""Restricted"" Content-Length: 13 Unauthorized
Gorilla の LoggingHandler - Apache-style logs を記録 - は通常のミドルウェアのハンドラとは少し違います。
シグネチャ func(out io.Writer, h http.Handler) http.Handler を使用しているので、次のハンドラだけでなく、ログが書き込まれる io.Writer も取得します。
ここでは、server.log ファイルにログを書き込む簡単な例を示します。
go get github.com/gorilla/handlers
main.go
package main import ( "github.com/gorilla/handlers" "net/http" "os" ) func main() { finalHandler := http.HandlerFunc(final) logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } http.Handle("/", handlers.LoggingHandler(logFile, finalHandler)) http.ListenAndServe(":3000", nil) } func final(w http.ResponseWriter, r *http.Request) { w.Write([]byte("OK")) }
このような些細なケースでは、コードはかなり明確です。しかし、より大きなミドルウェアチェーンの一部として LoggingHandler を使用したい場合はどうなるでしょうか?次のような宣言になってしまうかもしれません。
http.Handle("/", handlers.LoggingHandler(logFile, authHandler(enforceXMLHandler(finalHandler))))
... 脳が痛くなります!
これを明確にする一つの方法は、コンストラクタ関数 (ここでは myLoggingHandler と呼びましょう) を func(http.Handler) http.Handler というシグネチャで作成することです。これにより、他のミドルウェアと、よりきちんと入れ子にすることができます。
func myLoggingHandler(h http.Handler) http.Handler { logFile, err := os.OpenFile("server.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } return handlers.LoggingHandler(logFile, h) } func main() { finalHandler := http.HandlerFunc(final) http.Handle("/", myLoggingHandler(finalHandler)) http.ListenAndServe(":3000", nil) }
このアプリケーションを実行していくつかのリクエストを行うと、server.log ファイルは以下のようになるはずです。
$ cat server.log 127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "GET / HTTP/1.1" 200 2 127.0.0.1 - - [21/Oct/2014:18:56:36 +0100] "POST / HTTP/1.1" 200 2 127.0.0.1 - - [21/Oct/2014:18:56:43 +0100] "PUT / HTTP/1.1" 200 2
興味のある方のために、この記事の 3 つのミドルウェアハンドラを 1 つの例にまとめてみました。
余談ですが、Gorilla LoggingHandler はレスポンスのステータス (200) とレスポンスの長さ (2) をログに記録していることに注目してください。これは興味深いことです。上流のロギングミドルウェアはどのようにしてアプリケーションハンドラによって書かれたレスポンスボディを知ることができたのでしょうか?
これは、http.ResponseWriter をラップした独自の responseLogger 型を定義し、カスタムの responseLogger.Write() メソッドと responseLogger.WriteHeader() メソッドを作成することで行います。これらのメソッドはレスポンスを書き込むだけでなく、後で調べるためにサイズや状態を保存します。Gorilla の LoggingHandler は、通常の http.ResponseWriter の代わりに、チェーンの次のハンドラに responseLogger を渡します。
その他のツール
Justinas Stankevičius による Alice は巧妙で非常に軽量なパッケージで、 ミドルウェアハンドラをチェーンさせるためのシンタックスシュガーを提供します。最も基本的なところでは、Alice を使って以下のように書き換えることができます。
http.Handle("/", myLoggingHandler(authHandler(enforceXMLHandler(finalHandler))))
上記は以下のようになります。
http.Handle("/", alice.New(myLoggingHandler, authHandler, enforceXMLHandler).Then(finalHandler))
少なくとも私の目には、Alice のコードの方が一目で理解できるほど、若干わかりやすいです。しかし、Alice の本当の利点は、ハンドラチェーンを一度指定して、それを複数のルートで再利用できることです。こんな感じで。
stdChain := alice.New(myLoggingHandler, authHandler, enforceXMLHandler) http.Handle("/foo", stdChain.Then(fooHandler)) http.Handle("/bar", stdChain.Then(barHandler))
このブログ記事を楽しんでいただけたなら、Goを使ったプロフェッショナルなWebアプリケーションの構築方法についての私の新刊をチェックするのをお忘れなく!
ツイッターの @ajmedwards をフォローしてください。