skip to content

Does Go's http.Get follow a 302 redirect automatically, and how do you find the final URL?

level: juniorimportance: must knowfreq 48%

answer

  1. http.Get is not curl without -L
  2. hops happen before you see anything
  3. the final status is not the 302
  4. the response remembers its own request
  5. resp.Request.URL is the last hop fetched

basics

~10 s

Go's http.Get follows redirects automatically: http.DefaultClient stops only after ten consecutive hops, so you receive the final response, not the 302. The field resp.Request.URL holds the URL of the last request actually sent.

solid answer

~40 s

Yes — redirect following is on by default. `http.Get` uses `http.DefaultClient`, whose `CheckRedirect` field is nil, and the nil policy follows up to ten consecutive redirects. So `resp.StatusCode` is the status of the last hop (typically 200), and the intermediate 302 and its `Location` header never reach you. To learn where you ended up, read `resp.Request.URL`: `Response.Request` is the request that produced this response, populated for client requests, so after three hops it is the third request, not the one you built. If you need the redirect itself — the raw `Location` value, or the chain — you have to install your own `CheckRedirect`, because the default policy discards those responses for you.

code

go · 10 lines
go
resp, err := http.Get("https://example.com/old")
if err != nil {
	return err
}
defer resp.Body.Close()

// Final status of the last hop, not the 302 that started it.
fmt.Println(resp.StatusCode)
// The URL the client actually ended up requesting.
fmt.Println(resp.Request.URL)

go deeper

for a junior

Remember that redirect following is the default in Go and that you normally see only the last response. Be ready to name resp.Request.URL as the way to find out which URL was really fetched.

for a middle

Explain where the behaviour lives: http.Client applies the policy, http.RoundTripper does not, and a nil CheckRedirect means stop after ten hops. Say why the Location header is absent from what you get back.

for a senior

Show what this costs a tool that crawls URLs it does not control — statuses collapse to the last hop, so canonicalisation and moved-permanently reporting need the chain recorded deliberately rather than inferred from the response.

for a principal

Frame the default as a policy your service inherits silently: every outbound dependency will chase up to ten hops to hosts you never approved unless someone decides otherwise. Own that decision rather than discovering it in an incident.

## The default is on Engineers arriving from `curl` are often surprised here: `curl` follows redirects only when you pass `-L`, while Go's `http.Client` follows them unless you tell it not to. `http.Get(url)` is shorthand for `http.DefaultClient.Get(url)`. `http.Client` has a field: ``` CheckRedirect func(req *http.Request, via []*http.Request) error ``` When that field is nil — which it is on `http.DefaultClient` and on a plain `&http.Client{}` — the client applies its default policy: **stop after ten consecutive requests**. Anything up to ten redirect hops is followed silently. The same is true of `http.Head`, `http.Post` and `client.Do`. Redirect following lives in the `http.Client` layer, not in the transport. ## What you get back Because the intermediate hops are consumed by the client, the `*http.Response` you receive describes the **last** exchange: - `resp.StatusCode` is the final status. A URL that answers 302 and then 200 gives you 200. Beginners frequently write `if resp.StatusCode == 302` and find the branch never taken. - `resp.Header.Get("Location")` is empty, because the final 200 carries no `Location` header. The 302 that did carry one was read and discarded by the client. - `resp.Body` is the body of the final response only. ## Finding out where you landed `http.Response` has a `Request` field, documented as the request that was sent to obtain this response, populated only for client responses. After redirects, that is the **last** request the client built, so: ``` resp.Request.URL // the URL actually fetched resp.Request.Method // the method used on the last hop ``` This is the idiomatic way to detect that a redirect happened at all: compare `resp.Request.URL.String()` with the URL you asked for. A link checker that reports canonical URLs is doing exactly this — it never sees the 301, it just notices that the request it ended up making is not the request it started with. Note that `resp.Request.Body` is nil; the request body has already been consumed by the time you hold the response. `Response` also has a `Location` method, which resolves a `Location` header relative to the request URL. On a final 200 it returns `http.ErrNoLocation`, because there is no header to resolve — it is useful only on a response you deliberately stopped at. ## Where redirect following does not happen Redirect handling is a `Client` behaviour. An `http.RoundTripper` — `http.Transport`, or your own wrapper around it — performs exactly one HTTP transaction. Calling `http.DefaultTransport.RoundTrip(req)` yourself returns the 302 verbatim, `Location` header and all, and nothing further happens. That is occasionally the simplest way to look at a redirect, at the price of doing everything else (cookies, body draining, error wrapping) by hand. ## Why the ten-hop budget exists A server can send you round a cycle: host A redirects to host B, host B redirects back to host A. Without a limit the client would spin forever. Ten is the arbitrary-but-conventional cap Go picked; a legitimate chain is normally one to three hops. When the budget runs out you get an error rather than a response, which is a distinct situation worth handling in any tool that walks links it did not write. ## Practical shape for a link checker A crawler that wants both the destination and the fact that it moved does not need any configuration for the common case: 1. `resp, err := client.Get(u)` — hops are followed. 2. `final := resp.Request.URL.String()` — where it landed. 3. `moved := final != u` — whether anything redirected. 4. `resp.StatusCode` — the health of the destination. Only when you must report the exact status of the first hop (301 versus 302 versus 308) or the raw, unresolved `Location` value does the default policy stop being enough, and then you reach for `CheckRedirect`.

  • Does calling Transport.RoundTrip directly follow redirects too?
    No. An http.RoundTripper performs exactly one HTTP transaction; redirect following lives in http.Client. If you call http.DefaultTransport.RoundTrip yourself you get the 302 response with its Location header intact and nothing else happens. That is one way to inspect a redirect, but you then own cookie handling, body draining and error wrapping yourself.
  • Does http.Head follow redirects the same way?
    Yes. http.Head goes through http.DefaultClient and the same nil CheckRedirect policy, so you get the headers of the final hop. If you specifically want the first 3xx response, build your own client with a CheckRedirect rather than switching methods — the method has nothing to do with whether hops are followed.
  • How do you tell whether a redirect happened at all?
    Compare resp.Request.URL.String() with the URL you passed in. They differ exactly when the client followed at least one hop. There is no hop counter on the response, so if you need the number or the intermediate statuses you must record them from a CheckRedirect callback.

curl only chases redirects when you add -L; Go's client chases them by default and hands you the destination, keeping the forwarding notes to itself.

saying these in an interview costs you the question

  • Expects resp.StatusCode to be 302 after a redirect
  • Looks for the Location header on the final response
  • Thinks Go needs an option to follow redirects, like curl -L
  • Believes http.Transport.RoundTrip follows redirects
  • Assumes resp.Request is the request the caller built