2

When I make a request with Go's net/http/client package, and the server responds with 301 or 302 status code, but doesn't provide a Location header, we get an error object, and no response object. Example ReplIt session: https://replit.com/@sharat87/RedirectWithoutLocationGo#main.go. Including source here in case that session goes away in the future:

package main

import (
  "log"
    "net/http"
)

func main() {
    client := &http.Client{}
    resp, err := client.Get("https://httpbun.com/status/301")
    if err != nil {
        log.Println("Error getting", err)
    } else {
        log.Println(resp.Status)
    }
}

This prints the error message: Error getting Get "https://httpbun.com/status/301": 301 response missing Location header.

However, the Location header in 301 or 302 status responses is optional. Servers are not required to provide it, and clients are not obliged to redirect to it. In fact, in my case, I need to look at the other headers as well as the response body for what I need. But when the Location header is missing, Go just returns an error and discards the whole response. For example, curl doesn't throw an error in this case, and actually responds with the other headers:

$ curl -v https://httpbun.com/status/301
[... TLS logs redacted]
> GET /status/301 HTTP/1.1
> Host: httpbun.com
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Server: nginx/1.18.0 (Ubuntu)
< Date: Sun, 25 Jul 2021 02:18:53 GMT
< Content-Type: text/plain; charset=utf-8
< Content-Length: 21
< Connection: keep-alive
< X-Powered-By: httpbun
<

I dug into the source for the net/http/client module to try and see what might be happening. The error message comes up from this line in the source. Here, for the redirected request, we first check if a Location header is available, and if not, we are returning an error. This is very limiting especially because the spec doesn't describe this to be an error condition and in fact, lists a legitimate reason for doing this (i.e., the server doesn't know where to redirect to) (reference, links to sources).

So, my question is, is this intended? Am I missing something here? Would a PR to change this be invited? (Asking here because looks like Golang repo issues on GitHub are reserved for proposals?).

I'd love any advise here since I'm essentially blocked here.

Edit: Fixed in https://github.com/golang/go/issues/49281.

sharat87
  • 7,330
  • 12
  • 55
  • 80

2 Answers2

2

Create a transport wrapper that fixes up the response for the client. The following example sets the location header when the header is missing. Another option is to change the status code.

type locFix struct {
    http.RoundTripper
}

func (lf locFix) RoundTrip(req *http.Request) (*http.Response, error) {
    resp, err := lf.RoundTripper.RoundTrip(req)
    if err != nil {
        return resp, err
    }   
    if resp.StatusCode == 301 || resp.StatusCode == 302 {
        if resp.Header.Get("Location") == "" {      
            resp.Header.Set("Location", "http://example.com")
        }
    }
    return resp, err
}

Use a http.Client with the wrapped transport. Here’s how to create a wrapper for the net/http default client:

client := *http.DefaultClient
t := client.Transport
if t == nil {
    t = http.DefaultTransport
}
client.Transport = locFix{t}

resp, err = client.Get("https://httpbun.com/status/301")
Charlie Tumahai
  • 113,709
  • 12
  • 249
  • 242
  • This is actually a good idea. I didn't realize `RoundTripper` was a separate interface. I'll do some more experiments and proceed with this, although, I'd love for this to be fixed in the core package itself. I'll wait for a day and mark this as the answer. Thanks for sharing this – sharat87 Jul 25 '21 at 05:54
1

The default client follows redirects. You can bypass this by using RoundTrip directly:

package main

import (
   "fmt"
   "net/http"
)

func main() {
   req, err := http.NewRequest("GET", "http://httpbun.com/status/301", nil)
   if err != nil {
      panic(err)
   }
   res, err := new(http.Transport).RoundTrip(req)
   if err != nil {
      panic(err)
   }
   defer res.Body.Close()
   fmt.Printf("%+v\n", res)
}
Zombo
  • 1
  • 62
  • 391
  • 407
  • Hey, thanks for this pointer. It looks like if I go this route instead of the higher level client interface, I’ll have to handle a lot of stuff myself, like redirects themselves, cookie management, even timeouts. The Do method is doing all this for me and I need all those as well. So, is there a way I can get those here or will I need to maintain all those features as well? – sharat87 Jul 25 '21 at 03:08
  • Haha, yeah I wish. I’m developing a HTTP testing tool so I need to be able to show as much information as possible for requests. If there’s no location header, I want to be able to show the rest of the information to troubleshoot. ‍♂️ – sharat87 Jul 25 '21 at 03:35
  • Thanks for the effort man. I spent some doing an alternate implementation in Python and the requests library and that works fine. Doesn’t throw an error on missing Location header. But I’d love to use Go though. – sharat87 Jul 25 '21 at 03:40
  • Steven is correct: *The new permanent URI SHOULD be given by the Location field in the response.* (https://datatracker.ietf.org/doc/html/rfc2616#section-10.3.2) – jub0bs Jul 25 '21 at 09:49
  • 2
    @jub0bs, the word `SHOULD` here I doesn’t equal `MUST`. See the page linked to from the URL you shared (https://datatracker.ietf.org/doc/html/rfc2119#section-3). I’m dealing with a use case where I’m making the request as part of a http debugging/troubleshooting tool. I want to show all available headers and response body even if Location header is missing, so I can give the maximum information available for troubleshooting. Also, the RFC you linked is obsolete. Please refer And the other stack overflow post linked in my question. – sharat87 Jul 25 '21 at 10:20
  • @ShrikantSharat Points taken. – jub0bs Jul 25 '21 at 10:24