PHP Session Debugging

25 Mar 2011

For many PHP developers, calling session_start() and using $_SESSION for stuff you want to persist from page to page is all there is to know about sessions. This is understandable, because PHP's native session support is so simple and reliable. But, what if something goes wrong?

Understanding how sessions work is your best tool when it's time to debug a problem. There's really no substitute. I wrote The Truth about Sessions way back in 2003, and it focuses on an outdated technique, but I think it's still worth reading to learn a bit more about how sessions work. (Just read the first few sections.)

Nothing beats getting your hands dirty, and session_set_save_handler() can help. If you're not already using this function, the manual has an example that mimics the native behavior. Storing Sessions in a Database, another old article, shows you how to store sessions in MySQL. For now, it's just important that you're using a custom session handler, because this gives you more insight into what's really going on.

If you're having trouble getting session_set_save_handler() working, don't worry. Figuring out what's wrong is a valuable learning exercise. If you're using the example from the manual, note that it depends upon configuration directives like session.save_path being set correctly.

When something goes wrong, it's almost impossible to determine the exact cause of the problem without some record of what happened. Was the session identifier sent in the request? Was it the same one that was sent in the previous request? Was the session data saved properly? I could come up with dozens of questions like these, because there are a lot of things that can fail, but the point is that we need answers.

Let's look at the read() function from the example in the manual:

  1. <?php
  2.  
  3. function read($id)
  4. {
  5.     global $sess_save_path;
  6.  
  7.     $sess_file = "$sess_save_path/sess_$id";
  8.     return (string) @file_get_contents($sess_file);
  9. }
  10.  
  11. ?>

There are a few questions that can be asked:

With a few modifications, you can answer these questions:

  1. <?php
  2.  
  3. function read($id)
  4. {
  5.     global $sess_save_path;
  6.  
  7.     echo "<p>Session identifier is: {$id}</p>";
  8.  
  9.     $sess_file = "{$sess_save_path}/sess_{$id}";
  10.  
  11.     if (is_readable($sess_file)) {
  12.         $data = (string) file_get_contents($sess_file);
  13.         echo "<p>Can read from file: {$sess_file}</p>";
  14.         echo "<p>Data is: {$data}</p>";
  15.     } else {
  16.         echo "<p>Cannot read from file: {$sess_file}</p>";
  17.     }
  18.  
  19.     return $data;
  20. }
  21.  
  22. ?>

There are a few problems with this approach:

The manual includes the following note:

The write handler is not executed until after the output stream is closed. Thus, output from debugging statements in the write handler will never be seen in the browser. If debugging output is necessary, it is suggested that the debug output be written to a file instead.

This is good advice, but if you really want to use echo, you can force the session to finish its business by using session_write_close(). Call this from __destruct() if you're using a class, or use register_shutdown_function(). I frequently use session_write_close() for this reason, because I like to have debugging output on every page. It's super convenient.

As convenient as it is, I also log, for a couple of reasons:

For logging, I use error_log():

  1. <?php
  2.  
  3. error_log($message, 3, '/tmp/session.log');
  4.  
  5. ?>

As a starting point, here's an example of what I typically log:

  1. [25 Mar 2011 12:34:56][shiflett.org] [/]
  2.  
  3. Session::start()
  4.     $_COOKIE['PHPSESSID'] [412e11d5317627e48a4b0615c84b9a8f]
  5. Session::open()
  6. Session::read()
  7.     $id [412e11d5317627e48a4b0615c84b9a8f]
  8.     $data [count|i:1;]
  9. Session::write()
  10.     $id [412e11d5317627e48a4b0615c84b9a8f]
  11.     $data [count|i:2;]
  12. Session::close()

If you want to make $data a little easier to read, you have to use session_decode(). This is inconvenient for a couple of reasons:

You can write your own session_decode() in PHP (see the user contributed notes for some examples) or use WDDX:

  1. <?php
  2.  
  3. ini_set('session.serialize_handler', 'wddx');
  4.  
  5. ?>

If you use WDDX, you can use wddx_unserialize to unserialize the session data.

Please keep in mind that debugging in production requires extra consideration that I've not covered. If possible, do your session debugging elsewhere.

I'd love to go into more depth, but in the spirit of Ideas of March, I'm posting this short introduction, and hopefully it's helpful. If you have any questions or tips of your own, please leave a comment.