¿Puede PHP cURL recuperar encabezados de respuesta Y cuerpo en una sola solicitud?

314

¿Hay alguna forma de obtener los encabezados y el cuerpo para una solicitud cURL usando PHP? Encontré que esta opción:

curl_setopt($ch, CURLOPT_HEADER, true);

va a devolver el cuerpo más los encabezados , pero luego necesito analizarlo para obtener el cuerpo. ¿Hay alguna forma de obtener ambos de una manera más utilizable (y segura)?

Tenga en cuenta que para "solicitud única" me refiero a evitar emitir una solicitud HEAD antes de GET / POST.

gremo
fuente
3
Hay una solución integrada para esto, vea esta respuesta: stackoverflow.com/a/25118032/1334485 (agregó este comentario 'porque esta publicación todavía tiene muchas vistas)
Skacc
Mira este bonito comentario: secure.php.net/manual/en/book.curl.php#117138
user956584
Me dijeron que mi pregunta era un duplicado de esta pregunta. Si no es un duplicado, ¿alguien puede volver a abrirlo? stackoverflow.com/questions/43770246/… En mi pregunta, tengo un requisito concreto de usar un método que devuelva un objeto con encabezados y cuerpo separados y no una sola cadena.
1.21 gigavatios

Respuestas:

466

Una solución a esto se publicó en los comentarios de la documentación de PHP: http://www.php.net/manual/en/function.curl-exec.php#80442

Ejemplo de código:

$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);
// ...

$response = curl_exec($ch);

// Then, after your curl_exec call:
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($response, 0, $header_size);
$body = substr($response, $header_size);

Advertencia: como se señala en los comentarios a continuación, esto puede no ser confiable cuando se usa con servidores proxy o cuando se manejan ciertos tipos de redireccionamientos. La respuesta de @ Geoffrey puede manejar esto de manera más confiable.

iblue
fuente
22
También puede list($header, $body) = explode("\r\n\r\n", $response, 2)hacerlo, pero esto puede demorar un poco más, dependiendo del tamaño de su solicitud.
iblue
43
esta es una mala solución porque si usa un servidor proxy y su servidor proxy (por ejemplo, fiddler) agrega sus propios encabezados a la respuesta: estos encabezados rompieron todos los desplazamientos y debería usarlos list($header, $body) = explode("\r\n\r\n", $response, 2)como única variante de trabajo
msangel
55
@msangel Su solución no funciona cuando hay múltiples encabezados en la respuesta, como cuando el servidor realiza una redirección 302. ¿Alguna sugerencia?
Nate
44
@Nate, sí, lo sé. AFAIK, pero solo hay un posible encabezado adicional: con el código 100(Continuar). Para este encabezado, puede ir con la opción de solicitud que define correctamente: curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); deshabilitando el envío de esta respuesta de encabezado. En cuanto a 302, esto no debería suceder, porque el encabezado 302 es redirigido, no espera cuerpo, sin embargo, lo sé, a veces los servidores envían algún cuerpo con 302respuesta, pero de todos modos los navegadores lo ignorarán, hasta ahora, ¿por qué curl debería manejar esto? )
msangel
55
CURLOPT_VERBOSEestá destinado a generar información de proceso STDERR(puede molestar en CLI) y para el problema discutido es inútil.
hejdav
205

Muchas de las otras soluciones que ofrece este hilo no lo están haciendo correctamente.

  • La división \r\n\r\nno es confiable cuando CURLOPT_FOLLOWLOCATIONestá encendido o cuando el servidor responde con un código 100.
  • No todos los servidores cumplen con los estándares y transmiten solo \npara nuevas líneas.
  • Detectar el tamaño de los encabezados a través de CURLINFO_HEADER_SIZEtampoco es siempre confiable, especialmente cuando se usan proxies o en algunos de los mismos escenarios de redireccionamiento.

El método más correcto es usar CURLOPT_HEADERFUNCTION.

Aquí hay un método muy limpio para realizar esto usando cierres de PHP. También convierte todos los encabezados a minúsculas para un manejo consistente en servidores y versiones HTTP.

Esta versión retendrá encabezados duplicados

Esto cumple con RFC822 y RFC2616, por favor no sugiera ediciones para hacer uso de las mb_funciones de cadena, ¡es incorrecto!

$ch = curl_init();
$headers = [];
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

// this function is called by curl for each header received
curl_setopt($ch, CURLOPT_HEADERFUNCTION,
  function($curl, $header) use (&$headers)
  {
    $len = strlen($header);
    $header = explode(':', $header, 2);
    if (count($header) < 2) // ignore invalid headers
      return $len;

    $headers[strtolower(trim($header[0]))][] = trim($header[1]);

    return $len;
  }
);

