How Can I Make the Go HTTP Client NOT Follow Redirects Automatically?

55,684

Solution 1

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.

Solution 2

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

Solution 3

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!

Solution 4

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"
  }
}
Share:
55,684

Related videos on Youtube

rdegges
Author by

rdegges

I'm just a happy programmer that likes to hack stuff. I spend an inordinate amount of time building and growing developer tools and services. I love web security, cryptography, telephony, and general optimization. HACK THE PLANET!

Updated on July 08, 2022

Comments

  • rdegges
    rdegges almost 2 years

    I'm currently writing some software in Go that interacts with a REST API. The REST API endpoint I'm trying to query returns an HTTP 302 redirect along with an HTTP Location header, pointing to a resource URI.

    I'm trying to use my Go script to grab the HTTP Location header for later processing.

    Here's what I'm currently doing to achieve this functionality:

    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()
    
    }
    

    This feels like a bit of a hack to me. By overriding the http.Client's CheckRedirect function, I'm essentially forced to treat HTTP redirects like errors (which they aren't).

    I've seen several other places suggesting to use an HTTP transport instead of an HTTP client -- but I'm not sure how to make this work since I need the HTTP Client as I need to use HTTP Basic Auth to communicate with this REST API.

    Can any of you tell me a way to make HTTP requests with Basic Authentication -- while not following redirects -- that doesn't involve throwing errors and error handling?

    • Dmitri Goldring
      Dmitri Goldring about 10 years
      Looking at the source it doesn't look like it. The Location header is pulled after the CheckRedirect call and you do not have access to the interim response.
    • rdegges
      rdegges about 10 years
      I believe you're right @DmitriGoldring -- driving me crazy. There MUST be a way to get this going though -- I can't imagine there not being a good way to do this ><
  • Rick-777
    Rick-777 about 10 years
    The status code check is too strict. Instead of if resp.StatusCode != 200 && resp.StatusCode != 302, you should test if resp.StatusCode >= 400 because there are other common ones that should be allowed for, e.g. 204, 303, 307.
  • Sridhar Venkatakrishnan
    Sridhar Venkatakrishnan about 10 years
    You're absolutely right. Thanks for pointing that out! However, I'd prefer to leave that decision up to the programmer since they know the expected behaviour better. I've added a comment to this effect in the code.
  • Rick-777
    Rick-777 about 10 years
    Hmm, maybe. That's good provided they know the difference between, say 302 and 303. Many don't.
  • Mickaël Rémond
    Mickaël Rémond almost 8 years
    It seems this is for Go 1.7 (still in RC currently)
  • Vojtech Vitek
    Vojtech Vitek over 7 years
    Already upvoted a while ago, but I'm here back again. Thanks!