Why CORS proxies don't follow redirects for you
A request through corsproxy.dev to an API that happens to redirect, say an old path moved to a new one, can look like it worked and then fail anyway: the browser's console shows a CORS error, naming a domain you never called directly. The proxy call didn't fail. What failed is the second, invisible request the browser made on its own right after.
What the proxy actually does with a 3xx
corsproxy.dev forwards your request to the target with the fetch option redirect: 'manual', which means it does not automatically chase a redirect on your behalf. If the target responds with a 301, 302, 307, or any other 3xx, that status code and its Location header are handed back to you exactly as the target sent them, with corsproxy.dev's own CORS headers added on top. The proxy's response to a redirecting target is itself a redirect.
That's easy to miss, because it's rarely what you expect from a CORS proxy. You asked for a page or a payload, and what came back is a pointer to somewhere else.
Why your fetch() call fails right after a successful proxy response
Here's where the CORS proxy redirect problem usually shows up in practice. A plain fetch() call defaults to redirect: "follow": when the browser receives a 3xx for a request it made, it follows the Location header itself, transparently, before your code ever sees a response. So when your page calls:
const res = await fetch(
"https://api.corsproxy.dev/proxy?key=YOUR_KEY&url=" +
encodeURIComponent("https://api.example.com/old-path")
);
and https://api.example.com/old-path 301s to https://api.example.com/new-path, the browser doesn't hand your code the 301. It follows it immediately, which means the second request goes straight to api.example.com, not back through corsproxy.dev. That second request is now a plain cross-origin request from your page to a server you reached for a CORS proxy in the first place, presumably because it doesn't send CORS headers. If it doesn't, the browser blocks the response, and the console error names api.example.com, the domain your code never explicitly called.
Open your browser's network tab when this happens and you'll see it as two separate requests: a 301 (or whichever 3xx) from api.corsproxy.dev, immediately followed by a request straight to the target's own domain that gets blocked. The proxy did exactly what it was asked; the request that failed is one your code never issued.
Why we don't just follow the redirect ourselves
Following it server-side would remove the symptom, so it's worth saying why corsproxy.dev doesn't. The safety checks that make it reasonably safe to let a proxy fetch an arbitrary URL on your behalf, blocking private and loopback addresses and resolving hostnames through a DNS resolver that flags malicious domains, run once, before that initial request goes out. A redirect response is new information: a 3xx can point anywhere, including an address that would have been blocked if it had been the original target. Automatically following it would mean fetching a second, caller-influenced URL that never went through that check at all.
Handing the redirect back to the caller instead keeps every URL corsproxy.dev actually fetches inside the same validated path: if you want the destination fetched, you ask for it explicitly, by URL, and that URL goes through target validation like any other request.
The open-source relay makes a different tradeoff
The open-source Go relay is a simpler, separate program (not what runs the managed service), and it handles this differently: by default it follows up to 10 redirects itself (MAX_REDIRECTS, configurable), and it leans on the same safety mechanism for every hop, not an extra one bolted on for redirects specifically. Its outbound connections go through a dialer that checks the actual IP it's about to connect to, not just the hostname in the URL, on the initial request and on every redirect hop alike, which is also what keeps a DNS-rebinding attempt from slipping through on hop two (main.go:366-374, 554-586). That's a reasonable design for a relay you run yourself with your own trust boundary. It's a different one than a multi-tenant managed service makes for URLs supplied by whoever holds any customer's key.
Handling it from your own code
Setting redirect: 'manual' on your own fetch() call to corsproxy.dev doesn't help here: that only stops the browser from auto-following redirects it receives for your request, and the response you'd get back is opaque, you can't read the status or Location header off it. Two things actually work:
- Call the final URL directly, if you know it. If an API moved a path permanently, find the destination once, outside the browser, with a plain HTTP client, and proxy that URL going forward:
That one check tells you the canonical URL to pass to corsproxy.dev, skipping the redirect entirely on every later call.curl -sI https://api.example.com/old-path | grep -i ^location: - Self-host the open-source relay if the redirect is dynamic. When a target's redirect target genuinely varies per request and you can't pin a canonical URL, running the relay yourself gets you redirect-following with the tradeoffs above, in exchange for giving up the managed service's per-key quotas, upstream header storage, and multi-tenant target validation.