How does http.Client.CheckRedirect work, and what does returning http.ErrUseLastResponse do?
answer
- one hook on the client decides
- the callback sees the hops already made
- nil means go, not stop
- one sentinel means stop without an error
- ErrUseLastResponse leaves the body open
basics
~10 shttp.Client.CheckRedirect is a func(req, via) error the client calls before each redirect hop; nil means stop after ten. Returning http.ErrUseLastResponse makes Do return the latest response with its body unclosed and a nil error.
solid answer
~40 s`CheckRedirect` is a field on `http.Client` with the signature `func(req *http.Request, via []*http.Request) error`. It is called before every redirect hop: `req` is the request the client is about to send, and `via` holds the requests already made, oldest first, so `len(via)` is the hop count. Return nil and the hop proceeds. Return `http.ErrUseLastResponse` and `Do` hands you the most recent response — the 3xx itself — with a **nil error** and an **unclosed body**, which is how you read a raw `Location` header. Return any other error and `Do` returns the previous response with its body already **closed**, plus your error wrapped in a `*url.Error`. When the field is nil the client uses its built-in policy of stopping after ten consecutive redirects. It is per client, not per request.
code
go · 14 linesclient := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get("https://example.com/old")
if err != nil {
return err
}
defer resp.Body.Close()
// err is nil even though this is a 3xx.
location := resp.Header.Get("Location")go deeper
Know that a custom client can refuse redirects and that the way to do it is the CheckRedirect field. Be able to write the four-line client that returns http.ErrUseLastResponse and reads the Location header.
Explain the signature and all three return meanings, including which one leaves the body open and which one closes it. Say what via contains and that the policy belongs to the client, not the request.
Show judgment about what belongs in the callback: a host allowlist, a lower cap, chain logging. Be ready to say why the callback must be concurrency-safe and why a nil error on a 3xx surprises callers.
Treat the redirect policy as part of an outbound client's contract, alongside timeouts: decide once whether clients carrying credentials may change host at all, and make that the shared default rather than per-team folklore.
## The hook `http.Client` exposes exactly one lever over redirects: ``` type Client struct { Transport RoundTripper CheckRedirect func(req *Request, via []*Request) error Jar CookieJar Timeout time.Duration } ``` The client calls `CheckRedirect` **before** issuing each redirected request — never for the first request. The arguments are: - `req` — the request the client is *about* to send. Its `URL` is the resolved target, its `Header` is what the client has decided to forward. Mutating it here is how you add or remove a header for the next hop only. - `via` — the requests already made, **oldest first**. `via[0]` is the original request; `len(via)` is the number of hops so far. Note it is a slice of *requests*, not responses: you do not get the intermediate status codes here. ## Three possible returns, three different outcomes **Return nil.** The hop is made. This is the permissive case, and it is what you write when you only want to observe or to enforce a cap of your own. **Return `http.ErrUseLastResponse`.** This sentinel is special-cased. `Do` stops and returns the most recent response — the 3xx — with a **nil error** and, uniquely, with the body **not closed**. You now own that body and must close it. This is the supported way to say do not follow, let me look: ``` resp.StatusCode // 301, 302, 307... resp.Header.Get("Location") // the raw, unresolved header value ``` Because the error is nil, code that only checks `err != nil` will sail past and treat the 3xx as a normal response — a real bug in tools written by someone who expected an error. **Return any other error.** `Do` returns the previous response *and* an error. The response body has already been closed by the client, so you can read status and headers but not content. Your error is wrapped in a `*url.Error`, so callers should use `errors.As` to unwrap it rather than string-matching. This is the shape for a hard policy violation: refusing to leave the original host, refusing a downgrade from https to http, refusing a target that resolves to a private address. **Leave it nil.** The built-in policy applies: stop after ten consecutive redirects, with an error saying so — itself delivered as a `*url.Error`. ## It is a client-level policy There is no per-request redirect setting in `net/http`. `CheckRedirect` lives on the client, so a service that needs both behaviours keeps two clients, or branches inside one callback on something it can see — `req.URL`, or a marker header the caller set on the original request and that the callback can read from `via[0].Header`. The callback must also be safe for concurrent use. One `http.Client` is designed to be shared across goroutines, so the closure runs on many goroutines at once; do not have it write to a shared map without a lock. ## Typical uses **Capture the redirect.** Return `http.ErrUseLastResponse` and inspect the 3xx. A crawler that records where each link claims to point does this rather than following, because it wants the declared target, not the eventual page. **Cap the chain lower.** Ten is generous. A checker that treats more than three hops as a smell returns an error at `len(via) >= 3`, which turns an expensive chase into a fast, reportable result. **Refuse a host change.** Compare `req.URL.Host` against `via[0].URL.Host` and error out when they differ. This keeps a client that carries credentials from wandering to a host you never intended to talk to — a decision worth making explicitly for any client that sends a token. **Log the chain.** Return nil, but append `req.URL` to a slice or log `len(via)` and the target first. Since the callback sees every hop, this is the only place the full chain is visible. ## Common mistakes - Assuming nil means *stop*. Nil means *proceed*; the sentinel means stop. - Reading `resp.Body` after returning an ordinary error — it is closed. - Forgetting to close the body after `ErrUseLastResponse` — the client did not. - Checking `err != nil` to detect that a redirect was refused via `ErrUseLastResponse`: the error is deliberately nil, so branch on `resp.StatusCode` instead. - Trying to set the policy on `http.Transport`. It is not there; the transport does one round trip and knows nothing about redirects.
- You need one client that follows redirects and one that does not. Can you set that per request?No. CheckRedirect is a field on http.Client, not on http.Request, so the policy is per client. Either keep two clients, or branch inside a single callback on something visible to it, such as req.URL or a marker header the caller set on the original request and that you can read back from via[0].Header.
- What exactly does Do return when CheckRedirect returns an ordinary error?Both the previous response and an error. The client closes that response's body first, so you can still read its status and headers but not its content. Your error is wrapped in a *url.Error, so unwrap with errors.As rather than comparing strings, and remember the sentinel http.ErrUseLastResponse is the one return that does not take this path.
- After returning http.ErrUseLastResponse, who closes the response body?You do. That is the single path where the client hands back a non-final response with an unclosed body, precisely so you can read it. Treat it like any other response: defer resp.Body.Close(). Forgetting is a leak, and it is easy to forget because the code reads as if no request really completed.
saying these in an interview costs you the question
- Thinks returning nil from CheckRedirect blocks the redirect
- Believes CheckRedirect can be set per request
- Expects to read the 3xx body after returning a plain error
- Expects Do to return an error for ErrUseLastResponse
- Looks for a redirect setting on http.Transport