Avatar ·

Как в 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.