Example: HTTP Client Following Redirects Safely
HTTP redirects are convenient, but they can change the destination, method, request body, and credential boundary of a request.
For simple GET requests, the default behavior is often enough. For authenticated requests, file uploads, or requests to untrusted URLs, redirect behavior becomes part of your security and reliability policy.
This example shows how to handle redirects deliberately with http.Client.
Quick Example
A custom CheckRedirect lets you reject redirects that cross your trust boundary:
Use this when the destination must remain within a known host, service, or trust boundary.
If you do not need a custom policy, the default client already follows redirects and stops after 10 consecutive requests.
Redirect Behavior
Go's http.Client handles the standard redirect status codes:
For 307 and 308, Go can resend the request body only when Request.GetBody is available. http.NewRequest automatically provides GetBody for common in-memory body types such as bytes.Buffer, bytes.Reader, and strings.Reader.
For 301, 302, and 303, a request such as POST normally becomes GET, with no request body.
This behavior is part of the http.Client API, not something your CheckRedirect callback implements.
1. Make Request Bodies Replayable
A redirect can require the request body to be sent again.
For example, a file upload using 307 or 308 needs a fresh reader for every resend:
GetBody must return a new, independent stream.
The original file handle is used for the first request and is closed by the underlying transport when that request is finished, including when a redirect occurs. Subsequent redirect requests do not reuse that handle; they call GetBody to obtain fresh readers.
If you create a body but never pass the request to Client.Do, you remain responsible for closing that body yourself.
Seekable Streams Are Not the Same as Replayable Request Bodies
A file is seekable, but that does not automatically make it replayable from the HTTP client's perspective.
GetBody explicitly tells net/http how to create a fresh body:
Do not assume that Seek alone is enough for redirect handling.
Common Cases
For these body types, http.NewRequest automatically sets GetBody:
For a file or another custom stream, you may need to provide it yourself.
For large uploads, also consider whether replaying the entire body after a redirect is acceptable. A redirect can turn one large upload into multiple uploads.
2. Treat CheckRedirect as a Policy Hook
CheckRedirect receives two arguments:
req is the upcoming request.
via contains the requests already made, oldest first.
For example:
If there is no redirect yet, via is empty.
The default policy stops after 10 consecutive requests. Providing your own CheckRedirect replaces that default redirect-count policy, so a custom implementation should enforce its own limit if necessary.
A simple limit:
Stop Without Treating the Redirect as an Error
If you want the redirect response itself rather than following it, use:
For example:
This returns the most recent response without closing its body.
By contrast, returning an ordinary error stops the redirect and the previous response body is closed. The error is wrapped in *url.Error.
CheckRedirect Does Not Own Header Sanitization
net/http applies its built-in redirect header policy while constructing the next request, before CheckRedirect is called.
A custom CheckRedirect does not disable that built-in policy.
However, your callback receives the new request and can still modify it. Therefore, custom redirect logic can deliberately override the request headers that net/http prepared.
The req passed to CheckRedirect is a new request object for the next hop, with its own independent Header map.
3. Define the Redirect Trust Boundary
The most important question is not:
"Should I follow redirects?"
It is:
"Which destinations am I willing to trust?"
For an API client, you may want to remain on the same host:
For a stricter policy, validate the destination against an explicit allowlist instead of trusting arbitrary hostnames.
Do not assume that a redirect to a familiar-looking hostname is safe.
4. Understand How Credentials Are Forwarded
Redirect handling has security consequences when requests contain credentials.
Go's net/http client strips sensitive headers such as Authorization when redirecting to a domain that is neither the original domain nor a subdomain of it.
For example:
Sensitive headers may be retained.
And:
Sensitive headers may also be retained.
But:
Sensitive headers are stripped.
This is intentional. Go's redirect policy permits subdomains, unlike the browser Fetch standard. The Go security documentation explicitly describes this behavior.
That means a subdomain is not automatically a separate credential boundary.
If your application considers:
to be different security domains, enforce that policy yourself.
Reject Cross-Host Redirects
For an API client carrying credentials, a conservative policy is often simpler:
Strip Credentials Explicitly When Appropriate
If your application allows the redirect but does not want credentials forwarded:
There is no need to clone req.Header first: the redirect request already has its own header map.
Cookies Are Different
Cookies deserve separate treatment.
If the client has a CookieJar, cookies can change as redirects are followed. The jar decides which cookies belong to the destination.
Do not treat Cookie as equivalent to a manually supplied Authorization header.
5. Redirects and SSRF
Following redirects makes SSRF defenses harder.
Suppose your application allows:
but the server responds:
A client that blindly follows redirects may reach an internal service.
Hostname allowlists alone can also be insufficient.
Consider:
Every hostname may look legitimate while the final destination violates your actual network policy.
DNS adds another layer.
If a later redirect hop requires another DNS resolution, the resolved address can differ from an earlier resolution. This matters when an attacker can influence DNS or redirect destinations.
For security-sensitive clients, validate every redirect destination against the actual policy you need:
- allowed schemes
- allowed hosts
- allowed ports
- allowed IP ranges
- private and link-local address restrictions
- maximum redirect count
Do not rely on redirect behavior alone as an SSRF defense.
6. Redirect Response Bodies Still Have a Lifecycle
When following a redirect, the intermediate response body is not the final response body.
The client reads and closes the redirect response before continuing.
If CheckRedirect returns an ordinary error, the response associated with that failed redirect is also handled by net/http; the caller receives the previous response and the redirect error rather than an open intermediate body.
If you return:
the most recent response is returned instead, with its body left open for the caller.
This distinction matters when writing custom redirect policies.
7. Put a Timeout Around the Entire Operation
Redirects are part of the HTTP request lifecycle.
A client timeout covers the overall operation, including redirects and reading the response body:
For request-specific deadlines, use a context:
A redirect chain does not reset the request's overall deadline.
If the operation involves a large streaming response, be careful with Client.Timeout: the timeout remains active while the response body is being read.
For more detailed control, configure transport-level timeouts separately, such as:
DialContextTLSHandshakeTimeoutResponseHeaderTimeoutIdleConnTimeout
Use those when you need phase-specific behavior rather than one overall deadline.
8. Distinguish Redirect Success from HTTP Success
Client.Do returning nil error does not mean the server returned a successful HTTP status.
A non-2xx response is not itself a Client.Do error. Redirects may also have been followed successfully before the final response is returned.
After the redirect chain completes, check the final response:
This check does not inspect intermediate 301, 302, or other redirect responses. Those have already been handled by the client.
Always close the final response body:
Then read it using an appropriate size limit rather than assuming the response is small.
9. A Minimal Production Policy
For an authenticated API client, a reasonable starting point is:
Then:
This gives you three independent controls:
- How many redirects are allowed
- Where redirects are allowed to go
- How long the entire operation may run
10. Debugging Redirect Chains
When redirect behavior is surprising, httptrace can help expose what the transport is doing.
For example:
Use tracing to investigate connection reuse, DNS, TLS, and request timing.
It is a diagnostic tool, not a replacement for a redirect policy.
Gotchas & Pitfalls
Non-replayable Request Bodies Cannot Be Reused on 307/308 Redirects
307 and 308 preserve the method and body.
If GetBody is unavailable, the client cannot safely recreate the body for another request.
For non-replayable streams, do not assume a redirect can be followed transparently.
Seekable Does Not Mean Replayable
A stream may support Seek without providing the semantics required by net/http.
If a request may be redirected and the body must be resent, provide GetBody.
A Subdomain Is Not Automatically a Separate Credential Boundary
Go may retain sensitive headers when redirecting from a host to one of its subdomains.
If your security model treats subdomains as separate trust domains, enforce that explicitly.
Custom CheckRedirect Can Change the Prepared Request
net/http applies its built-in redirect header policy before calling CheckRedirect.
Your callback can still modify the upcoming request afterward.
Do not assume the request you receive is immutable.
ErrUseLastResponse Is Different from a Normal Redirect Error
Returning:
returns the latest response with its body open.
Returning another error causes the redirect to stop and the previous response to be returned with its body already closed.
Do Returning nil Error Does Not Mean 2xx
HTTP status handling is your application's responsibility.
Check the final response status explicitly.
Redirects Can Multiply Large Uploads
A 307 or 308 redirect can cause a large request body to be sent again.
For large uploads, redirect policy is also a bandwidth and latency decision.
Redirects Can Cross Security Boundaries
A URL that is safe to request directly may redirect somewhere unsafe.
Treat every redirect destination as another untrusted input.
Production Checklist
Before enabling automatic redirects for a security-sensitive client:
- Set an overall timeout.
- Limit the number of redirects.
- Decide whether cross-host redirects are allowed.
- Decide whether subdomain redirects are within your trust boundary.
- Understand how
Authorizationand cookies are forwarded. - Provide
GetBodywhen a request body must survive307or308. - Treat file and stream ownership explicitly.
- Consider SSRF when destinations are user-controlled.
- Validate every redirect destination when network access is restricted.
- Check the final HTTP status after redirects complete.
- Always close the final response body.
- Use tracing when redirect or connection behavior needs investigation.
Redirects are not just a convenience feature. They are part of the request's trust boundary, lifecycle, and failure model.
For production Go code, make that policy explicit rather than letting a server-controlled Location header decide where your client sends the next request.