DevTools Hub

Search tools

Search for a developer tool

Common Encoding Problems in APIs

A striking share of "the API is broken" bugs aren't logic bugs at all — they're encoding bugs. A token that works in Postman but fails from the browser, a name that renders as café instead of café, a payload that decodes to garbage only on one server and not another. Here are the encoding problems that actually show up in production APIs, roughly in order of how often they bite.

1. Double encoding

A value gets percent-encoded once by application code, then encoded again by a framework, proxy, or HTTP client that assumes it's still raw. %20 (an encoded space) becomes %2520 (an encoded %20). It compounds with every extra layer:

encodeURIComponent("100% off")            -> "100%25%20off"
encodeURIComponent("100%25%20off")        -> "100%2525%2520off"

The symptom is usually a literal %25 showing up somewhere it shouldn't, or a value that decodes to something almost-but-not-quite right. Fix it by encoding exactly once, as close as possible to the point where the value actually gets inserted into a URL — and if you're not sure how many layers a broken value has been through, run it through URL Decode repeatedly until it stops changing to see how deep it goes.

2. Standard Base64 dropped into a URL

An API returns a token or ID as standard Base64, and client code appends it straight onto a URL. Standard Base64's alphabet includes + and /, both of which mean something else in a URL — / reads as a path separator, and many query-string parsers (following the older application/x-www-form-urlencoded convention) silently decode + as a space. Either way the value that comes back out isn't the one that went in, and nothing raises an error — it just fails to decode correctly downstream. The fix is the Base64URL alphabet (-/_, no padding) for anything that might end up in a URL — it's exactly what JWTs use, for this reason. See Base64 vs URL Encoding for the full breakdown of why these two are easy to conflate.

3. Base64 padding mismatches between systems

Not every Base64 consumer agrees on padding. JWTs and many URL-safe implementations conventionally strip the trailing = characters; plenty of other decoders require them and reject input that isn't padded to a multiple of 4. Python's base64.b64decode is a common place this surfaces — b64decode("SGk") raises binascii.Error: Incorrect padding, while b64decode("SGk=") works fine, even though both represent the same two bytes. If a value is failing to decode only in one system, check whether padding survived the trip — you can always restore it yourself by appending = characters until the length is a multiple of 4.

4. Charset mismatches (mojibake)

Text encoded as UTF-8 but decoded as if it were Latin-1 (or vice versa) doesn't error — it just corrupts, because every byte value is technically valid in both charsets, just mapped to different characters. The classic tell is an accented character turning into two or three garbled ones:

"café" as UTF-8 bytes:        63 61 66 c3 a9
those same bytes read as Latin-1:  "café"

This happens when a Content-Type header omits or lies about its charset, or when one part of a pipeline (a database column, a CSV export, a legacy system) assumes Latin-1 while everything around it assumes UTF-8. There's no clever fix here beyond being explicit: declare charset=utf-8 on every response that sends text, and verify the sending and receiving ends actually agree rather than assuming they do.

5. Trusting API data as safe to drop into HTML

Data coming back from "your own" API still needs escaping before it lands in HTML — it's not automatically trustworthy just because your backend produced it, especially if any part of that data ultimately came from user input (a display name, a comment, a product title). Skipping this is the most common path to a reflected XSS bug: a name containing <img src=x onerror=alert(1)> renders as markup instead of text the moment it's concatenated into a page without escaping. Run untrusted text through HTML Encode at the point it gets rendered — most frameworks do this automatically for you, but raw string concatenation or dangerouslySetInnerHTML-style escape hatches don't.

6. Hand-built JSON strings

Building a JSON body by template-stringing values together instead of using a real serializer breaks the moment a value contains a quote, backslash, or newline:

// breaks if `name` contains a "
const body = `{"name": "${name}"}`;

A name of Alice "The Great" produces invalid JSON, and a name containing a raw newline does the same. Always build the object as data and hand it to JSON.stringify (or your language's equivalent) — never construct JSON by concatenating strings, even for "just one field." If you've inherited a payload built this way and need to confirm it's actually valid, paste it into JSON Formatter.

7. Binary payloads placed directly into a JSON field

JSON strings are Unicode text, not byte arrays — there's no way to represent arbitrary binary data (a file, an image, a raw hash) as a JSON string without first converting it to text. Trying to shove raw bytes in directly produces either an outright parse error or, worse, silent corruption if some of those bytes happen to form invalid UTF-8 sequences. The standard fix is to Base64-encode the binary data first and put that string in the JSON field — it's exactly why file uploads, embedded images, and cryptographic material so often travel as Base64 inside API payloads.

How to debug an encoding bug in general

  • Decode repeatedly, not just once. If a value looks almost right, it may be encoded more times than you think — keep decoding until it stabilizes.
  • Check the actual bytes, not just the rendered text. Mojibake and charset bugs are invisible until you look at the raw byte sequence — most editors and browser dev tools can show you a string's underlying bytes.
  • Compare a known-good payload against the broken one byte-for-byte rather than guessing — the difference is usually a single missing padding character, an extra layer of escaping, or one substituted character in the Base64 alphabet.
  • Reproduce with the simplest possible input. A single accented letter or a value containing one + is enough to confirm or rule out most of the problems above, without needing a full realistic payload.

Try it yourself

Decode a suspicious percent-encoded value with URL Decode, check what a Base64 string actually contains with Base64 Decode, or escape untrusted text before it reaches HTML with HTML Encode. All three run entirely in your browser, so nothing you paste in is ever sent anywhere.

Related tools