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协议规范程度不一样,而产生意外的结果。

与其说此为坑,倒不如说这是不熟悉协议的程序猿编出来的“坑”吧。

相关文章
相关标签/搜索