Turning Complex Problems Into Confident Technology

Fifteen API routes that never existed

Integrating other people's systems · Production · Inside the Workshop

The problem

I built a proxy in front of an accounting platform's API so that internal tools, and later an assistant, could read and write bookkeeping data without every caller holding the platform's credentials. It grew to around 170 routes.

Fifteen of them pointed at endpoints that do not exist.

How that happens

Not through carelessness, which is the uncomfortable part. A proxy route is a string that is forwarded to another string. Nothing in the type system, the compiler, the unit tests or the deployment has any opinion about whether the far end is real. A route that forwards to a URL that 404s is, from the proxy's point of view, working perfectly: it faithfully relayed a response.

So the fifteen sat there looking exactly like the other 155. They were only discovered by going through the vendor's documentation route by route and comparing, which is dull work that no test was ever going to do.

Two more failures of the same family

  • Literal routes hidden behind parameterised ones. A framework that matches /things/{id} before /things/summary will send every request for the summary into the by-identifier handler, which then asks the upstream for a thing called "summary" and gets a sensible-looking error. The route existed, was tested in isolation, and was unreachable in place. Ordering is part of the contract and is invisible in the route list.
  • A 403 that was not an authentication problem. The upstream returns 403 for endpoints the account's access level does not include. The proxy treated 403 the way most code does — as a broken or expired token — and dutifully refreshed credentials that were fine, repeatedly, and reported an auth failure for what was actually a permanent verdict about scope. Those routes are now documented as permanently unavailable to this account, which is a more useful answer than a retry.

What I chose, and why

The proxy now exposes its own route table as an endpoint. That sounds like a small addition and it changed how the thing is used: the authoritative answer to "what can I call" comes from the running service rather than from documentation, a wiki page or somebody's memory, and it is the first thing to consult before writing a client.

Errors are status-mapped rather than passed through raw, so a caller can distinguish "you may not do this" from "this is temporarily unavailable" from "you asked for something that does not exist" — which is precisely the distinction the 403 handling had been collapsing.

What I would do differently

I would have generated the routes from the vendor's specification rather than writing them, or failing that, written a check that walks every declared route against the live API and reports the ones that answer 404. Either would have caught all fifteen on the first run, and both are less work than the manual comparison that eventually found them.

More generally: I trusted my own route list because it compiled. A compiled integration is not a verified one, and the gap between those two things is where this entire category of bug lives.

Related

If this is your problem too

Integrations that "work" until someone calls the untested path are the usual reason for an integration review. The finance side of this system is written up as the financial management application.