json_encode silently returns false on invalid UTF-8 — caught us in prod
Sharing a production incident from this week because it was surprisingly hard to debug.
We have an API endpoint that serializes data from MySQL and returns JSON. Started getting silent 500 errors with no useful log output — the response was just empty. No exception, nothing.
After two hours of investigation: one column in MySQL was latin1 charset (legacy table, not migrated yet), and a row with extended characters was being read, converted by PDO to a PHP string with invalid UTF-8 bytes, and then passed to json_encode().
json_encode() returns false on invalid UTF-8. By default it does this silently. We were doing:
Which internally calls json_encode($data), gets false, and the framework choked on it quietly.
Fix 1 — detect it immediately:
Fix 2 — use JSON_INVALID_UTF8_SUBSTITUTE flag which replaces bad bytes with the unicode replacement character instead of failing.
Fix 3 — fix the actual root cause: migrate the column to utf8mb4.
We did all three. The flag is a good safety net but the real fix is the charset migration.
Hit this exact issue two years ago with a legacy database. JSON_THROW_ON_ERROR flag is also worth adding globally — it makes json_encode throw a JsonException on any failure instead of returning false, so nothing slips through silently:
After that incident we added a static analysis rule (PHPStan) to flag any json_encode() call without JSON_THROW_ON_ERROR. Caught three more potential silent failures in other parts of the codebase.
The JSON_INVALID_UTF8_SUBSTITUTE flag is underused. It’s been in PHP since 7.2 but most people don’t know it exists.
One thing to be careful with though: if you use it in an API that sends data to another service, you might be sending replacement characters (\uFFFD) which downstream might not handle gracefully either. Fine for logging or display, potentially problematic for data pipelines.
For the charset migration — if it’s a large table, do it with ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 during low-traffic window. Not online-safe on InnoDB for large tables without pt-online-schema-change.
Also check your MySQL connection charset config. Even if the column is utf8mb4, if your PDO DSN doesn’t specify charset=utf8mb4, the connection might be negotiated as latin1 and you get the same problem on the wire.
This one is easy to miss because the default PDO charset doesn’t match what you’d expect.
```php blocks are runnable.