Go排坑:http.ServeMux意外重定向的問題分析

little-cui發表於2019-03-02

何為http.ServeMux?

http.ServeMux是什麼?官方定義為http服務的多路複用器。可以讓開發在http伺服器中自定義不同的path路由和對應的處理函式,我們簡單舉個例子:

package main 

import (
    "net/http"
    "fmt"
)

func HandleABCFunc(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s %s
", r.Method, r.URL)
} 

func main() { 
    http.HandleFunc("/abc/", HandleABCFunc) 
    http.ListenAndServe(":8080", nil) 
}
複製程式碼

Where is the http.ServeMux? Are you kidding me? 別急,我們開啟看看http.HandleFunc原始碼

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
...
// HandleFunc registers the handler function for the given pattern
// in the DefaultServeMux.
// The documentation for ServeMux explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}
複製程式碼

原來go為了減少開發的重複性,簡單的封裝了一個預設的http.ServeMux,也就是說通過http.HandleFunc註冊的處理函式,統一由預設的http.ServeMux來解析和呼叫,如果你想定製http.ServeMux來處理自己的業務邏輯,那就需要修改上述例子:

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/abc/", HandleABCFunc)
	// ...
	http.ListenAndServe(":8080", mux)
}
複製程式碼

它有什麼坑?

OK!既然知道什麼是http.ServeMux,我這裡說下最近使用它遇到的一個問題,我們以上面/abc/為例子描述這個問題。

首先,把程式碼儲存到server.go,直接使用go命令跑起來。

go run server.go
複製程式碼

然後,我們再編寫一個客戶端client.go,列印服務端的返回body體資訊;接著與server.go一樣,直接使用go命令跑起來。

package main

import (
	"net/http"
	"io/ioutil"
	"fmt"
)

func main() {
	resp, _ := http.Post("http://127.0.0.1:8080/abc", "", nil)
	if resp != nil {
		body, _ := ioutil.ReadAll(resp.Body)
		fmt.Println(string(body))
		resp.Body.Close()
	}
}
複製程式碼

結果!我驚訝了!

go run client.go 
# GET /abc/
複製程式碼

我明明是POST請求,怎麼服務端收到的是GET?難道我命中坑位?

似是而非

習慣性地,我用cURL請求除錯一把服務端,發現了些端倪

curl -vL -XPOST http://127.0.0.1:8080/abc
*   Trying 127.0.0.1...
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /abc HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.47.0
> Accept: */*
> 
< HTTP/1.1 301 Moved Permanently
< Location: /abc/
< Date: Mon, 12 Nov 2018 02:49:30 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
< 
* Connection #0 to host 127.0.0.1 left intact
* Issue another request to this URL: `http://127.0.0.1:8080/abc/`
* Found bundle for host 127.0.0.1: 0x55b8d54ce0c0 [can pipeline]
* Re-using existing connection! (#0) with host 127.0.0.1
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> POST /abc/ HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.47.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Date: Mon, 12 Nov 2018 02:49:30 GMT
< Content-Length: 11
< Content-Type: text/plain; charset=utf-8
< 
POST /abc/
* Connection #0 to host 127.0.0.1 left intact
複製程式碼

如果直接看結果,cURL是預期的結果,還真以為是go的BUG,但仔細看了下請求過程,發現中間多了一次重定向請求,這就有點奇怪了?為什麼go服務端會返回301 Moved Permanently?只好翻翻官網資料。

https://golang.org/pkg/net/http/#ServeMux

If a subtree has been registered and a request is received naming the subtree root without its
trailing slash, ServeMux redirects that request to the subtree root (adding the trailing slash).
This behavior can be overridden with a separate registration for the path without the trailing
slash. For example, registering "/images/" causes ServeMux to redirect a request for "/images"
to "/images/", unless "/images" has been registered separately.
複製程式碼

對比上述例子,我請求的是/abc會被重定向為/abc/,處理方式就是返回客戶端讓其自己重定向請求到/abc/,聽起來很合理,但go客戶端為什麼修改了我的請求method呢?難道是go標準庫http.Client的BUG?再找找資料。

https://tools.ietf.org/html/rfc7231#section-6.4.2

Note: For historical reasons, a user agent MAY change the request
      method from POST to GET for the subsequent request.  If this
      behavior is undesired, the 307 (Temporary Redirect) status code
      can be used instead.
複製程式碼

真想大白!RFC7231中對301 Moved Permanently有一段額外說明,就是歷史原因,客戶端可能會將POST請求重定向為GET,為此如果真想不修改方法進行重定向,在HTTTP/1.1裡面新定義了307 Temporary Redirect來實現。

回過頭來看,cURL能執行正確,也說明了不同的客戶端實現,會導致不同的效果。

總結

使用http.ServeMux註冊路由時需要注意,資源是否包含下層資源,如果不包含就不要以/結尾;防止客戶端遵循HTTP協議規範程度不同,而產生意外的結果。

與其說此為坑,倒不如說這是不熟悉協議的程式猿編出來的“坑”吧。

相關文章