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.