13

I have made an oauth signed request to a REST API and have the response headers in an array like so:

[0] => HTTP/1.1 200 OK
[1] => Cache-Control: private
[2] => Transfer-Encoding: chunked
[3] => Content-Type: text/html; charset=utf-8
[4] => Content-Location: https://***
[5] => Server: Microsoft-IIS/7.0
[6] => Set-Cookie: ASP.NET_SessionId=***; path=/; HttpOnly
[7] => X-AspNetMvc-Version: 2.0
[8] => oauth_token: ***
[9] => oauth_token_secret: ***
[10] => X-AspNet-Version: 4.0.30319
[11] => X-Powered-By: ASP.NET
[12] => Date: Sat, 15 Sep 2012 02:01:15 GMT

I am trying to figure out how to parse the headers for easy retrieval of items such as the HTTP status code, Content-Location, oauth_token, and oauth_token_secret?

pgtips
  • 1,328
  • 6
  • 24
  • 43
  • It will be simpler / more direct to parse the whole text block with a single regex function call, then call `array_combine()` to form associative elements from the two capture groups (as described in the dupe target). – mickmackusa Sep 27 '22 at 23:27

7 Answers7

14

You'll need to iterate the array and check stripos() to find the header you're looking for. In most cases, you then explode() on : (limiting to 2 resultant parts), but the HTTP response code will require you to explode on the spaces.

// Get any header except the HTTP response...
function getResponseHeader($header, $response) {
  foreach ($response as $key => $r) {
     // Match the header name up to ':', compare lower case
     if (stripos($r, $header . ':') === 0) {
        list($headername, $headervalue) = explode(":", $r, 2);
        return trim($headervalue);
     }
  }
}
// example:
echo getResponseHeader("Content-Type");
// text/html; charset=utf-8

// Get the HTTP response code
foreach ($response as $key => $r) {
  if (stripos($r, 'HTTP/') === 0) {
    list(,$code, $status) = explode(' ', $r, 3);
    echo "Code: $code, Status: $status";
    break;
  }
}
Michael Berkowski
  • 267,341
  • 46
  • 444
  • 390
  • 2
    No, you CANNOT do it like this. This will blindly return headers which dont exist but contain the field name in their value. E.g. getResponseHeader("Session") will return the cookie. – Phil Oct 06 '16 at 20:41
  • Response code assumes "HTTP/1.1". Also contains a bug where only the first word of the status is fetched. What about "HTTP/1.1 404 Not Found". – Phil Oct 06 '16 at 20:43
  • @Phil_1984_ This is true. A left-anchored match of the string is necessary to prevent the match in value issue, and slightly more intelligent parsing for the status code with an `explode()` limit. I'll modify these later when I have time (also the function definition doesn't have enough arguments). You're always free to suggest edits or post a new answer. – Michael Berkowski Oct 06 '16 at 21:02
  • please note that some headers have the same header name like `Set-Cookie` and this function will return only the first header of them. – Accountant م Jul 13 '19 at 02:40
  • Good answer, still accurate with just one problem. Need to consider other HTTP protocol version as now we can have first header looks like "HTTP/2 200". So for all be aware of it in your parsing! It's not limited to `stripos($r, 'HTTP/1.1')` better use `stripos($r, 'HTTP/')` – Jacek Rosłan Dec 18 '19 at 14:07
  • @JacekRosłan Excellent addition - I'll modify it above, if you have sufficient rep/privileges, feel free to propose edits to existing answers and include justification. The community will review them. – Michael Berkowski Dec 18 '19 at 15:59
9

It seems that the only header withou a : is the HTTP version and status. Do an array_shift to extract that, iterate through the others creating an array like so:

$parsedHeaders = array();
foreach ($headers as $header) {
    if (! preg_match('/^([^:]+):(.*)$/', $header, $output)) continue;
    $parsedArray[$output[1]] = $output[2];
}

ps: untested.

— edit —

enjoy ;)

/**
 * Parse a set of HTTP headers
 *
 * @param array The php headers to be parsed
 * @param [string] The name of the header to be retrieved
 * @return A header value if a header is passed;
 *         An array with all the headers otherwise
 */
function parseHeaders(array $headers, $header = null)
{
    $output = array();

    if ('HTTP' === substr($headers[0], 0, 4)) {
        list(, $output['status'], $output['status_text']) = explode(' ', $headers[0]);
        unset($headers[0]);
    }

    foreach ($headers as $v) {
        $h = preg_split('/:\s*/', $v);
        $output[strtolower($h[0])] = $h[1];
    }

    if (null !== $header) {
        if (isset($output[strtolower($header)])) {
            return $output[strtolower($header)];
        }

        return;
    }

    return $output;
}
mbomb007
  • 3,788
  • 3
  • 39
  • 68
Felds Liscia
  • 339
  • 1
  • 10
  • Your first regex needs delimiters (`/`): `if (! preg_match('/^([^:]):(.*)$/', $header, $output)) continue;` – mbomb007 Jan 07 '19 at 23:05
2

Short answer if you have pecl_http: http://php.net/manual/it/function.explode.php

Slightly longer answer:

