2

I have Sentry keeping track of uncaught exceptions in my PHP application, and I noticed a peculiar uncaught exception from PDO. The code looks like this:

   /**
    * @return boolean TRUE if the connection to the database worked; FALSE otherwise. 
    */
   public function verifyDatabase() {
       try{ 
           $this->pdo->query('SELECT 1');
           return true;
       }
       catch (\PDOException $e) {
           echo 'Lost connection to database: ' . $e->getMessage() . PHP_EOL;
           return false;
       }
    }

This should catch errors like "MySQL server has gone away", and it indeed works on my development machine. However, Sentry recently recorded this error:

ErrorException PDO::query(): MySQL server has gone away

According to sentry, this was thrown by the $this->pdo->query('SELECT 1'); statement above. Errors like this should have been caught by the try/catch. Why is PDO throwing an ErrorException rather than a PDOException?

404 Not Found
  • 3,635
  • 2
  • 28
  • 34
  • 1
    because it is. period. I never bother with PDOException which will be rolled up in catching `Exception`, and I also catch `Throwable` to avoid (sometimes) a dreaded WSOD or 500 on a critical API call. – YvesLeBorg Mar 12 '19 at 18:38

2 Answers2

1

I can't reproduce an ErrorException.

$pdo = new PDO('mysql:host=127.0.0.1;dbname=test', ..., ...);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

sleep(20); // during this sleep, I stop my MySQL Server instance.

$result = $pdo->query("SELECT 1");

Output:

Warning: PDO::query(): MySQL server has gone away

Warning: PDO::query(): Error reading result set's header

Fatal error: Uncaught PDOException: SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
Stack trace:
#0 /Users/bkarwin/Documents/SO/pdo.php(8): PDO->query('SELECT 1')

This shows it throws a PDOException when the server has gone away, not an ErrorException.

Tested with MySQL 5.6.37 and PHP 7.1.23.

I wonder if the code you show in your question is actually the code that is deployed and throwing the exception to Sentry. Perhaps you have some code like:

   catch (\PDOException $e) {
       throw ErrorException($e->getMessage());
   }

Either in your verifyDatabase() function, or else in the code that calls verifyDatabase(). In other words, what does your app do when verifyDatabase() returns false?

Bill Karwin
  • 538,548
  • 86
  • 673
  • 828
  • This is a worker script that's supposed to just run indefinitely. when `verifyDatabase()` returns false, the script just exists and is restarted by the supervisor process so it can reconnect to the database. – 404 Not Found Mar 12 '19 at 19:20
  • Okay, and does the code that's currently deployed have any case of throwing an ErrorException? Don't assume -- go check it. – Bill Karwin Mar 12 '19 at 19:53
0

Okay, I think I've figured it out. It appears that this is related to a bug in which the PDO MySQL driver emits warnings even when they are supposed to be disabled (See also: this answer). I believe Sentry is then catching these as errors.

I was finally able to replicate, and solve this, by modifying Bill Karwin's test script:

// Initialize the Sentry reporting client
$ravenClient = new \Raven_Client(SENTRY_KEY);
$ravenClient->install();

echo 'Connecting...' . PHP_EOL;

$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

echo 'Connected. Waiting...' . PHP_EOL;

sleep(20); // during this sleep, I stop my MySQL Server instance.

echo 'Querying...' . PHP_EOL;

try {
    $result = $pdo->query("SELECT 1");
}
catch(\PDOException $e) {
    echo 'Caught PDOException ' . $e->getMessage() . PHP_EOL;
}

This will print the following:

Connecting...
Connected. Waiting...
Querying...
PHP Warning:  PDO::query(): MySQL server has gone away in /home/xxx/src/test.php on line 37
PHP Stack trace:
PHP   1. {main}() /home/xxx/src/test.php:0
PHP   2. PDO->query() /home/xxx/src/test.php:37
Caught PDOException SQLSTATE[HY000]: General error: 2006 MySQL server has gone away
Done.

And Sentry will record an ErrorException on the query() line.

I was able to solve this issue by implementing the "solution" posted by jferrer on the PHP bug report.

// Convert NOTICE, WARNING, ... in Exceptions
$convertErrorToException = function ($level, $message, $file, $line){
    throw new ErrorException($message, 0, $level, $file, $line);
};

// The $previousErrorHandler will contain Sentry's handler
$previousErrorHandler = set_error_handler($convertErrorToException);

try {
    $result = $pdo->query("SELECT 1");
}
catch(\PDOException $e) {
    echo 'Caught PDOException ' . $e->getMessage() . PHP_EOL;
}
catch(\ErrorException $e) {
    echo 'Caught ErrorException ' . $e->getMessage() . PHP_EOL;
}

// Restore Sentry as the default handler
set_error_handler($previousErrorHandler);

This results in just the ErrorException being thrown and caught:

Connecting...
Connected. Waiting...
Querying...
Caught ErrorException PDO::query(): MySQL server has gone away
Done.
404 Not Found
  • 3,635
  • 2
  • 28
  • 34