Работа с HTTP API в PHP: как делать правильно
Голый file_get_contents на боевом проекте рано или поздно повесит скрипт на минуту из-за отсутствия таймаута. Разбираем, как обращаться к внешним API в PHP так, чтобы падения зависимости не роняли ваш код.

Вы дёргаете внешний платёжный шлюз через file_get_contents($url). В обычный день всё работает. Потом у провайдера случается сетевой сбой, ответ не приходит, и ваш PHP-процесс висит на дефолтном таймауте — а он для сокетов в PHP по умолчанию 60 секунд (default_socket_timeout). За это время воркеры FPM заканчиваются, и падает уже весь сайт, а не одна интеграция. Разберём, как обращаться к HTTP API правильно: с таймаутами, повторами, обработкой ошибок и без слепого доверия к чужому серверу.
Статья про исходящие запросы к чужим REST/JSON API из PHP 8.x. Приём вебхуков, проектирование собственного API и OAuth-потоки — отдельные темы, здесь их не трогаем.
Задача и почему очевидное решение подводит
Нужно из PHP получить данные по HTTP: курс валют, статус заказа у маркетплейса, ответ от LLM-провайдера. Очевидное решение — одна строка:
$json = file_get_contents('https://api.example.com/rate');
$data = json_decode($json, true);
Что здесь ломается на боевом:
- Нет таймаута. Если сервер отвечает медленно, вы ждёте до
default_socket_timeout. Приallow_url_fopenв проде это прямой путь к исчерпанию воркеров. - Нет доступа к HTTP-коду. На 404 или 500
file_get_contentsвернётfalseи кинет warning, а тело ошибки вы не увидите. - Нет заголовков и тела для POST без ручной сборки
stream_context_create— код быстро превращается в кашу. json_decodeвернётnullи на пустой строке, и на битом JSON — вы не отличите одно от другого без проверкиjson_last_error().
Три способа делать запрос
1. Голый cURL — когда нельзя ставить зависимости
Расширение cURL идёт с большинством сборок PHP и даёт полный контроль. Минимально корректный GET-запрос выглядит так:
<?php
// PHP 8.x, расширение ext-curl
$ch = curl_init('https://api.example.com/rate');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true, // вернуть тело как строку, а не печатать
CURLOPT_TIMEOUT => 10, // общий таймаут запроса, секунды
CURLOPT_CONNECTTIMEOUT => 5, // таймаут на установку соединения
CURLOPT_HTTPHEADER => ['Accept: application/json'],
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno !== 0) {
throw new RuntimeException('Сетевая ошибка cURL: ' . $errno);
}
if ($status >= 400) {
throw new RuntimeException("HTTP $status, тело: $body");
}
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
Обратите внимание на JSON_THROW_ON_ERROR — с PHP 7.3 json_decode кидает JsonException при битом ответе вместо тихого null. Это тот случай, когда явная ошибка лучше молчаливой.
2. Guzzle — стандарт для приложений
Guzzle — самый распространённый HTTP-клиент в PHP, ставится через composer require guzzlehttp/guzzle. Он реализует PSR-7 (сообщения) и PSR-18 (клиент), поэтому код не привязан жёстко к самой библиотеке.
<?php
// guzzlehttp/guzzle 7.x
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
$client = new Client([
'base_uri' => 'https://api.example.com',
'timeout' => 10, // общий таймаут
'connect_timeout' => 5,
'http_errors' => true, // кидать исключение на 4xx/5xx
]);
try {
$response = $client->post('/orders', [
'json' => ['sku' => 'ABC-123', 'qty' => 2], // сериализует и ставит Content-Type
'headers' => ['Authorization' => 'Bearer YOUR_API_TOKEN'],
]);
// getBody() возвращает поток; при JSON удобнее так:
$data = json_decode((string) $response->getBody(), true, 512, JSON_THROW_ON_ERROR);
} catch (RequestException $e) {
// у RequestException есть ответ, если сервер что-то вернул
$status = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 0;
throw new RuntimeException("Запрос к API упал, HTTP $status", 0, $e);
}
Опция json сама кодирует массив и выставляет Content-Type: application/json — руками собирать заголовок не нужно.
3. Symfony HttpClient — если уже в экосистеме Symfony
Если проект на Symfony или Laravel (в Laravel есть свой фасад Http поверх Guzzle), берите то, что уже в стеке, а не тащите вторую библиотеку. Symfony HttpClient (symfony/http-client) умеет ленивые ответы: исключение по HTTP-коду выбрасывается только когда вы обращаетесь к телу или статусу, а не в момент запроса.
Что выбрать
| Критерий | Голый cURL | Guzzle | Symfony HttpClient |
|---|---|---|---|
| Зависимости через Composer | Нет | Да | Да |
| PSR-18 совместимость | Нет | Да | Да |
| Читаемость при POST/JSON | Низкая | Высокая | Высокая |
| Параллельные запросы | Вручную через curl_multi | Через промисы | Из коробки, ленивые ответы |
| Когда брать | Скрипт без Composer, минимальный вес | Отдельное приложение, много интеграций | Уже используете Symfony |
Практический критерий простой: есть Composer и приложение живёт долго — Guzzle или клиент вашего фреймворка. Одноразовый скрипт на голом хостинге без прав на установку пакетов — cURL.
Таймауты, повторы и идемпотентность
Внешний API отвечает не всегда и не всегда быстро. Три правила, которые экономят инциденты.
Запрос без таймаута — это не запрос, а мина замедленного действия: она сработает в тот день, когда у провайдера ляжет сеть, а не когда вы к этому готовы.
Ставьте оба таймаута. connect_timeout отдельно от общего: медленное соединение и медленный ответ — разные проблемы. Разумная отправная точка для синхронного запроса из веб-процесса — 3–5 секунд на соединение и 10 на весь запрос, дальше настраивайте по факту.
Повторяйте только безопасные запросы. GET можно повторить смело. POST, создающий заказ или списывающий деньги, повторять опасно: вы создадите два заказа. Если API поддерживает ключ идемпотентности (часто это заголовок вроде Idempotency-Key с вашим UUID) — используйте его, тогда повтор безопасен. Точное имя заголовка смотрите в документации конкретного API.
Делайте паузы между повторами. Мгновенный повтор в цикле добьёт уже перегруженный сервер. Экспоненциальная задержка (1 с, 2 с, 4 с) щадит и провайдера, и ваши воркеры. В Guzzle это делается через middleware Middleware::retry; конкретную сигнатуру колбэков сверьте в актуальной документации Guzzle 7.x, она различается между минорными версиями.
Типичные ошибки
- Логировать токен целиком. Заголовок
Authorizationв логах — это утечка. Маскируйте: пишите тип и последние 4 символа, не весь ключ. - Отключать проверку сертификата.
CURLOPT_SSL_VERIFYPEER => false"чтобы заработало" открывает MITM. Если сертификат не проходит — обновите пакетca-certificatesна сервере, а не отключайте проверку. - Считать 200 гарантией успеха. Часть API отдаёт 200 с телом
{"error": ...}. Проверяйте не только HTTP-код, но и бизнес-статус в теле. - Складывать секреты в код. Токены — в переменные окружения или защищённое хранилище, не в репозиторий.
- Игнорировать заголовки лимитов. Многие API возвращают
X-RateLimit-RemainingиRetry-After. Читайте их и притормаживайте до того, как получите 429.
Что делать дальше
Вынесите работу с каждым внешним API в отдельный класс-обёртку: он инкапсулирует базовый URL, таймауты, авторизацию и разбор ошибок, а остальной код вызывает методы вроде getOrder($id), не зная про HTTP. Так вы смените клиент или добавите повторы в одном месте. Дальше — навесьте на этот класс интерфейс PSR-18, чтобы в тестах подменять реальные запросы моком. И заведите метрику: сколько запросов к каждому провайдеру, сколько ошибок, какой процентиль времени ответа — именно эти цифры скажут, когда пора ставить кеш или очередь между вами и чужим API.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: PHP Manual: cURL Functions, Guzzle Documentation
Частые вопросы
У меня PHP 7.4, JSON_THROW_ON_ERROR сработает?
На хостинге отключён allow_url_fopen, а cURL нет — что делать?
Запрос иногда падает с ошибкой 'SSL certificate problem' — как чинить?
Guzzle не кидает исключение на 500, хотя http_errors включён — почему?
Можно ли делать несколько запросов к API параллельно?
Какой таймаут ставить, чтобы не рвать медленные, но валидные ответы?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.