get_headers

Fetches the HTTP response headers sent by a server for the given URL.

Signature
get_headers(string $url, bool $associative = false, ?resource $context = null): array|false
  • Added in PHP 5.0

Parameters

ParameterTypeDescription
$urlstringThe target URL to request.
$associativeoptionalbool= falseWhen true, the response is parsed and the array is keyed by header name instead of by position.
$contextoptional?resource= nullA stream context created with stream_context_create(), or null to use the default context.

Return value

array|falseAn indexed array of raw header lines, or an associative array keyed by header name when associative is true, or false on failure.

Errors

Does not throw. It emits an E_WARNING and returns false when the request fails, and a ValueError if url contains a null byte.

Changelog

  • PHP 8.0.0The associative parameter changed type from int to bool.
  • PHP 7.1.0The context parameter was added.

get_headers() performs an HTTP request and returns just the response headers. It is the quickest way to answer questions like "does this URL exist", "what status does it return", "where does it redirect to" and "what content type does it claim".

It sends GET, not HEAD

By default the server sends a full response body which PHP then discards, so checking a large file wastes bandwidth. Pass a stream context with 'method' => 'HEAD' to avoid that — though some servers handle HEAD badly, so fall back to GET if the result looks wrong.

Redirects produce several status lines

When allow_url_fopen-style redirect following kicks in, the returned array contains the headers of every hop. In associative mode a repeated header such as Location becomes a nested array. The final status is the last numeric-keyed entry, not $headers[0]; reading index 0 gives you the first redirect's status instead.

Requirements and safety

The function needs allow_url_fopen enabled. Never pass a user-supplied URL to it without validating the host first: it will happily request internal addresses, which is a server-side request forgery. For anything beyond a quick check — timeouts, retries, real error handling — use cURL.

Example

Example · php
<?php
$headers = get_headers('https://example.com/', true);

if ($headers === false) {
    echo "Request failed\n";
} else {
    // With redirects, the status lines are the numeric keys.
    $statuses = array_filter($headers, 'is_int', ARRAY_FILTER_USE_KEY);
    echo 'final status: ' . end($statuses) . "\n";

    echo 'content-type: ' . ($headers['Content-Type'] ?? 'unknown') . "\n";
}

When to use it

  • Check that an external link still resolves before publishing it
  • Read the Content-Length and Content-Type of a remote file before downloading it
  • Trace where a shortened URL ultimately redirects
  • Verify a CDN is returning the caching headers you configured

Frequently asked questions

Does get_headers send a HEAD request?

No, it sends GET by default and discards the body. Pass a stream context with 'method' => 'HEAD' to avoid downloading the response body.

How do I get just the HTTP status code?

Take the last element whose key is numeric, since redirects add one status line per hop, and parse the number out of it with explode or a regular expression.

Why does get_headers return false?

Usually because allow_url_fopen is disabled, DNS or the connection failed, or the scheme is unsupported. It also emits an E_WARNING describing the cause.

More examples

A cheap HEAD request via a stream context

The context switches the method to HEAD so the server sends headers only, and adds a five-second timeout and a User-Agent that some hosts require. Content-Length then tells you the file size without downloading a single byte of it.

Example · php
<?php
$context = stream_context_create([
    'http' => [
        'method'  => 'HEAD',
        'timeout' => 5,
        'header'  => "User-Agent: link-checker/1.0\r\n",
    ],
]);

$headers = get_headers('https://example.com/large.zip', true, $context);
var_dump($headers['Content-Length'] ?? null);

Extracting a status code and following the redirect chain

In indexed mode every hop appears in order, so scanning for lines beginning with HTTP/ and Location: reconstructs the whole redirect chain. A 301 followed by a 200 shows up as two HTTP/ lines with one Location between them.

Example · php
<?php
$headers = get_headers('https://example.com/old-page');
if ($headers === false) {
    exit("unreachable\n");
}

foreach ($headers as $line) {
    if (str_starts_with($line, 'HTTP/')) {
        echo "hop: $line\n";
    } elseif (stripos($line, 'Location:') === 0) {
        echo "  -> " . trim(substr($line, 9)) . "\n";
    }
}

Validating the URL before requesting it

Both calls return false before any request is made: the first because file:// is not an allowed scheme, the second because 127.0.0.1 falls in the reserved 127.0.0.0/8 loopback range. Note that FILTER_FLAG_NO_PRIV_RANGE alone would not catch it — that flag only rejects 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and fc00::/7, so FILTER_FLAG_NO_RES_RANGE must be combined with it. Without checks like these, a user-supplied URL lets an attacker probe your internal network.

Example · php
<?php
function safeHeaders(string $url): array|false {
    $parts = parse_url($url);
    if (!in_array($parts['scheme'] ?? '', ['http', 'https'], true)) {
        return false;
    }
    $ip = gethostbyname($parts['host'] ?? '');
    if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
        return false;
    }
    return @get_headers($url, true);
}

var_dump(safeHeaders('file:///etc/passwd'));
var_dump(safeHeaders('http://127.0.0.1/admin'));

Discussion

  • Be the first to comment on this lesson.