emahiro/b.log

日々の勉強の記録とか育児の記録とか。

ビルドタグで appengine.Main() の向き先が変わる話

Overview

AppEngine の Go1.11 対応において gin の version を最新の 1.3 系にあげたときに以下の問題にはまったのでその調査メモです。

  • gin の version を現時点(201904)での最新の 1.3 にあげ、appengine.Main() を使うコードに変更すると、AppEngine / Go1.9 で動いていた c.Request.Header.Get("Host") が空文字で返ってきてしまい、既存コードに影響が出てしまったこと。

原因

  • gin1.3 以降の appengine.Main() では internal.Main() が呼ばれる。appengine.Main() の中身は appengineかどうかのビルドタグで制御されていて、Go1.9 までのビルドと Go1.11 からのビルドでは appengine.Main() の実行される先が異なる。

  • gin 1.3 系(Go1.11)

  • gin 1.1系(~Go1.9)

挙動の違いを調査するために Go1.9 までで動作していた appengine.Main() の定義元の appengine_internal.Main() の中身を探します。

appengine_internal パッケージについて。

appengine_internal パッケージは Go1.11 で対応した gin の appengine.Main() では呼ばれません。

探し方

goapp env GOROOT
/PATHTO/google-cloud-sdk/platform/google_appengine/goroot-1.9 

このように appengine の goroot が出力されるので、このディレクトリ配下の src を探します。
※ goapp の go の version は 1.9

まず見たのは以下 - /PATHTO/google-cloud-sdk/platform/google_appengine/goroot-1.9/src/appengine_internal/internal.go

ここの Main 関数を確認します。

func Main() {
    close(appPackagesInitialized)
    flag.Parse()
    serveHTTP()
}

serveHTTP() メソッドを確認します。

// serveHTTP serves App Engine HTTP requests.
func serveHTTP() {
    // The development server reads the HTTP address and port that the
    // server is listening to from stdout. We listen on 127.0.0.1:0 or
    // [::1]:0 to avoid firewall restrictions.
    conn, err := net.Listen("tcp", "127.0.0.1:0")
    if err != nil {
        log.Print("appengine: couldn't listen on IPv4 TCP socket: ", err)
        conn, err = net.Listen("tcp", "[::1]:0")
        if err != nil {
            log.Fatal("appengine: couldn't listen on IPv6 TCP socket: ", err)
        }
    }

    addr := conn.Addr().(*net.TCPAddr)

    fmt.Fprintf(os.Stdout, "%s\t%d\n", addr.IP, addr.Port)
    os.Stdout.Close()

    err = http.Serve(conn, http.HandlerFunc(handleFilteredHTTP))
    if err != nil {
        log.Fatal("appengine: ", err)
    }
}

この err = http.Serve(conn, http.HandlerFunc(handleFilteredHTTP)) に着目します。

func handleFilteredHTTP(w http.ResponseWriter, r *http.Request) {
    // Patch up RemoteAddr so it looks reasonable.
    if addr := r.Header.Get("X-Appengine-Remote-Addr"); addr != "" {
        r.RemoteAddr = addr
    } else {
        // Should not normally reach here, but pick
        // a sensible default anyway.
        r.RemoteAddr = "127.0.0.1"
    }

    // Create a private copy of the Request that includes headers that are
    // private to the runtime and strip those headers from the request that the
    // user application sees.
    creq := *r
    r.Header = make(http.Header)
    for name, values := range creq.Header {
        if !strings.HasPrefix(name, "X-Appengine-Dev-") {
            r.Header[name] = values
        }
    }
    ctx := &httpContext{req: &creq, done: make(chan struct{})}
    r = registerContext(r, ctx)

    http.DefaultServeMux.ServeHTTP(w, r)
    close(ctx.done)

    unregisterContext(r)
}

以下の部分を抜粋します。

r.Header = make(http.Header)
for name, values := range creq.Header {
    if !strings.HasPrefix(name, "X-Appengine-Dev-") {
        r.Header[name] = values
    }
}
  • 1.9以下でビルドされ、call されていた appengine_intenal.Main() では Header に関して X-Appengine-Dev-XXXX と言う文字列を持つヘッダー以外を Request Header に入れ直していました。

念の為、go1.11 以降で使用される appengine.Main() の実装について調べてみます。

エントリの冒頭でも記載してますが、 https://github.com/golang/appengine/blob/master/internal/main_vm.go#L19 を確認します。

func Main() {
    MainPath = filepath.Dir(findMainPath())
    installHealthChecker(http.DefaultServeMux)

    port := "8080"
    if s := os.Getenv("PORT"); s != "" {
        port = s
    }

    host := ""
    if IsDevAppServer() {
        host = "127.0.0.1"
    }
    if err := http.ListenAndServe(host+":"+port, http.HandlerFunc(handleHTTP)); err != nil {
        log.Fatalf("http.ListenAndServe: %v", err)
    }
}

この実装の中の handleHTTP の実装の中を見ます。 (ref:https://github.com/golang/appengine/blob/master/internal/api.go#L87-L152 )

※ 長いので header に値を set してる箇所のみ抜粋。
(ref: https://github.com/golang/appengine/blob/master/internal/api.go#L140 )

w.Header().Set(logFlushHeader, strconv.Itoa(flushes))

go1.11 以降で call されてる appengine.Main() の実装の中身を確認すると go1.9 までとは実装が異なっていました。

1.9以下でビルドされ、call されていた appengine_intenal.Main() では Header に関して X-Appengine-Dev-XXXX と言う文字列を持つヘッダー以外を Request Header に入れ直していました。

go1.11 以降では、Header から 取り出して Header に入れ直す処理を通っていないので c.Request.Header.Get("XXXX") で取り出せないものが発生していました。

まとめ

1.11 対応では call される appengine.Main() の中身が違うので、既存のコードを見直してみると良いかもしれません。

ビルドタグについて

qiita.com

追記

godoc の Request の説明を見ると

For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.

ref: https://golang.org/pkg/net/http/#Request

と記載されていた。最新版の go のドキュメントによるとそもそも Request.Header の中に Host field は含まないことになったらしい。