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

101
我正在使用Go编写一些与REST API交互的软件。我试图查询的REST API端点返回一个HTTP 302重定向,以及一个指向资源URI的HTTP Location头。
我正在尝试使用我的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()

}

这对我来说感觉有点像是一个 hack。通过覆盖 http.ClientCheckRedirect 函数,我本质上被迫将 HTTP 重定向视为错误(而它们并不是错误)。
我看到其他一些地方建议使用 HTTP 传输而不是 HTTP 客户端 - 但我不确定如何使其工作,因为我需要 HTTP 客户端以便使用 HTTP 基本身份验证来与此 REST API 通信。
你们中有人能告诉我一种在不跟随重定向的情况下进行基本身份验证的 HTTP 请求的方法吗?这不涉及抛出错误和错误处理。

从源代码(http://golang.org/src/pkg/net/http/client.go)来看,似乎不是这样的。在调用`CheckRedirect`后会提取`Location`标头,您无法访问中间响应。 - Dmitri Goldring
我相信你是对的 @DmitriGoldring -- 这让我发疯了。不过一定有办法让它运行起来 -- 我无法想象没有好的方法来做到这一点 >< - rdegges
5个回答

215

现在有一个更简单的解决方案:

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

通过这种方式,http包自动知道:"啊,我不应该跟随任何重定向",但不会抛出任何错误。源代码的注释如下:

作为一个特例,如果CheckRedirect返回ErrUseLastResponse, 那么最近的响应将以未关闭其正文的形式返回,并带有nil错误。


12

另一种选择是使用客户端本身,而不需要往返通信:

// 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   
}

更新: 此解决方案适用于go < 1.7版本。


8

这是可能的,但解决方案需要略微转换一下思路。以下是一个用golang编写的示例测试。

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
}

您应该能够将上述代码放入自己的包中,名为“redirects”,并在获取所需依赖项后运行它。

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

希望这能帮到你!

状态码检查过于严格。你应该测试if resp.StatusCode >= 400,而不是if resp.StatusCode != 200 && resp.StatusCode != 302,因为还有其他常见的状态码也应该被允许,比如204、303、307等。 - Rick-777
1
你说得完全正确。感谢指出这一点!然而,我更愿意让程序员自己决定,因为他们更了解预期的行为。我已经在代码中添加了相应的注释。 - Sridhar Venkatakrishnan
嗯,也许吧。只要他们知道302和303之间的区别就好了。很多人不知道。 - Rick-777

5
使用基本身份验证进行请求,不跟随重定向,请使用接受*RequestRoundTrip函数。
以下是代码:
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))
}

输出

{
  "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"
  }
}

1
作为最佳答案的补充,
您可以控制粒子大小。
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)
        },
    }

参考资料:https://golangbyexample.com/http-no-redirect-client-golang/

这篇文章介绍了如何在Golang中使用HTTP客户端来禁用重定向。通过设置Transport属性的DisableRedirects属性,可以禁用HTTP客户端的重定向功能。此外,该文章还提供了代码示例,以帮助读者更好地理解如何实现。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接