Be wary of http/client.go
Recently, I found out an interesting problem in Go. The problem can be reduced to a simple client request to a HTTP server.
Suppose we have a HTTP server, which serves only one rooted path /foo/.
package main
import (
"io"
"log"
"net/http"
"net/http/httputil"
)
func handleFoo(w http.ResponseWriter, req *http.Request) {
// request details
dump, _ := httputil.DumpRequest(req, true)
log.Println(string(dump))
if auth := req.Header.Get("Authorization"); auth != "Bearer GoodToken" {
http.Error(w, "401 Unauthorized", http.StatusUnauthorized)
return
}
io.WriteString(w, "Hello World!")
}
func main() {
http.HandleFunc("/foo/", handleFoo)
log.Fatal(http.ListenAndServe(":12345", nil))
}
handleFoo simplily verifies that correct token is sent in the Authorization header. Otherwise, it returns 401 Unauthorized.
The client sends request with correct token to the server.
package main
import (
"io"
"log"
"net/http"
"os"
)
func main() {
req, _ := http.NewRequest(http.MethodGet, "http://localhost:12345/foo", nil)
req.Header.Set("Authorization", "Bearer GoodToken")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if _, err := io.Copy(os.Stdout, resp.Body); err != nil {
log.Fatal(err)
}
}
What do you think the response is? 401 Unauthorized or Hello World!?
The answer is it depends. It depends on the version of Go that the client code is running. If the client code is running on Go <1.8.0, the response is 401 Unauthorized. Otherwise, it's Hello World!.
But why?
It's not trivial to see at first glance. Let me list down what happens step by step.
-
Client sends
GET /foo HTTP/1.1 Host: localhost:12345 Authorization: Bearer GoodToken ... -
Server receives the request.
ServeMuxdetermines that the requested path/foomatch the registered rooted path/foo/.ServeMuxdecides to send redirect (doc). -
Server responds with header
HTTP/1.1 301 Moved Permanently Location: /foo/ ... -
Client receives response and follows redirect by sending another request to server.
-
Server receives the 2nd request and let
handleFoohandle it.
Both 1.8.0 and versions <1.8.0 follow the same steps when processing the reqeust. The difference lies in Step #4.
Prior to 1.8.0, following redirect in Go will NOT copy the original headers even if it's for the same domain. This wasn't changed util this commit. What happened in Go <1.8.0 is that the Authorization header wasn't copied unpon redirect. Therefore, handleFoo returns 401 Unauthorized.
Initially, I was quite surprised by the behaviour. After reading through this issue and HTTP spec, I realized that http/client.go didn't actually do anything wrong because the HTTP spec didn't specify following redirect should copy original headers. It's just that the well known HTTP clients, e.g. curl, httpie, rest-client, etc., have established the convention.
Be wary of http/client.go.