edit full article archive to publication standard
Build and deploy / deploy (push) Successful in 18s

This commit is contained in:
2026-07-31 23:08:19 +03:00
parent 440c8721dc
commit 7c5b19c960
28 changed files with 583 additions and 39 deletions
+8 -5
View File
@@ -115,7 +115,7 @@ const practiceArticle = {
excerpt: 'Разбираю, как собрать один полезный диагностический факт при фатальной ошибке PHP: где работает set_error_handler, зачем нужен shutdown-обработчик и какие данные нельзя писать в лог.',
readingMinutes: 10,
contentHtml: [
paragraph('Интеграционный endpoint вернул 500, а в журнале осталась только дата и адрес скрипта. На следующий день партнёр повторяет запрос, но уже с другими данными, и причина исчезает. В такой ситуации не помогает ещё один <code>try/catch</code> вокруг вызова API: часть ошибок PHP до него не дойдёт. Вопрос этой заметки простой: как оставить один диагностический факт с операцией и местом падения, не превращая журнал в копию чужого запроса?'),
paragraph('Интеграционный endpoint вернул 500, а в журнале осталась только дата и адрес скрипта. На следующий день партнёр повторяет запрос, но уже с другими данными, и причина исчезает. В такой ситуации не помогает ещё один <code>try/catch</code> вокруг вызова API: часть ошибок PHP до него не дойдёт. Вопрос этой заметки простой: как оставить один диагностический факт с операцией и местом падения, не превращая журнал в копию чужого запроса? Цена ошибки — повторный разбор интеграции без исходных фактов.'),
heading('Почему одного set_error_handler недостаточно'),
paragraph('Первое, что обычно хочется сделать, — повесить <code>set_error_handler</code> и считать задачу закрытой. У функции есть граница: пользовательский обработчик не получает <code>E_ERROR</code>, <code>E_PARSE</code>, <code>E_CORE_ERROR</code> и <code>E_COMPILE_ERROR</code>. Он также не может увидеть ошибку, случившуюся до регистрации обработчика. Это не дефект функции, а условие, от которого надо строить диагностику.'),
paragraph('Поэтому я разделяю три случая. Обычное предупреждение попадает в обработчик ошибок. Непойманное исключение или <code>Error</code> в PHP 7 попадает в обработчик исключений. Для части фатальных ошибок остаётся функция завершения: PHP вызывает её после окончания скрипта или после <code>exit()</code>, а <code>error_get_last()</code> даёт тип, сообщение, файл и строку последней ошибки. Функция завершения не заменяет нормальную обработку исключений, но закрывает именно этот зазор.'),
@@ -243,7 +243,7 @@ const mechanismArticle = {
contentHtml: [
paragraph('После ночной выгрузки в логе стоит «запрос выполнен», потому что <code>curl_exec()</code> вернул строку. Утром выясняется, что строкой была HTML-страница с 403, а заказы не дошли. Ошибка в проверке не синтаксическая: код спросил cURL только о доставке ответа, а бизнес-код сделал вывод о результате всей операции. Разберём один вопрос: какой минимальный набор проверок отличает сетевой сбой, HTTP-отказ и рабочий ответ партнёра?'),
heading('У одного вызова три разных результата'),
paragraph('При включённом <code>CURLOPT_RETURNTRANSFER</code> функция <code>curl_exec()</code> возвращает тело ответа при успехе cURL и <code>false</code> при его ошибке. Проверять результат надо строгим сравнением: непустое тело может быть строкой <code>&quot;0&quot;</code>, которая в обычном условии ведёт себя как ложь. Главное здесь другое: статус 404 или 500 сам по себе не считается ошибкой cURL. Документация прямо предлагает читать HTTP-статус через <code>curl_getinfo()</code>.'),
paragraph('При включённом <code>CURLOPT_RETURNTRANSFER</code> функция <code>curl_exec()</code> возвращает тело ответа при успехе cURL и <code>false</code> при его ошибке. Проверять результат надо строгим сравнением: непустое тело может быть строкой <code>&quot;0&quot;</code>, которая в обычном условии ведёт себя как ложь. Главное здесь другое: статус 404 или 500 сам по себе не считается ошибкой cURL. Документация прямо предлагает читать HTTP-статус через <code>curl_getinfo()</code>. Цена ошибки — записать страницу отказа как успешный ответ и отправить дальше неверные данные.'),
paragraph('Отсюда порядок проверки. Сначала узнаём, состоялась ли передача: <code>$body === false</code>, <code>curl_errno()</code> и <code>curl_error()</code>. Затем читаем <code>http_code</code>, тип содержимого и время из <code>curl_getinfo()</code>. Только после этого разбираем тело как JSON или иной формат, который обещан договором с партнёром. Если смешать уровни, журнал начинает сообщать «ошибка API» и для DNS, и для 401, и для сломанного JSON.'),
figure('/assets/editorial/2018/curl-outcome-classifier.svg', 'Диаграмма классификации ответа cURL: false ведёт к транспортной ошибке; строка проверяется по HTTP-коду, затем по контракту тела', 'Положительный результат cURL означает, что библиотека получила ответ. Он ещё не означает, что HTTP-запрос и бизнес-операция завершились успешно.'),
heading('Что сохранять для каждого уровня'),
@@ -342,7 +342,7 @@ const fieldArticle = {
readingMinutes: 9,
contentHtml: [
paragraph('В обработчике ответа часто встречается одна строка: <code>if (!$data) { throw new Exception(&quot;bad response&quot;); }</code>. После неё невозможно понять, что случилось: партнёр вернул пустой список, честное <code>null</code>, число <code>0</code> или HTML вместо JSON. Ниже я оставляю пример в рамках PHP 7.1: в этой версии ещё нет <code>JSON_THROW_ON_ERROR</code>, поэтому после <code>json_decode()</code> нужно явно проверить состояние декодера.'),
paragraph('Главный вопрос здесь узкий: как отделить ошибку разбора JSON от корректного JSON, который не соответствует нашему договору? Ответ состоит из двух проверок подряд. Сначала сразу читаем <code>json_last_error()</code>. Только если там <code>JSON_ERROR_NONE</code>, проверяем тип и обязательные поля ответа.'),
paragraph('Главный вопрос здесь узкий: как отделить ошибку разбора JSON от корректного JSON, который не соответствует нашему договору? Ответ состоит из двух проверок подряд. Сначала сразу читаем <code>json_last_error()</code>. Только если там <code>JSON_ERROR_NONE</code>, проверяем тип и обязательные поля ответа. Цена ошибки — показать пользователю пустой результат там, где партнёр вернул повреждённый или чужой формат.'),
heading('Почему null не доказывает ошибку'),
paragraph('По RFC 8259 JSON-текстом может быть не только объект или массив: допустимы также строка, число, <code>false</code>, <code>true</code> и <code>null</code>. PHP отражает это напрямую: <code>json_decode(&quot;null&quot;)</code> возвращает <code>null</code>, но <code>null</code> возвращается и когда строку нельзя декодировать. Одна проверка на значение не различает эти случаи.'),
paragraph('То же происходит с пустыми коллекциями. После <code>json_decode(&quot;[]&quot;, true)</code> получится пустой массив, который в PHP является ложным в условии. Это может быть правильный ответ поиска: товаров нет. Но тот же <code>if (!$data)</code> назовёт его «битым JSON». Сначала нужно проверить синтаксис, затем форму данных, и только потом решать, допустим ли пустой результат для данной операции.'),
@@ -449,6 +449,7 @@ function decodeCreatedOrder($body, $requestId)
};
const revisions = [practiceArticle, mechanismArticle, fieldArticle];
export { revisions };
function plainText(content) {
return content
@@ -491,8 +492,10 @@ for (const revision of revisions) {
assertRevisionQuality(revision);
}
if (!process.argv.includes('--print-revisions')) {
if (!process.argv.includes('--print-revisions') && process.argv[1]?.endsWith('upgrade-2018-02.mjs')) {
throw new Error('Usage: node scripts/upgrade-2018-02.mjs --print-revisions');
}
process.stdout.write(JSON.stringify(revisions, null, 2) + '\n');
if (process.argv.includes('--print-revisions')) {
process.stdout.write(JSON.stringify(revisions, null, 2) + '\n');
}