$header = "...";
$parsed = array_map(function($x) { return array_map("trim", explode(":", $x, 2)); }, array_filter(array_map("trim", explode("\n", $header))));
lethalman
  • 1,976
  • 1
  • 14
  • 18
2

I ended up with this solution which uses regex to find all the keys and values in the header combined with some array mutation from https://stackoverflow.com/a/43004994/271351 to get the regex matches into an associative array. This isn't 100% appropriate for the problem asked here since it takes in a string, but joining an array of strings to get a single string would work as a precursor to this. My case had to deal with raw headers, thus this solution.

preg_match_all('/^([^:\n]*): ?(.*)$/m', $header, $headers, PREG_SET_ORDER);
$headers = array_merge(...array_map(function ($set) {
    return array($set[1] => trim($set[2]));
}, $headers));

This yields an associative array of the headers. If the first line of the headers is included as input (e.g. GET / HTTP/1.1), this will ignore it for the output.

cjbarth
  • 4,189
  • 6
  • 43
  • 62
1

best way without http_parse_headers();

function strHeaders2Hash($r) {
    $o = array();
    $r = substr($r, stripos($r, "\r\n"));
    $r = explode("\r\n", $r);
    foreach ($r as $h) {
        list($v, $val) = explode(": ", $h);
        if ($v == null) continue;
        $o[$v] = $val;
    }
    return $o;
}
Puffy
  • 401
  • 6
  • 13
0

It looks like you're using get_headers function, if so, use the second parameter of the this function which replaces the numerical values for the output array keys and replaces them with string keys, check out the manual for get_headers function.

a small example would be:

<?php
    $output = get_headers('http://google.com', 1);
    print_r($output);

will produce something like the following array:

Array
(
    [0] => HTTP/1.0 301 Moved Permanently
    [Location] => http://www.google.com/
    [Content-Type] => Array
        (
            [0] => text/html; charset=UTF-8
            [1] => text/html; charset=ISO-8859-1
        )

    [Date] => Array
        (
            [0] => Tue, 24 Sep 2013 11:57:10 GMT
            [1] => Tue, 24 Sep 2013 11:57:11 GMT
        )

    [Expires] => Array
        (
            [0] => Thu, 24 Oct 2013 11:57:10 GMT
            [1] => -1
        )

    [Cache-Control] => Array
        (
            [0] => public, max-age=2592000
            [1] => private, max-age=0
        )

    [Server] => Array
        (
            [0] => gws
            [1] => gws
        )

    [Content-Length] => 219
    [X-XSS-Protection] => Array
        (
            [0] => 1; mode=block
            [1] => 1; mode=block
        )

    [X-Frame-Options] => Array
        (
            [0] => SAMEORIGIN
            [1] => SAMEORIGIN
        )

    [Alternate-Protocol] => Array
        (
            [0] => 80:quic
            [1] => 80:quic
        )

    [1] => HTTP/1.0 200 OK
    [Set-Cookie] => Array
        (
            [0] => PREF=ID=58c8f706594fae17:FF=0:TM=1380023831:LM=1380023831:S=_ehOnNWODZqIarXn; expires=Thu, 24-Sep-2015 11:57:11 GMT; path=/; domain=.google.com
            [1] => NID=67=L85IlJW5yG4l9Suyf1LwKMUTcVHyGv4u9tuuMlBH4pfT1syOJvspcgRJ9uTde1xLTDhI2QcOG_fuJY3sfhw49mayT5WdMHnGeMyhh3SgFTRYVF0RAtBXXmjyDFzMqPKu; expires=Wed, 26-Mar-2014 11:57:11 GMT; path=/; domain=.google.com; HttpOnly
        )

    [P3P] => CP="This is not a P3P policy! See http://www.google.com/support/accounts/bin/answer.py?hl=en&answer=151657 for more info."
)
Ma'moon Al-Akash
  • 4,445
  • 1
  • 20
  • 16
0

If you want to be extra-safe, use Symfony HTTP Foundation:

composer require symfony/http-foundation

use Symfony\Component\HttpFoundation\Request;

$request = Request::createFromGlobals();

// retrieves an HTTP request header, with normalized, lowercase keys
$request->headers->get('host');
$request->headers->get('content-type');

If you'd rather not have that dependency, here's an example that I've put together to find out if the Cache-Control header has the no-cache value, for instance:

/**
*  [
*    0 => 'Cache-Control: no-cache, no-store, no-validate',
*    1 => 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0',
*  ]
*/
$headers = headers_list();

foreach ( $headers as $header_string ) {
     /*
     * Regex Samples:
     * "Foo: Bar"
     * "Foo-Bar: Baz-Bar:1"
     *
     * Matches:
     * "Foo"
     * "Foo-Bar"
     */
    preg_match( '#^.+?(?=:)#', $header_string, $key );

    if ( empty( $key ) ) {
        continue;
    }

    $key   = strtolower( reset( $key ) );
    $value = strtolower( ltrim( strtolower( $header_string ), $key . ':' ) );

    if ( $key == 'cache-control' ) {
        if ( strpos( $value, 'no-cache' ) !== false || strpos( $value, 'no-store' ) !== false ) {
            $nocache = true;
        }
    }
}
Lucas Bustamante
  • 15,821
  • 7
  • 92
  • 86