Quick answer: To handle redirects when scraping, let your HTTP client follow 3xx responses automatically but cap the number of hops (five to ten) to avoid loops, resolve relative Location headers against the current URL, and carry cookies forward so session-based redirects work. Most clients follow redirects by default, but you should decide deliberately whether to follow them, because a redirect can leak you off-site or into an infinite loop.
Redirects are one of those things that work silently until they do not. Then you are staring at a scraper that returns a login page, an empty body, or a "too many redirects" error, and it is not obvious why. This post explains how 3xx status codes actually work and how to handle each case in a scraper.
The 3xx family, code by code
A redirect is a response in the 3xx range with a Location header telling the client where to go next. The differences between the codes matter more than most people think, especially for non-GET requests.
| Code | Name | Method preserved? | Cacheable? | Typical use |
|---|---|---|---|---|
| 301 | Moved Permanently | Often changed to GET | Yes | URL moved for good |
| 302 | Found | Often changed to GET | No | Temporary redirect |
| 303 | See Other | Always switch to GET | No | After a POST, go GET a result |
| 307 | Temporary Redirect | Yes, method kept | No | Temporary, keep POST as POST |
| 308 | Permanent Redirect | Yes, method kept | Yes | Permanent, keep POST as POST |
The historically messy part is method handling. For 301 and 302, many clients and browsers rewrite a POST into a GET when following the redirect, which is technically non-compliant but became a de facto standard. 303 says do this on purpose. 307 and 308 were added precisely to remove the ambiguity: they guarantee the method and body are preserved. When you scrape with GET requests this rarely bites you, but the moment you POST a form and get redirected, the code matters.
Following vs not following
Most HTTP clients follow redirects by default. That is usually what you want for scraping, since a 301 to the canonical URL is exactly the page you were after. But there are cases where you want to see the redirect instead of chasing it:
- Detecting canonical URLs. If you want to record that
/old-pathnow points to/new-path, you need to see the301and itsLocation, not silently land on the destination. - Avoiding off-site jumps. A redirect can send you to a completely different domain. If you only want to stay on the target site, inspect the
Locationbefore following. - Login walls. A
302to/loginis a strong signal you were blocked or logged out, and following it just gives you a login form to parse by mistake.
The practical answer is to follow redirects but keep the final URL and the chain so you can reason about what happened.
The three things that break scrapers
Redirect loops
Site A redirects to B, B redirects back to A, and your client spins until it errors or hangs. Every well-behaved client caps the number of redirects it will follow (browsers use around twenty; libraries often default lower). Always keep a cap. If you build the following logic yourself, count hops and stop.
Relative vs absolute Location
The Location header is allowed to be a relative URL like /new-path or even ../thing. You must resolve it against the URL you just requested, not against the site root and not as-is. Getting this wrong sends requests to malformed URLs. Good HTTP clients resolve relative locations for you, which is a strong reason to lean on the client rather than parsing Location by hand.
Cookies across redirects
Session-based flows often set a cookie on the first response and expect it back on the redirect target. If your client does not carry cookies forward across the hop, the destination sees an unauthenticated request and bounces you to a login or a 403. Use a client with a cookie store, or a session object, so cookies persist across the chain. If you are seeing a redirect to a login page you did not expect, dropped cookies are the usual culprit.
Handling redirects in code
Here is Python with requests, which follows redirects by default and exposes the full chain.
import requests
resp = requests.get("https://example.com/old-path", allow_redirects=True)
print("final url:", resp.url)
print("status:", resp.status_code)
for hop in resp.history:
print(hop.status_code, "->", hop.headers.get("Location"))
To inspect a redirect without following it, turn following off.
resp = requests.get("https://example.com/old-path", allow_redirects=False)
if resp.is_redirect:
print("would go to:", resp.headers["Location"])
Use a Session when cookies must survive the chain.
session = requests.Session() # persists cookies across redirects
resp = session.get("https://example.com/login-required")
In Node with fetch, redirects are followed automatically. Set redirect: "manual" to inspect instead.
const res = await fetch("https://example.com/old-path", { redirect: "manual" });
if (res.status >= 300 && res.status < 400) {
console.log("redirect to:", res.headers.get("location"));
}
For more on what each status code means for a scraper beyond the 3xx range, see HTTP status codes for web scraping. A stubborn 403 after a redirect is often a blocking signal rather than a routing one, which we cover in HTTP 403 forbidden when scraping.
How a managed API handles redirects for you
When you fetch through the link.sc API, redirects are followed on the server side and you get the final rendered content, with cookies and relative locations resolved along the way. You do not write hop counting or Location parsing.
curl -X POST https://api.link.sc/v1/fetch \
-H "x-api-key: lsc_your_key_here" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/old-path", "format": "markdown"}'
The response is the destination page as clean markdown:
{ "content": "# The Real Page\n\nContent after all redirects were followed..." }
If you need to send your own cookies through the chain (for a session you control legitimately), the fetch body accepts cookies and headers, so the request that lands on the final URL carries what it needs. This is the same reason the managed approach is handy for JavaScript-heavy sites: the hard parts happen before the content reaches you.
A quick checklist
- Follow redirects, but cap the hop count to avoid loops.
- Keep the final URL and the redirect chain for debugging.
- Resolve relative
Locationheaders against the current request URL. - Persist cookies across hops with a session or cookie store.
- Treat an unexpected redirect to
/loginas a blocking signal, not a routing detail. - Mind method preservation:
307and308keep your method,301,302, and303may switch it to GET.
Redirects are simple once you know the rules, and dangerous when you assume the defaults are always right. Decide deliberately whether to follow, keep a cap, and carry cookies, and most 3xx surprises disappear.
Want redirects, cookies, and rendering handled for you? Get a free link.sc key and fetch any URL as clean markdown.