net/http
の ResponseWriter へのレスポンスヘッダへの書き込みは、以下のようにWriteしたあとに変更しても影響はないとされている。
Changing the header map after a call to WriteHeader (or Write) has no effect unless the modified headers are trailers.
https://golang.org/src/net/http/server.go#L99
またGoogle App Engineのドキュメントには、BodyからContent-Typeを推定すると記載されている。
Content-Type
If you do not explicitly set this header, the http.ResponseWriter class detects the content type from the start of the response body, and sets the Content-Type header accordingly.
このヘッダーを明示的に設定しない場合は、http.ResponseWriter クラスがレスポンス本文の先頭からコンテンツ タイプを検出し、それに応じて Content-Type ヘッダーが設定されます。
しかし上記の仕様とは異なる結果となることがあり、
実行環境、Content-Typeを設定するタイミングHTTPメソッドによってResponseとして返ってくるContent-Typeが変わる。
ローカルで実行したときの4つの結果はすべて想定通りである。
想定外の挙動は以下の2つ。
- GAE(appdngine.Main)とGAE(http.ListenAndServe)どちらもGETのときはContent-Typeがapplication/jsonにならない。
- GAE(appdngine.Main) | Writeの後 | POST の場合に、Content-Typeがapplication/jsonになっている。
実行環境 | Content-Typeを設定するタイミング | HTTPメソッド | Responseとして返ってくるContent-Type |
---|---|---|---|
ローカル | Writeの前 | GET | application/json |
ローカル | Writeの前 | POST | application/json |
ローカル | Writeの後 | GET | text/plain; charset=utf-8 |
ローカル | Writeの後 | POST | text/plain; charset=utf-8 |
GAE(appdngine.Main) | Writeの前 | GET | text/html; charset=UTF-8 |
GAE(appdngine.Main) | Writeの前 | POST | application/json |
GAE(appdngine.Main) | Writeの後 | GET | text/html; charset=UTF-8 |
GAE(appdngine.Main) | Writeの後 | POST | application/json |
GAE(http.ListenAndServe) | Writeの前 | GET | text/html; charset=UTF-8 |
GAE(http.ListenAndServe) | Writeの前 | POST | application/json |
GAE(http.ListenAndServe) | Writeの後 | GET | text/html; charset=UTF-8 |
GAE(http.ListenAndServe) | Writeの後 | POST | text/plain; charset=utf-8 |
まだコードは読めていないが予想としては、UTF-8とutf-8と大文字、小文字のパターンが存在している。Go標準は小文字なので、大文字のときはGAEが書き換えている可能性がある。
つまりGAEでGETのときはresponseを書き換えられている。GAE(http.ListenAndServe) | Writeの後 | POST のときだけは、Go標準の挙動としてtext/plain; charset=utf-8が返っている。
検証コード
package main
import (
"net/http"
"google.golang.org/appengine"
)
// 正しい実装
func handler1(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"test": "hello"}`))
}
// 正しくない実装
func handler2(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"test": "hello"}`))
w.Header().Set("Content-Type", "application/json")
}
func main() {
http.HandleFunc("/1", handler1)
http.HandleFunc("/2", handler2)
// ローカル
// http.ListenAndServe(":8080", nil)
// GAE(appdngine.Main)
// appengine.Main()
// GAE(http.ListenAndServe)
// http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), nil)
}
まだ実装の調査できてないけどいったんメモです。