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

何爲http.ServeMux?

http.ServeMux是什麼?官方定義爲http服務的多路複用器。能夠讓開發在http服務器中自定義不一樣的path路由和對應的處理函數,咱們簡單舉個例子:html

package main 

import (
    "net/http"
    "fmt"
)

func HandleABCFunc(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s %s\n", 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源碼golang

// 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來處理本身的業務邏輯,那就須要修改上述例子:bash

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

它有什麼坑?

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

首先,把代碼保存到server.go,直接使用go命令跑起來。less

go run server.go
複製代碼

而後,咱們再編寫一個客戶端client.go,打印服務端的返回body體信息;接着與server.go同樣,直接使用go命令跑起來。curl

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?難道我命中坑位?this

似是而非

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

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?只好翻翻官網資料。spa

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協議規範程度不一樣,而產生意外的結果。

與其說此爲坑,倒不如說這是不熟悉協議的程序猿編出來的「坑」吧。

相關文章
相關標籤/搜索