8 lines
15 KiB
JSON
8 lines
15 KiB
JSON
{
|
||
"index": 355,
|
||
"slug": "editorial-2018-02-field-php-diagnostics",
|
||
"title": "PHP: почему null не доказывает ошибку JSON",
|
||
"excerpt": "Пустой массив, null и false могут быть корректным JSON, а ошибка декодирования тоже часто возвращает null. Разбираем ответ API по слоям: транспорт, синтаксис и контракт данных.",
|
||
"contentHtml": "<p>Клиент вызывает API, получает HTTP 200 и показывает пустой результат. В журнале остаётся одна запись: <code>bad response</code>. Пользователь не понимает, создался ли заказ. Разработчик не знает, что вернул партнёр: пустой список, <code>null</code>, HTML страницы ошибки или JSON без обязательного поля. Цена такой ошибки — неверное решение на границе интеграции. Можно повторить операцию, хотя она уже прошла, или скрыть реальный сбой под пустым экраном.</p>\n<p>Частая причина — условие <code>if (!$data)</code> сразу после <code>json_decode()</code>. В PHP это условие смешивает несколько разных значений. Пустой массив ложен. <code>false</code> и <code>0</code> тоже ложны. Корректный JSON <code>null</code> превращается в <code>null</code>. Невалидная строка часто даёт тот же <code>null</code>. Одного значения переменной недостаточно для диагноза.</p>\n<p>Надёжный порядок состоит из трёх границ. Сначала проверяется транспорт и HTTP-статус. Затем читается ошибка декодирования сразу после <code>json_decode()</code>. Только после успешного разбора проверяется форма ответа и обязательные поля контракта. Этот порядок не угадывает причину по пустому значению. Он сохраняет причинность.</p>\n<h2>Что именно различает PHP</h2>\n<p><code>json_decode()</code> переводит JSON-строку в значение PHP. При флаге <code>true</code> объект становится ассоциативным массивом. Литералы <code>true</code>, <code>false</code> и <code>null</code> сохраняют свои типы. Если строка не разбирается, функция также может вернуть <code>null</code>. Поэтому результат декодера нужно читать вместе с состоянием JSON-расширения.</p>\n<p>В PHP 7.1 нет флага <code>JSON_THROW_ON_ERROR</code>. Для этой версии проверка <code>json_last_error()</code> — основной способ отличить ошибку разбора от корректного значения. В новых версиях можно включить исключение декодера, но проверка контракта ответа всё равно остаётся отдельной задачей.</p>\n<p>Например, строка <code>null</code> — допустимый JSON. Но если endpoint создания заказа обязан вернуть объект с <code>order_id</code>, такой ответ не подходит операции. Это не синтаксическая ошибка. Это корректный формат с неверной формой данных. То же относится к <code>[]</code>: для поиска это может означать «ничего не найдено», а для создания заказа — отсутствие результата.</p>\n<figure><img src=\"/assets/editorial/2018/json-payload-diagnostic.svg\" alt=\"Поток диагностики JSON: HTTP-статус, декодирование, проверка формы и контракта\" /><figcaption>Один ответ проходит последовательные проверки. Ошибка транспорта, ошибка JSON и нарушение контракта не должны получать одно сообщение.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>HTTP 200, результат <code>null</code></td><td>Корректный JSON <code>null</code> или ошибка разбора</td><td>Сразу прочитать <code>json_last_error()</code></td><td>Разделить ветки формата и контракта</td></tr><tr><td>HTTP 200, пустой массив</td><td>Валидный список без элементов</td><td>Проверить <code>JSON_ERROR_NONE</code> и смысл endpoint</td><td>Принять только там, где пустой список разрешён</td></tr><tr><td>HTTP 503, тело HTML</td><td>Удалённый сервер или прокси вернул страницу ошибки</td><td>Сохранить статус, размер и безопасный отпечаток тела</td><td>Обработать транспортную ошибку; JSON не разбирать как успешный ответ</td></tr><tr><td>HTTP 200, объект без <code>order_id</code></td><td>JSON синтаксически корректен, но договор изменился или нарушен</td><td>Проверить тип, наличие и непустое значение поля</td><td>Отклонить ответ как ошибку контракта</td></tr><tr><td>Разные ответы дают один лог</td><td>Ошибки сведены к <code>bad response</code></td><td>Добавить категорию и request id</td><td>Логировать отдельные события с безопасным контекстом</td></tr></tbody></table></div>\n<h2>Учебный обработчик</h2>\n<p>Ниже приведён учебный пример для PHP 7.1. Он не делает HTTP-запрос и не обещает поведение конкретного API. Предполагается, что код выше уже проверил статус ответа. Для операции создания заказа договор намеренно мал: успешное тело должно быть JSON-объектом с непустым строковым <code>order_id</code>.</p>\n<pre><code><?php\n\nfunction logPayloadEvent($kind, array $context)\n{\n $context['kind'] = $kind;\n error_log(json_encode($context, JSON_UNESCAPED_UNICODE));\n}\n\nfunction rejectPayload($reason, $requestId, $body)\n{\n logPayloadEvent('contract_error', array(\n 'reason' => $reason,\n 'request_id' => $requestId,\n 'body_bytes' => strlen($body),\n 'body_sha256' => hash('sha256', $body),\n ));\n\n throw new UnexpectedValueException($reason);\n}\n\nfunction decodeCreatedOrder($body, $requestId)\n{\n if ($body === '') {\n rejectPayload('empty response body', $requestId, $body);\n }\n\n $data = json_decode($body, true);\n $jsonError = json_last_error();\n\n if ($jsonError !== JSON_ERROR_NONE) {\n logPayloadEvent('json_decode_error', array(\n 'request_id' => $requestId,\n 'json_error' => $jsonError,\n 'body_bytes' => strlen($body),\n 'body_sha256' => hash('sha256', $body),\n ));\n\n throw new UnexpectedValueException('response is not valid JSON');\n }\n\n if (!is_array($data)) {\n rejectPayload('valid JSON is not an object', $requestId, $body);\n }\n\n if (!array_key_exists('order_id', $data)\n || !is_string($data['order_id'])\n || $data['order_id'] === ''\n ) {\n rejectPayload('order_id is missing or has the wrong type', $requestId, $body);\n }\n\n return $data;\n}</code></pre>\n<p>Снимок <code>$jsonError</code> появляется сразу после декодирования. Это важно: следующая операция JSON может изменить состояние последней ошибки. В журнал попадают категория, код ошибки, идентификатор запроса, размер тела и SHA-256. Хеш помогает сопоставить одинаковые ответы без записи всего тела. Само тело может содержать токены, персональные данные или HTML от прокси, поэтому его нельзя бездумно отправлять в общий журнал.</p>\n<p>Проверка <code>is_array()</code> в этом примере не означает, что любой массив годится. При <code>json_decode($body, true)</code> и объект, и JSON-массив становятся PHP-массивом. Для данного договора после неё нужны проверка поля и его типа. Если endpoint должен возвращать список, проверяется список. Если он разрешает <code>null</code>, это правило фиксируется в контракте операции, а не выводится из поведения PHP.</p>\n<h2>Отрицательный путь нельзя пропускать</h2>\n<p>Положительный пример редко показывает проблему. Нужны ответы, которые похожи по результату, но требуют разных действий. HTML от reverse proxy нельзя считать JSON. <code>null</code> нельзя считать ошибкой только потому, что оно ложно в PHP. Пустой массив нельзя объявлять сбоем без знания смысла запроса. Объект с чужим набором полей нельзя принимать только потому, что декодер не сообщил об ошибке.</p>\n<ol><li>Проверить HTTP-статус до разбора тела. Для статуса 503 сохранить транспортную категорию и не выдавать ответ за успешный JSON.</li><li>Передать в декодер <code>{\"order_id\":\"A-17\"}</code>. Ожидается <code>JSON_ERROR_NONE</code> и массив с непустым идентификатором.</li><li>Передать <code><html>maintenance</html></code>. Ожидается <code>JSON_ERROR_SYNTAX</code> и событие <code>json_decode_error</code>.</li><li>Передать строку <code>null</code>. Разбор должен завершиться без ошибки, после чего проверка формы должна отклонить ответ для операции создания заказа.</li><li>Передать <code>[]</code>. Разбор должен завершиться без ошибки, но отсутствие <code>order_id</code> должно дать <code>contract_error</code>.</li><li>Отдельно описать операцию поиска, где <code>[]</code> является успешным результатом. Нельзя переносить правило создания заказа на другой endpoint.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта схема не проверяет, что удалённый сервис действительно вернул ожидаемый бизнес-результат. Синтаксически правильный JSON может содержать устаревший идентификатор, неверное состояние заказа или лишнее поле. Для этого нужны правила предметного контракта и проверка HTTP-семантики.</p>\n<p>Схема не защищает от повторной отправки операции. Если клиент не знает, был ли заказ создан до обрыва ответа, решение о повторе требует идемпотентности или отдельного запроса состояния. Нельзя превращать любую ошибку декодирования в автоматический retry. HTML от прокси и повреждённый ответ партнёра могут повторяться.</p>\n<p>В PHP 7.1 доступен код ошибки, но не современное исключение <code>JSON_THROW_ON_ERROR</code>. При обновлении PHP можно сократить ветку декодирования. Нельзя сокращать проверку обязательных полей. Флаг <code>true</code> у <code>json_decode()</code> также влияет на то, как код различает объект и список; это решение нужно закрепить в локальном контракте.</p>\n<h2>Критерий готовности</h2>\n<p>Изменение готово, если для пяти входов из порядка действий система выдаёт разные проверяемые результаты: транспортная ошибка, ошибка JSON, корректный <code>null</code> не того типа, пустой допустимый список и успешный объект с идентификатором. В журнале есть request id и категория, но нет полного чувствительного тела. В коде отсутствует проверка <code>if (!$data)</code> как единственный диагноз. Каждый endpoint явно описывает, допустимы ли <code>null</code>, пустой список и дополнительные поля.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.php.net/manual/en/function.json-decode.php\" target=\"_blank\" rel=\"noopener\">PHP Manual: json_decode</a> — возвращаемые значения, UTF-8, ассоциативные массивы и флаг <code>JSON_THROW_ON_ERROR</code>.</li><li><a href=\"https://www.php.net/manual/en/function.json-last-error.php\" target=\"_blank\" rel=\"noopener\">PHP Manual: json_last_error</a> — коды ошибки последней операции JSON.</li><li><a href=\"https://www.rfc-editor.org/rfc/rfc8259.html\" target=\"_blank\" rel=\"noopener\">RFC 8259: The JSON Data Interchange Syntax</a> — допустимые JSON-значения и синтаксис.</li></ul>"
|
||
}
|