Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

Three providers, three error formats: normalising them into one honest message

My little downloader site talks to three different video APIs. One handles TikTok and Instagram, one handles Facebook, and one handles YouTube. They each do the same job — take a link, return a file — but when something

My little downloader site talks to three different video APIs. One handles TikTok and Instagram, one handles Facebook, and one handles YouTube. They each do the same job — take a link, return a file — but when something goes wrong, they each complain in a completely different language. If I passed those errors straight through, my visitors would see three kinds of nonsense. So I built a small normaliser, and it turned out to be one of the most useful pieces of code on the site.

The problem

Here is what a failure actually looks like from each provider.

The all-in-one provider returns HTTP 200 with a body like {"status": "error", "msg": "Service is busy"}. The Facebook provider returns a proper HTTP 500 with {"error": "Failed to fetch video"}. The YouTube provider sometimes returns {"code": 429, "message": "Too many requests"} and sometimes an empty body with no JSON at all. Three providers, four shapes, and none of them say anything a normal visitor would understand.

Worse, the shapes change. The all-in-one provider once started returning {"success": false, "error": "..."} for a week, then went back. If my front end had been written against one shape, it would have broken silently. So I stopped trusting the shape and started trusting a mapping.

The normaliser

The idea is simple: every provider response passes through one function that returns the same array, whatever it was given. It looks roughly like this:

function normalise_error($raw, $httpCode) {
    $data = json_decode($raw, true);
    if (is_array($data)) {
        $msg = $data['msg'] ?? $data['error'] ?? $data['message'] ?? '';
        $code = $data['code'] ?? $data['status'] ?? $httpCode;
    } else {
        $msg = '';
        $code = $httpCode;
    }
    $msg = is_string($msg) ? trim($msg) : '';
    return ['code' => $code, 'message' => $msg];
}

That is the easy half. The harder half is deciding what the visitor sees. A raw provider message is written for a developer, not for someone who just pasted a link. "Service is busy" is honest but vague; "Failed to fetch video" is honest but useless; an empty body says nothing at all. So the normaliser does not stop at shape — it maps the message to one of a small set of plain sentences:

  • Service is busy — the provider is overloaded, try again in a minute.
  • This link did not work — the video may be deleted or private, or the link is wrong.
  • Too many requests — we have hit the provider's limit for now, please try again later.
  • Something went wrong on our side — the fallback when nothing matched.

The mapping is a few stripos checks against the raw message, in order from most specific to most general. "Service is busy" is checked first because the words "busy" also appear inside other messages. If nothing matches, the visitor gets the fallback, and I log the raw message so I can add a mapping for it later. The log holds the provider's message only — never the visitor's link, never anything about the visitor.

Why the fallback matters

The fallback is the part I am most careful about. It is tempting to show the provider's raw message when nothing matches, on the grounds that more detail is more helpful. It is not. A visitor who pastes a TikTok link does not know what "upstream 502" means, and a message they cannot act on is just noise. So the fallback always says the same thing: something went wrong on our side, please try again in a little while. Meanwhile the log tells me the truth, and if the same raw message keeps appearing, I add it to the map.

That is the split that makes the whole thing work: the visitor gets a short, honest sentence they can act on, and I get the raw detail in a log where it is actually useful.

What I learned

First, never trust an error shape. Providers change their JSON without telling you, and a 200 status can still mean failure. Check the body, not just the status code.

Second, keep the set of visitor-facing messages small. Four sentences cover almost everything, and because there are only four, I can word each one properly instead of hedging.

Third, the normaliser belongs in one place. My site has one resolve.php endpoint that calls all three providers, so the mapping lives there, once. If I had copied the checks into three places, they would have drifted apart within a week — I know this because that is exactly what happened before I pulled them together.

It is not glamorous code. There is no library, no framework, and no clever trick. It is about thirty lines of plain PHP, and it is the reason a visitor on ramrom.io sees "the provider is busy, try again in a minute" instead of a JSON blob. That is the whole point of a tool site: the visitor should never have to read an API's mind.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.