How to Fix a file_get_contents() Warning Instead of the PHP Error

I’ve run into this problem more times than I care to admit: I call file_get_contents() on a URL, something goes wrong with SSL, and PHP gives me both a warning and an error.

For example, hitting an endpoint with a mismatched SSL certificate:

file_get_contents('https://invalid-certificate.com');

produces output like this:

PHP warning: Peer certificate CN='*.invalid-certificate.net' did not match expected CN='invalid-certificate.com'
PHP error: file_get_contents(https://invalid-certificate.com): failed to open stream: operation failed

That warning is gold, because it tells me exactly what went wrong. But if I wrap the call with @ and check error_get_last(), all I get back is the vague final error:

$response = @file_get_contents('https://invalid-certificate.com');

if ($response === false) {
    $error = error_get_last();
    throw new \Exception($error['message']);
}

Which only says:

file_get_contents(https://invalid-certificate.com): failed to open stream: operation failed

Not very helpful. So the question is: how do I promote the warning into something I can actually catch and use?

My Approach Use an Error Handler

The trick is to install a temporary set_error_handler() before the call and turn PHP warnings into exceptions. That way, instead of PHP printing the warning and moving on, I can intercept it and throw it.

Turn Warning Into Exception

This is the simplest way. Any warning becomes an ErrorException, which I can catch:

function get_or_throw(string $url, $context = null): string
{
    set_error_handler(function ($severity, $message, $file, $line) {
        if ($severity & (E_WARNING | E_USER_WARNING | E_NOTICE | E_USER_NOTICE)) {
            throw new \ErrorException($message, 0, $severity, $file, $line);
        }
        return false;
    });

    try {
        $data = file_get_contents($url, false, $context);
        if ($data === false) {
            throw new \RuntimeException("file_get_contents failed for {$url}");
        }
        return $data;
    } finally {
        restore_error_handler();
    }
}

try {
    $response = get_or_throw('https://invalid-certificate.com');
} catch (\ErrorException $e) {
    echo $e->getMessage();
}

Now the exception message will be the original SSL warning, like:

Peer certificate CN='*.invalid-certificate.net' did not match expected CN='invalid-certificate.com'

Exactly what I want.

Collect All Warning

Sometimes I don’t just want the first warning I’d like to gather all of them and throw one combined message. Here’s a pattern for that:

function get_or_throw_collect(string $url, $context = null): string
{
    $messages = [];

    set_error_handler(function ($severity, $message) use (&$messages) {
        if ($severity & (E_WARNING | E_USER_WARNING | E_NOTICE | E_USER_NOTICE)) {
            $messages[] = $message;
            return true;
        }
        return false;
    });

    try {
        $data = file_get_contents($url, false, $context);
        if ($data === false) {
            $msg = $messages
                ? "HTTP/SSL warnings:\n- " . implode("\n- ", $messages)
                : "file_get_contents failed for {$url}";
            throw new \RuntimeException($msg);
        }
        return $data;
    } finally {
        restore_error_handler();
    }
}

try {
    $response = get_or_throw_collect('https://invalid-certificate.com');
} catch (\RuntimeException $e) {
    echo $e->getMessage();
}

With this, I’ll see both the SSL CN mismatch and the final “failed to open stream” message, nicely bundled together.

Bonus Why I Sometimes Switch to cURL

As much as I like squeezing file_get_contents() into production, sometimes I just want more structured error reporting. That’s when I reach for cURL:

function curl_get_or_throw(string $url): string
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => false,
        CURLOPT_SSL_VERIFYPEER => true,
        CURLOPT_SSL_VERIFYHOST => 2,
    ]);

    $data = curl_exec($ch);

    if ($data === false) {
        $err  = curl_error($ch);
        $code = curl_errno($ch);
        curl_close($ch);
        throw new \RuntimeException("cURL error {$code}: {$err}");
    }

    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    curl_close($ch);

    if ($status >= 400) {
        throw new \RuntimeException("HTTP {$status} for {$url}");
    }

    return $data;
}

This way I get an explicit error code and message, and I can handle SSL issues more predictably.

Final Thought

I began this journey frustrated that error_get_last() only gave me vague, unhelpful messages. The breakthrough came when I learned to catch warnings directly with set_error_handler(). Now, depending on the project, I either convert warnings into exceptions for simplicity or collect them all for richer context. And when I need even deeper insights, I turn to cURL for structured error handling. My takeaway is simple: don’t settle for cryptic PHP errors catch the warnings, make them useful, and turn SSL failures into actionable feedback.

Related blog posts