$data = curl_exec($ch);
print_r($headers);
Geoffrey
fuente
12
OMI, esta es la mejor respuesta en este hilo y soluciona problemas con redirecciones que ocurrieron con otras respuestas. Es mejor leer la documentación de CURLOPT_HEADERFUNCTION para comprender cómo funciona y posibles problemas. También he realizado algunas mejoras en la respuesta para ayudar a otros.
Simon East
Genial, he actualizado la respuesta para atender encabezados duplicados. En el futuro, no vuelva a formatear el código a lo que cree que debería ser. Esto está escrito de manera de dejar en claro dónde están los límites de la función de cierre.
Geoffrey
@ Geoffrey ¿Es $headers = [];válido el php?
thealexbaron
66
@thealexbaron Sí, es a partir de PHP 5.4, ver: php.net/manual/en/migration54.new-features.php
Geoffrey
44
Esta respuesta está muy subestimada para un enfoque tan ordenado y compatible con RFC. Esto debe hacerse una respuesta adhesiva y trasladarse a la parte superior. Solo desearía que hubiera un enfoque más rápido para obtener el valor de un encabezado deseado en lugar de analizar primero todos los encabezados.
Fr0zenFyr
114

Curl tiene una opción integrada para esto, llamada CURLOPT_HEADERFUNCTION. El valor de esta opción debe ser el nombre de una función de devolución de llamada. Curl pasará el encabezado (¡y solo el encabezado!) A esta función de devolución de llamada, línea por línea (por lo que se llamará a la función para cada línea de encabezado, comenzando desde la parte superior de la sección del encabezado). Su función de devolución de llamada puede hacer cualquier cosa con ella (y debe devolver el número de bytes de la línea dada). Aquí hay un código de trabajo probado:

function HandleHeaderLine( $curl, $header_line ) {
    echo "<br>YEAH: ".$header_line; // or do whatever
    return strlen($header_line);
}


$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://www.google.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_HEADERFUNCTION, "HandleHeaderLine");
$body = curl_exec($ch); 

Lo anterior funciona con todo, diferentes protocolos y proxys también, y no necesita preocuparse por el tamaño del encabezado, o establecer muchas opciones de rizo diferentes.

PD: para manejar las líneas de encabezado con un método de objeto, haga esto:

curl_setopt($ch, CURLOPT_HEADERFUNCTION, array(&$object, 'methodName'))
Skacc
fuente
Como nota, se llama a la función de devolución de llamada para cada encabezado y parece que no se recortan. Puede usar una variable global para contener todos los encabezados o puede usar una función anónima para la devolución de llamada y usar una variable local (local para el ámbito primario, no la función anónima).
MV.
2
@ MV Gracias, sí, por "línea por línea" quise decir "cada encabezado". Edité mi respuesta para mayor claridad. Para obtener la sección de encabezado completa (también conocida como todos los encabezados), también puede usar un método de objeto para la devolución de llamada para que una propiedad de objeto pueda contenerlos a todos.
Skacc
8
Esta es la mejor respuesta de la OMI. No causa problemas con múltiples "\ r \ n \ r \ n" cuando se usa CURLOPT_FOLLOWLOCATION y supongo que no se verá afectado por encabezados adicionales de proxies.
Rafał G.
Funcionó muy bien para mí, también consulte stackoverflow.com/questions/6482068/… en caso de problemas
RHH
1
Sí, este es el mejor enfoque, sin embargo, la respuesta de @ Geoffrey lo hace más limpio al usar una función anónima sin necesidad de variables globales y demás.
Simon East
39

¿es esto lo que estás buscando?

curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:'));
$response = curl_exec($ch); 
list($header, $body) = explode("\r\n\r\n", $response, 2);
usuario1031143
fuente
8
Esto funciona normalmente, excepto cuando hay un HTTP / 1.1 100 Continuar seguido de un descanso y luego HTTP / 1.1 200 OK. Yo iría con el otro método.
ghostfly
1
Consulte la respuesta seleccionada de stackoverflow.com/questions/14459704/… antes de implementar algo como esto. w3.org/Protocols/rfc2616/rfc2616-sec14.html (14.20) A server that does not understand or is unable to comply with any of the expectation values in the Expect field of a request MUST respond with appropriate error status. The server MUST respond with a 417 (Expectation Failed) status if any of the expectations cannot be met or, if there are other problems with the request, some other 4xx status.
Alrik
Este método también falla en los redireccionamientos 302 cuando curl está configurado para seguir el encabezado de ubicación.
Simon East
10

