JSON и API в PHP: чек-лист перед выкатом в продакшен
Что проверить в коде, который парсит JSON и ходит во внешние API, прежде чем выкатывать его на боевой сервер: от флагов json_decode до таймаутов cURL.

Код, который в разработке отработал на трёх тестовых ответах, в проде встречает пустой body, HTTP 503 от чужого сервиса, битую кодировку и JSON на 40 мегабайт. Разница между «работает у меня» и «работает под нагрузкой» — в десятке проверок, которые легко забыть. Ниже — чек-лист для PHP 8.x: что учесть в парсинге JSON и в HTTP-клиенте, прежде чем это увидят реальные пользователи.
Речь о PHP 8.1–8.3. Часть поведения (флаг JSON_THROW_ON_ERROR, исключения по умолчанию) появилась в 7.3–8.0, поэтому если у вас 7.x — сверяйтесь с разделом про версии в конце.
Парсинг JSON: что проверить в json_decode
Проверяйте результат, а не надейтесь на него
Классическая ошибка — обращаться к полю сразу после json_decode, не проверив, что декодирование вообще прошло. При невалидном входе функция по умолчанию возвращает null, и следующая строка падает на обращении к свойству null.
Начиная с PHP 7.3 есть флаг, который превращает молчаливый null в исключение JsonException. Используйте его — так ошибка не утечёт дальше по коду замаскированной.
<?php
// PHP 8.x
try {
// associative = true вернёт массив вместо stdClass;
// depth ограничивает вложенность, JSON_THROW_ON_ERROR даёт исключение
$data = json_decode(
$rawBody,
associative: true,
depth: 64,
flags: JSON_THROW_ON_ERROR
);
} catch (\JsonException $e) {
// логируем сырой ответ обрезанным, чтобы не забить лог гигабайтом
error_log('JSON decode failed: ' . $e->getMessage());
throw new \RuntimeException('Некорректный ответ внешнего сервиса', previous: $e);
}
Проверка: скормите функции строку '{"a":}' — без флага вернётся null, с флагом поймаете JsonException.
Числа, которые ломаются молча
JSON не различает целые и дробные, а PHP на 64-битных системах не удержит число больше PHP_INT_MAX и превратит его в float — с потерей точности. Если API отдаёт длинные идентификаторы (например, ID сущностей в 18–19 цифр), декодируйте их как строки флагом JSON_BIG_INT_AS_STRING, иначе 9007199254740993 тихо станет 9007199254740992.
Битый ID заказа не выдаёт себя ошибкой. Он просто не находит запись в базе, и вы полдня ищете, где потерялась единица в последнем разряде.
Кодировка ответа
json_decode ждёт валидный UTF-8. Если сервис отдаёт latin1 или Windows-1251 (а такое встречается у старых интеграций), декодирование упадёт с ошибкой Malformed UTF-8 characters. Не глушите её флагом — приведите вход к UTF-8 mb_convert_encoding до парсинга, ориентируясь на заголовок Content-Type ответа.
Размер входа
Ответ на 50 МБ, загруженный в память целиком, положит воркер по memory_limit. Если ждёте большие ответы — не читайте их через json_decode одной строкой. Потоковый разбор JSON стандартными средствами PHP не делается, для этого берут пакеты вроде потоковых парсеров; это отдельная тема, здесь фиксируем только сам риск и лимит на размер тела.
Кодирование JSON: что уходит в ответ
При отдаче JSON проверяйте флаги json_encode. По умолчанию кириллица экранируется в \uXXXX, а слэши — в \/. Это валидно, но раздувает ответ и мешает читать логи.
<?php
header('Content-Type: application/json; charset=utf-8');
echo json_encode(
$payload,
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR
);
Отдельно проверьте, что в $payload не попадают ресурсы или замыкания — json_encode их не сериализует, и без JSON_THROW_ON_ERROR вы получите false вместо ответа и пустое тело у клиента.
HTTP-клиент: где ломается запрос к чужому API
Таймауты — не опция, а обязанность
Запрос без таймаута висит до дефолта операционной системы. Один зависший внешний сервис при этом держит ваши php-fpm воркеры занятыми, и через минуту весь пул забит ожиданием — падает уже ваш сайт, а не чужой API. Всегда задавайте и таймаут соединения, и общий таймаут.
<?php
// cURL, PHP 8.x
$ch = curl_init('https://api.example.com/v1/orders');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5, // сколько ждём установки соединения, сек
CURLOPT_TIMEOUT => 15, // общий лимит на весь запрос, сек
CURLOPT_HTTPHEADER => ['Accept: application/json'],
CURLOPT_FAILONERROR => false, // не хотим, чтобы 4xx/5xx маскировались
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno !== 0) {
throw new \RuntimeException('cURL error: ' . curl_strerror($errno));
}
Проверка: направьте запрос на порт, где никто не слушает — при CURLOPT_CONNECTTIMEOUT = 5 ошибка придёт через пять секунд, а не через минуту.
Проверяйте HTTP-код отдельно от тела
Частая ошибка — парсить тело, не глядя на статус. Сервис вернул 500 с HTML-страницей ошибки, а вы пытаетесь декодировать её как JSON. Сначала статус, потом Content-Type, и только затем парсинг.
- 2xx — обрабатываем тело;
- 4xx — ошибка на нашей стороне (неверный запрос, истёк токен), повтор бессмысленен;
- 5xx и сетевые ошибки — временный сбой, здесь уместен повтор с задержкой;
- 429 — вас лимитируют, смотрите заголовок
Retry-Afterи ждите указанное время.
Повторы без лавины
Повтор запроса после 5xx — правильно. Повтор без задержки в цикле — способ добить и без того лежащий сервис и словить у него бан по частоте. Используйте экспоненциальную задержку: 1, 2, 4 секунды, не больше трёх-четырёх попыток. И не повторяйте неидемпотентные запросы (создание платежа) вслепую — можно списать деньги дважды.
Проверка TLS
Не отключайте CURLOPT_SSL_VERIFYPEER. В интернете полно советов поставить его в false, чтобы «убрать ошибку сертификата». Это убирает не ошибку, а защиту от подмены ответа. Если сертификат не проходит проверку — обновите пакет CA-сертификатов на сервере или задайте путь через CURLOPT_CAINFO, но не выключайте верификацию на проде.
Секреты и логи
Токены и ключи API не хранятся в коде и не попадают в git. Читайте их из переменных окружения или защищённого хранилища. В логах запросов вырезайте заголовок Authorization и чувствительные поля тела — иначе ключ окажется в общем лог-файле, к которому есть доступ у половины команды.
Сводный чек-лист
| Проверка | Чем грозит пропуск |
|---|---|
| JSON_THROW_ON_ERROR при декодировании | обращение к null, падение ниже по коду |
| JSON_BIG_INT_AS_STRING для длинных ID | тихая потеря точности идентификаторов |
| Приведение входа к UTF-8 | ошибка Malformed UTF-8 |
| Лимит на размер тела ответа | превышение memory_limit, падение воркера |
| CONNECTTIMEOUT и TIMEOUT в cURL | зависшие воркеры, отказ всего сайта |
| Проверка HTTP-кода до парсинга | попытка декодировать HTML-ошибку как JSON |
| Повторы с экспоненциальной задержкой | добивание сервиса, бан по частоте |
| SSL_VERIFYPEER = true | подмена ответа внешнего сервиса |
| Токены вне кода, маскировка в логах | утечка ключей |
Что сделать перед выкатом
Пройдитесь по коду интеграции с таблицей выше как по списку задач. Отдельно проверьте поведение на трёх искусственных ответах: пустое тело, HTTP 500 с HTML внутри, обрезанный на середине JSON. Если все три обрабатываются без фатала и пишут внятную ошибку в лог — интеграция готова к продакшену.
Дальше настройте мониторинг: считайте долю неуспешных ответов внешнего API и время его ответа (percentile 95, а не среднее). Именно рост p95 первым покажет, что чужой сервис деградирует, задолго до того, как ваши воркеры начнут висеть на таймаутах.
Тендерный специалист 44-ФЗ / 223-ФЗ: Разбор PDF-документации закупки, поиск «заточек» под поставщика, чек-лист заявки.
Спросить за 20 ₽Источники: PHP Manual: json_decode, PHP Manual: cURL setopt options
Частые вопросы
У меня PHP 7.2, флаг JSON_THROW_ON_ERROR не работает — что делать?
Использовать cURL напрямую или брать Guzzle?
Что вернёт json_decode при пустой строке?
Почему кириллица в ответе превращается в \u0430\u0431?
Достаточно ли задать только CURLOPT_TIMEOUT без CONNECTTIMEOUT?
Стоит ли повторять запрос при ошибке автоматически?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.