8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 356,
|
||
"slug": "editorial-2018-02-mechanism-php-diagnostics",
|
||
"title": "PHP и cURL: как доказать, что интеграция действительно сработала",
|
||
"excerpt": "Строка от curl_exec() доказывает только получение ответа. Разбираем транспорт, HTTP-статус и контракт тела, чтобы не принять 403 или неверный JSON за успешную операцию.",
|
||
"contentHtml": "<p>Ночной обмен заказами завершился без исключения. Утром в локальной базе нет новых записей. В журнале стоит «запрос выполнен»: <code>curl_exec()</code> вернул строку. Позже выясняется, что строкой была HTML-страница с кодом 403. Код принял ответ сервера за результат операции. Цена ошибки — потерянная запись, повторная ручная обработка и риск отправить один заказ дважды.</p>\n<p>Чтобы назвать интеграцию успешной, нужно подтвердить три разных факта: передача состоялась, сервер вернул допустимый HTTP-статус, а тело соответствует договору. Каждый факт относится к своему слою. Если пропустить один слой, диагностика превращается в догадку и может запустить повтор там, где состояние уже изменилось.</p>\n<h2>Что именно сообщает curl_exec()</h2>\n<p>При включённом <code>CURLOPT_RETURNTRANSFER</code> функция возвращает результат передачи, а при ошибке cURL — <code>false</code>. В первом случае cURL мог получить и полезный ответ, и страницу ошибки. Значение <code>false</code> показывает ошибку выполнения передачи, например DNS, таймаут или TLS; оно не описывает ответ приложения партнёра.</p>\n<p>HTTP 401, 404 или 500 обычно не становятся ошибкой cURL. Сервер успел ответить, поэтому функция может вернуть тело. Статус нужно читать отдельно через <code>curl_getinfo()</code>. Проверка <code>if ($body)</code> смешивает ещё больше случаев: пустое тело, строку <code>\"0\"</code> и <code>false</code>. Сначала используйте строгое сравнение с <code>false</code>, затем проверяйте статус и формат тела.</p>\n<p>После статуса остаётся третий вопрос: что лежит в теле. Код 200 может содержать ожидаемый JSON, ошибочный JSON, HTML прокси или корректный JSON без обязательного идентификатора. Протокол завершился, но результат операции ещё не доказан.</p>\n<figure><img src=\"/assets/editorial/2018/curl-outcome-classifier.svg\" alt=\"Схема диагностики cURL: false ведёт к транспортной ошибке; строка проверяется по HTTP-коду и контракту тела\" /><figcaption>Положительный результат cURL означает, что библиотека получила ответ. Он ещё не означает, что HTTP-запрос и бизнес-операция завершились успешно.</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><code>$body === false</code></td><td>Транспорт или TLS</td><td><code>curl_errno</code>, <code>curl_error</code>, URL без секрета, время</td><td>Проверить DNS, сертификат, таймаут и доступность хоста</td></tr><tr><td>Есть тело, <code>http_code</code> 401 или 403</td><td>Авторизация или права</td><td>HTTP-код, операция, внешний ID, request ID</td><td>Проверить учётные данные и область доступа; не печатать токен</td></tr><tr><td>Есть тело, <code>http_code</code> 404</td><td>Адрес или версия API</td><td>HTTP-код и маршрут без query-параметров</td><td>Сверить путь, метод и версию endpoint</td></tr><tr><td>Есть тело, <code>http_code</code> 500</td><td>Ошибка удалённой стороны</td><td>HTTP-код, request ID, время и размер тела</td><td>Передать партнёру ID запроса; не повторять изменяющий запрос вслепую</td></tr><tr><td>Статус 2xx, но поле результата отсутствует</td><td>Нарушен контракт полезной нагрузки</td><td>Тип тела, JSON-ошибка и обязательные поля</td><td>Отклонить ответ и открыть разбор контракта</td></tr><tr><td>Таймаут после POST</td><td>Результат операции неизвестен</td><td>Идемпотентный ключ или запрос статуса операции</td><td>Не отправлять тот же POST автоматически без гарантии</td></tr></tbody></table></div>\n<h2>Минимальный клиент с раздельными ошибками</h2>\n<p>Ниже учебный пример для PHP с cURL. Он не задаёт универсальные таймауты и не заменяет правила конкретного API. Его задача — показать границы классификации. Для production-кода добавьте лимит размера ответа, нужный метод, авторизацию и логгер проекта.</p>\n<pre><code><?php\n\nfunction requestPartner($url, $requestId)\n{\n $handle = curl_init($url);\n\n curl_setopt_array($handle, array(\n CURLOPT_RETURNTRANSFER => true,\n CURLOPT_CONNECTTIMEOUT => 3,\n CURLOPT_TIMEOUT => 10,\n CURLOPT_HTTPHEADER => array(\n 'Accept: application/json',\n 'X-Request-Id: ' . $requestId,\n ),\n ));\n\n $body = curl_exec($handle);\n $curlErrno = curl_errno($handle);\n $curlError = curl_error($handle);\n $info = curl_getinfo($handle);\n curl_close($handle);\n\n if ($body === false) {\n throw new RuntimeException(json_encode(array(\n 'kind' => 'transport_error',\n 'request_id' => $requestId,\n 'curl_errno' => $curlErrno,\n 'curl_error' => $curlError,\n 'total_time' => $info['total_time'],\n )));\n }\n\n $status = (int) $info['http_code'];\n if ($status < 200 || $status >= 300) {\n throw new RuntimeException(json_encode(array(\n 'kind' => 'http_error',\n 'request_id' => $requestId,\n 'http_code' => $status,\n 'content_type' => $info['content_type'],\n 'body_bytes' => strlen($body),\n 'total_time' => $info['total_time'],\n )));\n }\n\n return array(\n 'body' => $body,\n 'content_type' => $info['content_type'],\n 'http_code' => $status,\n 'total_time' => $info['total_time'],\n );\n}</code></pre>\n<p>Снимайте сведения с handle до <code>curl_close()</code>. В диагностическую запись достаточно положить класс ошибки, request ID, статус, размер тела и время. URL нормализуйте: query-параметры могут содержать ключи, подписи или персональные данные. Тело не копируйте в общий журнал. Если расследование требует фрагмента, маскируйте его и ограничивайте отдельным безопасным контуром.</p>\n<h2>После 2xx проверьте контракт</h2>\n<p>Статус 2xx означает, что сервер успешно получил, понял и принял запрос на протокольном уровне. Он не говорит, что ответ подходит вашему коду. Например, операция создания заказа может требовать объект с непустым <code>order_id</code>. Ответ <code>{"accepted":true}</code> может быть валидным для другого endpoint, но не для этого. Код 202 вдобавок может означать только постановку работы в очередь.</p>\n<pre><code><?php\n\nfunction decodeCreatedOrder($body)\n{\n $data = json_decode($body, true);\n\n if (json_last_error() !== JSON_ERROR_NONE) {\n throw new UnexpectedValueException('Response is not valid JSON');\n }\n\n if (!is_array($data)\n || !isset($data['order_id'])\n || !is_string($data['order_id'])\n || $data['order_id'] === ''\n ) {\n throw new UnexpectedValueException('Response has no order_id');\n }\n\n return $data;\n}</code></pre>\n<p>Это тоже учебный пример. Он показывает порядок, а не готовую схему валидации для всех API. При JSON без обязательных полей <code>json_decode()</code> может завершиться успешно, поэтому одной проверки ошибки парсинга недостаточно. Для PHP 7.1 проверка <code>json_last_error()</code> нужна явно; в любом случае остаётся проверка типа ответа и обязательных полей.</p>\n<h2>Отрицательный путь: таймаут после записи</h2>\n<p>Таймаут оставляет результат неизвестным: клиент не получил финальный ответ и не может по одному факту таймаута решить, дошёл ли POST до партнёра. Повтор может создать дубль заказа. Поэтому нельзя ставить одинаковое действие «повторить» на все ошибки.</p>\n<p>Явный 500 даёт больше информации, чем таймаут, но тоже не доказывает отсутствие побочного эффекта: сервер мог изменить состояние, а затем вернуть ошибку. Для изменяющего запроса повтор допустим только при понятном условии: операция идемпотентна, запрос содержит ключ идемпотентности или API позволяет запросить состояние по внешнему ID. Иначе сохраните результат как требующий сверки до нового запроса.</p>\n<h2>Воспроизводимая проверка</h2>\n<p>Для проверки не нужен настоящий партнёр. Достаточно тестового endpoint или mock-сервиса, который по параметру возвращает 200 с корректным JSON, 200 с HTML или сломанным JSON, 401, 404, 500 и отдельный сценарий разрыва соединения. Сравнивайте не только текст исключения, но и поля записи: у каждого сценария должен быть свой <code>kind</code>, а у таймаута — статус «неизвестно», а не «отказ».</p>\n<ol><li>Найдите проверки <code>if (!$body)</code> и замените их на строгое сравнение <code>$body === false</code>.</li><li>Соберите <code>curl_errno()</code>, <code>curl_error()</code> и <code>curl_getinfo()</code> сразу после вызова, пока handle открыт.</li><li>Прогоните недоступный адрес и проверьте ветку <code>transport_error</code> с ненулевым кодом cURL.</li><li>Прогоните 401, 404 и 500; у них должна сработать ветка <code>http_error</code>, а не транспортная ошибка.</li><li>Прогоните 200 с корректным, но неожиданным телом и 200 с невалидным JSON. Убедитесь, что слой контракта не принимает ни один ответ автоматически.</li><li>Добавьте таймаут после POST и проверьте, что код не повторяет запись без ключа идемпотентности или запроса состояния. Для 500 применяйте то же правило, если договор не гарантирует отсутствие побочного эффекта.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта схема не определяет, какие статусы считать повторяемыми. Решение зависит от метода, договора и побочных эффектов операции. Она также не решает проблему синтаксической ошибки в коде до запуска, не заменяет мониторинг веб-сервера и не доказывает доступность всех маршрутов партнёра.</p>\n<p>Не отключайте проверку TLS ради «успешного» запроса. Не записывайте Authorization, cookie и полный ответ в общий лог. Не называйте 2xx бизнес-успехом, пока код не проверил контракт. Если API возвращает 202, отдельно выясните, означает ли он завершение действия или только постановку в очередь.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Диагностика готова, если локальный набор ответов различает транспортную ошибку, HTTP-отказ, неверный JSON, нарушение контракта, подтверждённый ответ и неизвестный результат после таймаута. Для каждой записи доступны request ID и время, секреты не попадают в журнал, а повтор изменяющего POST требует доказанной идемпотентности или проверки состояния. В таком виде журнал помогает найти владельца проблемы и не маскирует неопределённость под «запрос выполнен».</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.php.net/manual/en/function.curl-exec.php\" target=\"_blank\" rel=\"noopener\">PHP manual: curl_exec</a> — возвращаемое значение, строгое сравнение с false и то, что HTTP-коды ошибок не считаются ошибкой cURL</li><li><a href=\"https://www.php.net/manual/en/function.curl-getinfo.php\" target=\"_blank\" rel=\"noopener\">PHP manual: curl_getinfo</a> — сведения о последней передаче, включая http_code, content_type и total_time</li><li><a href=\"https://www.php.net/manual/en/function.curl-errno.php\" target=\"_blank\" rel=\"noopener\">PHP manual: curl_errno</a> — код последней ошибки cURL и ноль при отсутствии ошибки</li><li><a href=\"https://www.php.net/manual/en/function.json-decode.php\" target=\"_blank\" rel=\"noopener\">PHP manual: json_decode</a> — преобразование JSON в значение PHP и режимы обработки ошибок</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/rfc9110.html#name-status-codes\" target=\"_blank\" rel=\"noopener\">RFC 9110, Status Codes</a> — семантика классов статус-кодов HTTP и различие между 2xx, 4xx и 5xx</li></ul>"
|
||
}
|