Solo configure las opciones:

  • CURLOPT_HEADER, 0

  • CURLOPT_RETURNTRANSFER, 1

y use curl_getinfo con CURLINFO_HTTP_CODE (o no opte param y tendrá una matriz asociativa con toda la información que desee)

Más en: http://php.net/manual/fr/function.curl-getinfo.php

Cyril H.
fuente
55
Esto no parece devolverle los encabezados de respuesta. O al menos no hay forma de recuperarlos usando curl_getinfo().
Simon East
8

Si desea específicamente el Content-Type, hay una opción especial de cURL para recuperarlo:

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$response = curl_exec($ch);
$content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
pr1001
fuente
El OP preguntó si hay una manera de recuperar los encabezados, no un encabezado específico, esto no responde a la pregunta del OP.
Geoffrey
2
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);

$parts = explode("\r\n\r\nHTTP/", $response);
$parts = (count($parts) > 1 ? 'HTTP/' : '').array_pop($parts);
list($headers, $body) = explode("\r\n\r\n", $parts, 2);

Funciona con HTTP/1.1 100 Continueantes de otros encabezados.

Si necesita trabajar con servidores con errores que envían solo LF en lugar de CRLF como saltos de línea, puede usar preg_splitlo siguiente:

curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, 1);
curl_setopt($ch, CURLOPT_HEADER, 1);

$parts = preg_split("@\r?\n\r?\nHTTP/@u", $response);
$parts = (count($parts) > 1 ? 'HTTP/' : '').array_pop($parts);
list($headers, $body) = preg_split("@\r?\n\r?\n@u", $parts, 2);
Enyby
fuente
¿No debería $parts = explode("\r\n\r\nHTTP/", $response);tener el tercer parámetro para explotar como 2?
user4271704
@ user4271704 No. Permite buscar el último mensaje HTTP. HTTP/1.1 100 ContinuePuede aparecer muchas veces.
Enyby
Pero él dice algo más: stackoverflow.com/questions/9183178/... ¿ cuál de ustedes tiene razón?
user4271704
HTTP/1.1 100 ContinuePuede aparecer muchas veces. Él ve el caso si aparece solo una vez, pero está mal en el caso común. Por ejemplo, para HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK...\r\n\r\n...su código no funciona correctamente
Enyby
1
La división en \ r \ n no es confiable, algunos servidores no se ajustan a las especificaciones HTTP y solo enviarán un \ n. El estándar RFC establece que las aplicaciones deben ignorar \ r y dividirse en \ n para mayor confiabilidad.
Geoffrey
1

Mi camino es

$response = curl_exec($ch);
$x = explode("\r\n\r\n", $v, 3);
$header=http_parse_headers($x[0]);
if ($header=['Response Code']==100){ //use the other "header"
    $header=http_parse_headers($x[1]);
    $body=$x[2];
}else{
    $body=$x[1];
}

Si es necesario, aplique un bucle for y elimine el límite de explosión.

Roy
fuente
1

Aquí está mi contribución al debate ... Esto devuelve una matriz única con los datos separados y los encabezados enumerados. Esto funciona sobre la base de que CURL devolverá datos de un fragmento de encabezado [línea en blanco]

curl_setopt($ch, CURLOPT_HEADER, 1); // we need this to get headers back
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_VERBOSE, true);

// $output contains the output string
$output = curl_exec($ch);

$lines = explode("\n",$output);

$out = array();
$headers = true;

foreach ($lines as $l){
    $l = trim($l);

    if ($headers && !empty($l)){
        if (strpos($l,'HTTP') !== false){
            $p = explode(' ',$l);
            $out['Headers']['Status'] = trim($p[1]);
        } else {
            $p = explode(':',$l);
            $out['Headers'][$p[0]] = trim($p[1]);
        }
    } elseif (!empty($l)) {
        $out['Data'] = $l;
    }

    if (empty($l)){
        $headers = false;
    }
}
Antonio
fuente
0

El problema con muchas respuestas aquí es que "\r\n\r\n"puede aparecer legítimamente en el cuerpo del html, por lo que no puede estar seguro de que está dividiendo los encabezados correctamente.

Parece que la única forma de almacenar encabezados por separado con una llamada curl_execes mediante una devolución de llamada como se sugiere anteriormente en https://stackoverflow.com/a/25118032/3326494

Y luego (para obtener de manera confiable) solo el cuerpo de la solicitud, deberá pasar el valor del Content-Lengthencabezado substr()como un valor de inicio negativo.

