DEV Community

Cover image for I built an API client that runs in your browser. Here's how I handled CORS without running an open proxy
Anirudha Sonwane
Anirudha Sonwane

Posted on

I built an API client that runs in your browser. Here's how I handled CORS without running an open proxy

 I built a free API client that runs in the browser as part of Tool Reign. It isn't open source, so there's no code in this post. But the design decisions, especially around the proxy, might help if you're building something similar.

Why a browser-based API client is hard

A browser can send a request to any URL, but it only lets your page read the response if the server allows it. That's CORS, and a lot of APIs don't allow it. Other limits:

  • Some headers can't be set from a page (Host, Origin, Cookie and a few more)
  • Set-Cookie can't be read
  • You only see the response headers the server chose to expose
  • Detailed timing (DNS, TLS) isn't available

Two modes

  • Direct: the request goes from your browser to the API. Nothing passes through my server. It works for any API that allows CORS.
  • Proxy (opt-in): the request goes browser → Tool Reign's server → the API. It shows headers, cookies and timing that the browser hides, and it works for APIs that block browser requests.

Direct is the default. If a request fails with a CORS error, the tool offers to retry through the proxy, with a notice explaining what the proxy sees. Localhost and private-network targets stay Direct-only, on purpose.

The proxy is where the risk is

An endpoint that fetches any URL you give it is the textbook setup for server-side request forgery, and it attracts abuse. This is what I designed it to do: [VERIFY EACH LINE AGAINST YOUR CODE]

  • Resolve the hostname myself and reject anything private or reserved: loopback, private ranges, link-local (including the cloud metadata address), carrier-grade NAT, multicast, and the IPv6 equivalents, including IPv4-mapped addresses
  • Connect to the address I validated instead of resolving again, so DNS rebinding can't swap in a private IP
  • Don't follow redirects automatically; check each hop, and don't carry Authorization or Cookie to a different origin
  • Cap request size (5 MB), response size (10 MB) and time (30 seconds)
  • Accept calls only from the site's own origin, with rate limits per client
  • Store nothing. The app's logs hold a request ID, a status class, an error code and a duration, and no URLs, headers or bodies

The privacy trade-off

The most sensitive things people type into an API client are tokens and keys, and a proxy sees them. That's why it's opt-in, why there's a consent notice the first time, and why the advice is not to use production credentials through it. If you don't want to trust a proxy at all, Direct mode never touches my server.

I'd rather say that plainly than call it "private" and hope nobody checks. Open the Network tab and watch: in Direct mode your request goes to the API you entered (plus analytics requests if you accepted the banner).

Three things that broke while deploying

  1. My zip file. I built the archive on Windows with PowerShell's Compress-Archive. It wrote paths with backslashes, so the Linux host saw files literally named like "src\server.js" and crashed with "cannot find module." A tar-built zip fixed it.
  2. The health check. The hosting platform probes the app to check it's alive. My origin check rejected those probes, so the app stayed marked unhealthy. Health routes need to be exempt.
  3. DNS. At some point during setup, my root domain's A record turned into a "Parked" placeholder, and visitors landed on my registrar's parked page. I never pinned down which step caused it. Check your DNS after every domain change.

What's in the tool

Collections, environments with variables, request history, params, headers, body and auth, plus response body, headers, cookies and timing, code snippets, and import, backup and restore.

Try it: [https://toolreign.com/developer/api-client/]

I'd like to know what's missing for you, and I'm happy to answer architecture questions in the comments.

Top comments (4)

Collapse
 
omyvnss profile image
Om Yaduvanshi •

good stuff, anirudha. this is an unusually honest build post, the part about a proxy seeing tokens is the bit most people would bury in a footnote, and you gave it a section heading instead.

the per-hop redirect check before carrying Authorization forward is the detail i was waiting for. most proxy writeups hand-wave redirects entirely.

direct-first with proxy as opt-in is the right default. one real-world snag: store-nothing logs make debugging proxy failures harder in practice. fine tradeoff, just a real one.

Collapse
 
anirudha_sonwane_ca3fc720 profile image
Anirudha Sonwane •

Thanks Om. Yes, it's a real trade-off. Logging only a request ID, status class, error code and duration tells me a call failed but not why. I just hit that myself: a network error that looked identical for several different causes.

I'm fixing it by moving the diagnosis to the client: structured error codes and hints in the response, a health probe, and a "test connection" button. The person debugging gets the reason, and my server still doesn't keep their URLs or tokens.

Collapse
 
szp2005 profile image
szp2005 •

About the per-client rate limits: if you ever add IP reputation to catch people rotating proxies, don't trust one feed. Some flag whole AWS or GCP ranges as proxies, so a dev calling from a cloud VM looks like an abuser. I only count a datacenter IP as a proxy when two dedicated sources agree. For residential IPs one is enough.

Collapse
 
unitbuilds profile image
UnitBuilds •

Do not follow external links, this is a phishing scam. DEV.to uses Sloan for automated messaging.