如何让 Go HTTP 客户端不会自动跟随重定向?

我目前正在用 Go 编写一些与 REST API 交互的软件。我试图查询的 REST API 端点返回一个 HTTP 302重定向和一个 HTTP Location 头,指向一个资源 URI。

我正在尝试使用 Go 脚本获取 HTTP Location 标头,以便以后处理。

下面是我目前为实现这一功能所做的工作:

package main


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


var BASE_URL = "https://api.example.com/v1"
var STORMPATH_API_KEY_ID = "xxx"
var STORMPATH_API_KEY_SECRET = "xxx"


func noRedirect(req *http.Request, via []*http.Request) error {
return errors.New("Don't redirect!")
}


func main() {


client := &http.Client{
CheckRedirect: noRedirect
}
req, err := http.NewRequest("GET", BASE_URL+"/tenants/current", nil)
req.SetBasicAuth(EXAMPLE_API_KEY_ID, EXAMPLE_API_KEY_SECRET)


resp, err := client.Do(req)


// If we get here, it means one of two things: either this http request
// actually failed, or we got an http redirect response, and should process it.
if err != nil {
if resp.StatusCode == 302 {
fmt.Println("got redirect")
} else {
panic("HTTP request failed.")
}
}
defer resp.Body.Close()


}

我觉得这有点像黑客。通过重写 http.ClientCheckRedirect函数,我实际上被迫把 HTTP 重定向当作错误(它们不是错误)来对待。

我看到其他一些地方建议使用 HTTP 传输而不是 HTTP 客户机——但是我不确定如何使这个工作,因为我需要 HTTP 客户机,因为我需要使用 HTTP 基本认证来与这个 REST API 通信。

你们有谁能告诉我一种使用基本身份验证(Basic Authentication)发出 HTTP 请求的方法——同时不遵循重定向——这种方法不涉及抛出错误和错误处理?

63030 次浏览

It is possible, but the solution inverts the problem a little. Here's a sample written up as a golang test.

package redirects


import (
"github.com/codegangsta/martini-contrib/auth"
"github.com/go-martini/martini"
"net/http"
"net/http/httptest"
"testing"
)


func TestBasicAuthRedirect(t *testing.T) {
// Start a test server
server := setupBasicAuthServer()
defer server.Close()


// Set up the HTTP request
req, err := http.NewRequest("GET", server.URL+"/redirect", nil)
req.SetBasicAuth("username", "password")
if err != nil {
t.Fatal(err)
}


transport := http.Transport{}
resp, err := transport.RoundTrip(req)
if err != nil {
t.Fatal(err)
}
// Check if you received the status codes you expect. There may
// status codes other than 200 which are acceptable.
if resp.StatusCode != 200 && resp.StatusCode != 302 {
t.Fatal("Failed with status", resp.Status)
}


t.Log(resp.Header.Get("Location"))
}




// Create an HTTP server that protects a URL using Basic Auth
func setupBasicAuthServer() *httptest.Server {
m := martini.Classic()
m.Use(auth.Basic("username", "password"))
m.Get("/ping", func() string { return "pong" })
m.Get("/redirect", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/ping", 302)
})
server := httptest.NewServer(m)
return server
}

You should be able to put the above code into it's own package called "redirects" and run it after fetching the required dependencies using

mkdir redirects
cd redirects
# Add the above code to a file with an _test.go suffix
go get github.com/codegangsta/martini-contrib/auth
go get github.com/go-martini/martini
go test -v

Hope this helps!

To make request with Basic Auth that does not follow redirect use RoundTrip function that accepts *Request

This code

package main


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


func main() {
var DefaultTransport http.RoundTripper = &http.Transport{}


req, _ := http.NewRequest("GET", "http://httpbin.org/headers", nil)
req.SetBasicAuth("user", "password")


resp, _ := DefaultTransport.RoundTrip(req)
defer resp.Body.Close()
contents, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Printf("%s", err)
os.Exit(1)
}
fmt.Printf("%s\n", string(contents))
}

outputs

{
"headers": {
"Accept-Encoding": "gzip",
"Authorization": "Basic dXNlcjpwYXNzd29yZA==",
"Connection": "close",
"Host": "httpbin.org",
"User-Agent": "Go 1.1 package http",
"X-Request-Id": "45b512f1-22e9-4e49-8acb-2f017e0a4e35"
}
}

Another option, using the client itself, without the RoundTrip:

// create a custom error to know if a redirect happened
var RedirectAttemptedError = errors.New("redirect")


client := &http.Client{}
// return the error, so client won't attempt redirects
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
return RedirectAttemptedError
}
// Work with the client...
resp, err := client.Head(urlToAccess)


// test if we got the custom error
if urlError, ok := err.(*url.Error); ok && urlError.Err == RedirectAttemptedError{
err = nil
}

UPDATE: this solution is for go < 1.7

There's a much simpler solution right now:

client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}

This way, the http package automatically knows: "Ah, I shouldn't follow any redirects", but does not throw any error. From the comment in the source code:

As a special case, if CheckRedirect returns ErrUseLastResponse, then the most recent response is returned with its body unclosed, along with a nil error.

As an addition of top rated answer,

You can control the particle size

func myCheckRedirect(req *http.Request, via []*http.Request, times int) error {
err := fmt.Errorf("redirect policy: stopped after %d times", times)
if len(via) >= times {
return err
}
return nil
}


...


client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return myCheckRedirect(req, via, 1)
},
}

ref: https://golangbyexample.com/http-no-redirect-client-golang/