mal
fuente
1
Puede aparecer legítimamente, pero su respuesta es incorrecta. Content-Length no tiene que estar presente en una respuesta HTTP. El método correcto para analizar manualmente los encabezados es buscar la primera instancia de \ r \ n (o \ n \ n). Esto podría hacerse simplemente limitando la explosión para devolver solo dos elementos, es decir: list($head, $body) = explode("\r\n\r\n", $response, 2);sin embargo, CURL ya lo hace por usted si usacurl_setopt($ch, CURLOPT_HEADERFUNCTION, $myFunction);
Geoffrey
-1

En caso de que no pueda / no use CURLOPT_HEADERFUNCTIONu otras soluciones;

$nextCheck = function($body) {
    return ($body && strpos($body, 'HTTP/') === 0);
};

[$headers, $body] = explode("\r\n\r\n", $result, 2);
if ($nextCheck($body)) {
    do {
        [$headers, $body] = explode("\r\n\r\n", $body, 2);
    } while ($nextCheck($body));
}
K-Gun
fuente
-2

Devuelve encabezados de respuesta con un parámetro de referencia:

<?php
$data=array('device_token'=>'5641c5b10751c49c07ceb4',
            'content'=>'测试测试test'
           );
$rtn=curl_to_host('POST', 'http://test.com/send_by_device_token', array(), $data, $resp_headers);
echo $rtn;
var_export($resp_headers);

function curl_to_host($method, $url, $headers, $data, &$resp_headers)
         {$ch=curl_init($url);
          curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $GLOBALS['POST_TO_HOST.LINE_TIMEOUT']?$GLOBALS['POST_TO_HOST.LINE_TIMEOUT']:5);
          curl_setopt($ch, CURLOPT_TIMEOUT, $GLOBALS['POST_TO_HOST.TOTAL_TIMEOUT']?$GLOBALS['POST_TO_HOST.TOTAL_TIMEOUT']:20);
          curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
          curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
          curl_setopt($ch, CURLOPT_HEADER, 1);

          if ($method=='POST')
             {curl_setopt($ch, CURLOPT_POST, true);
              curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
             }
          foreach ($headers as $k=>$v)
                  {$headers[$k]=str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $k)))).': '.$v;
                  }
          curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
          $rtn=curl_exec($ch);
          curl_close($ch);

          $rtn=explode("\r\n\r\nHTTP/", $rtn, 2);    //to deal with "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK...\r\n\r\n..." header
          $rtn=(count($rtn)>1 ? 'HTTP/' : '').array_pop($rtn);
          list($str_resp_headers, $rtn)=explode("\r\n\r\n", $rtn, 2);

          $str_resp_headers=explode("\r\n", $str_resp_headers);
          array_shift($str_resp_headers);    //get rid of "HTTP/1.1 200 OK"
          $resp_headers=array();
          foreach ($str_resp_headers as $k=>$v)
                  {$v=explode(': ', $v, 2);
                   $resp_headers[$v[0]]=$v[1];
                  }

          return $rtn;
         }
?>
diyism
fuente
¿Estás seguro de que $rtn=explode("\r\n\r\nHTTP/", $rtn, 2);es correcto? ¿No debería eliminarse el tercer parámetro de explosión?
user4271704
@ user4271704, el tercer parámetro es tratar con el encabezado "HTTP / 1.1 100 Continue \ r \ n \ r \ nHTTP / 1.1 200 OK ... \ r \ n \ r \ n ..."
diyism
Pero él dijo algo más: stackoverflow.com/questions/9183178/... ¿ cuál de ustedes tiene razón?
user4271704
@ user4271704 el enlace al que te refieres también usa: explode("\r\n\r\n", $parts, 2); así que ambos son correctos.
Cyborg
-5

Si realmente no necesitas usar curl;

$body = file_get_contents('http://example.com');
var_export($http_response_header);
var_export($body);

Que salidas

array (
  0 => 'HTTP/1.0 200 OK',
  1 => 'Accept-Ranges: bytes',
  2 => 'Cache-Control: max-age=604800',
  3 => 'Content-Type: text/html',
  4 => 'Date: Tue, 24 Feb 2015 20:37:13 GMT',
  5 => 'Etag: "359670651"',
  6 => 'Expires: Tue, 03 Mar 2015 20:37:13 GMT',
  7 => 'Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT',
  8 => 'Server: ECS (cpm/F9D5)',
  9 => 'X-Cache: HIT',
  10 => 'x-ec-custom-error: 1',
  11 => 'Content-Length: 1270',
  12 => 'Connection: close',
)'<!doctype html>
<html>
<head>
    <title>Example Domain</title>...

Ver http://php.net/manual/en/reserved.variables.httpresponseheader.php

Bevan
fuente
16
uhm, tampoco necesitas PHP, pero de eso se trata la pregunta ...
Hans Z.