Как в PHP cURL получить заголовки ответа HTTP и тело ответа в одном запросе?
Всем привет! Я пишу парсер на PHP с использованием cURL. Мне нужно получить не только содержимое страницы (тело ответа), но и HTTP-заголовки (например, Content-Type, Status Code, Location при редиректах).
Проблема в том, что когда я использую curl_exec(), я получаю только тело ответа. Если же я включаю опцию CURLOPT_HEADER => true, то в результат выводится всё вместе — и заголовки, и тело, и их приходится разбирать вручную через explode(), что очень неудобно.
Вот мой текущий код:
$ch = curl_init('https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
curl_close($ch);
// Приходится костыльно разделять
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $header_size);
$body = substr($response, $header_size);
Этот способ работает, но он кажется мне грязным. К тому же, если сайт использует перенаправления или chunked encoding, разбор может сломаться. Есть ли встроенный способ в PHP cURL получить заголовки и тело как отдельные структуры в одном запросе, без ручного парсинга?
Важно: я не хочу делать два запроса — один для HEAD, другой для GET. Нужно именно одним запросом получить и заголовки, и тело.
<<Используйте CURLOPT_HEADERFUNCTION для чистого разделения
Да, в PHP cURL есть элегантное решение вашей проблемы. Вместо того чтобы включать заголовки в вывод и потом их парсить, используйте callback-функцию через опцию CURLOPT_HEADERFUNCTION. Эта опция позволяет обрабатывать каждый заголовок отдельно, не смешивая их с телом ответа.
Вот правильный способ:
<?php
$ch = curl_init('https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$headers = [];
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header_line) use (&$headers) {
$headers[] = $header_line;
return strlen($header_line);
});
$body = curl_exec($ch);
curl_close($ch);
// Теперь $headers содержит массив строк заголовков
// $body содержит только тело ответа
print_r($headers);
echo "---\n";
echo $body;
?>
Как это работает
- Callback-функция вызывается для каждой строки заголовка (включая пустую строку-разделитель).
- Вы можете сохранять заголовки в массив, ассоциативный массив или обрабатывать их на лету.
- Callback обязан вернуть длину строки (
strlen($header_line)), иначе cURL будет считать, что данные не обработаны. - Тело ответа остаётся в
curl_exec()нетронутым.
Продвинутый пример с парсингом в ассоциативный массив
<?php
$ch = curl_init('https://httpbin.org/response-headers?freeform=hello');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$headers = [];
$status_code = null;
curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header_line) use (&$headers, &$status_code) {
// Парсим статусную строку (первая строка)
if (preg_match('/^HTTP\/\d+\.\d+ (\d+)/', $header_line, $matches)) {
$status_code = (int)$matches[1];
return strlen($header_line);
}
// Парсим обычные заголовки
$len = strlen($header_line);
if (strpos($header_line, ':') !== false) {
list($name, $value) = explode(':', $header_line, 2);
$headers[trim($name)] = trim($value);
}
return $len;
});
$body = curl_exec($ch);
curl_close($ch);
echo "Status Code: $status_code\n";
echo "Content-Type: " . ($headers['Content-Type'] ?? 'N/A') . "\n";
echo "Body: $body\n";
?>
Дополнительные советы
- Редиректы: Если включён
CURLOPT_FOLLOWLOCATION, callback будет вызван для заголовков каждого редиректа. Вы можете отслеживать последовательность редиректов, добавляя их в отдельный массив. - Производительность: Этот способ быстрее, чем парсинг всего ответа, так как заголовки обрабатываются потоково, без копирования больших строк.
- Ошибки: Всегда проверяйте
curl_error()после выполнения.
Это стандартный и рекомендуемый подход в PHP для получения заголовков и тела в одном запросе. Он чище, надёжнее и легче поддерживается.
<<Альтернатива: использование CURLOPT_HEADER + CURLINFO_HEADER_SIZE
Если вам не хочется писать callback-функцию или вы работаете с очень старым PHP (до 5.1.3, где CURLOPT_HEADERFUNCTION ещё не было), можно использовать классический метод с CURLOPT_HEADER и curl_getinfo(), но с важными улучшениями:
<?php
$ch = curl_init('https://example.com');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HEADER, true);
$response = curl_exec($ch);
// ВАЖНО: получаем размер заголовков ДО закрытия дескриптора
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$header_raw = substr($response, 0, $header_size);
$body = substr($response, $header_size);
// Разбираем заголовки построчно
$header_lines = explode("\r\n", $header_raw);
$headers = [];
foreach ($header_lines as $line) {
if (strpos($line, ':') !== false) {
list($key, $value) = explode(':', $line, 2);
$headers[trim($key)] = trim($value);
} elseif (preg_match('/^HTTP\/\d+\.\d+ (\d+)/', $line, $m)) {
$headers['Status-Code'] = $m[1];
}
}
print_r($headers);
echo $body;
?>
Минусы этого подхода:
- При редиректах (
CURLOPT_FOLLOWLOCATION)CURLINFO_HEADER_SIZEвозвращает размер только последних заголовков. Все предыдущие заголовки редиректов попадут в тело, что сломает парсинг. - Неэффективно при больших ответах — весь ответ хранится в памяти как одна строка.
Когда это оправдано: Для простых GET-запросов без редиректов, когда вы уверены в структуре ответа. Для продакшена всё же рекомендую первый вариант с callback.