About the Author

Chris Shiflett

Hi, I’m Chris: web craftsman, community leader, husband, father, and partner at Fictive Kin.


PHP Session Debugging

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:

<?php
 
function read($id)
{
    global $sess_save_path;
 
    $sess_file = "$sess_save_path/sess_$id";
    return (string) @file_get_contents($sess_file);
}
 
?>

There are a few questions that can be asked:

  • What is the value of $id?
  • Are we able to read from $sess_file?
  • What data is returned?

With a few modifications, you can answer these questions:

<?php
 
function read($id)
{
    global $sess_save_path;
 
    echo "<p>Session identifier is: {$id}</p>";
 
    $sess_file = "{$sess_save_path}/sess_{$id}";
 
    if (is_readable($sess_file)) {
        $data = (string) file_get_contents($sess_file);
        echo "<p>Can read from file: {$sess_file}</p>";
        echo "<p>Data is: {$data}</p>";
    } else {
        echo "<p>Cannot read from file: {$sess_file}</p>";
    }
 
    return $data;
}
 
?>

There are a few problems with this approach:

  • Although you may not mind, depending upon where you're debugging and who has access, each echo is vulnerable to XSS. (In older versions of PHP, $id was not restricted to any particular format. This is no longer the case.) If you do mind, use htmlentities() or htmlspecialchars().
  • Using echo means you can't debug Ajax requests, API requests, etc.
  • Although open(), read(), and gc() are executed (in this order) before the output stream is closed, this is not true of write() and close().

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:

  • Logs have a recorded history. The current page can't always tell the whole story of what went wrong.
  • Logs capture all requests, not just the one for the current page. This is especially important for Ajax and APIs.

For logging, I use error_log():

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

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

[25 Mar 2011 12:34:56][shiflett.org] [/]
 
Session::start()
    $_COOKIE['PHPSESSID'] [412e11d5317627e48a4b0615c84b9a8f]
Session::open()
Session::read()
    $id [412e11d5317627e48a4b0615c84b9a8f]
    $data [count|i:1;]
Session::write()
    $id [412e11d5317627e48a4b0615c84b9a8f]
    $data [count|i:2;]
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:

  • Unlike unserialize() (which won't work, because the format is slightly different), session_decode() assigns the result to $_SESSION. You'll have to preserve $_SESSION yourself if you want to use this for debugging. I wish it had the same optional argument that print_r() has, so that we could opt to have it return the result instead of assign it. Anyone want to help us out? :-)
  • Because session_decode() assigns the result to $_SESSION, it won't work in read().

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

<?php
 
ini_set('session.serialize_handler', 'wddx');
 
?>

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.

About this post

PHP Session Debugging was posted on Fri, 25 Mar 2011. If you liked it, follow me on Twitter or share:

10 comments

1.Ferenc Kovacs said:

thanks for the blogpost, I've just created the feature request for you:

http://bugs.php.net/bug.php?id=54383

cheers :)

Tyrael

Fri, 25 Mar 2011 at 15:26:26 GMT Link


2.Joel Caton said:

Thanks Chris,

I'm going to put this to use.

- Joel

Fri, 25 Mar 2011 at 16:05:57 GMT Link


3.Simon Wood said:

Your opening line "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." I don't agree with. Nearly all developers I meet know how to override the default PHP session code with session_set_save_handler().

This is because a lot of us deal with clustered webservers and can't use the default file based sessions. Maybe the group of developers I know are not normal, but most of them roll their own session code to use a DB or Memcached instead of the default files. Because they roll their own session code they know how it works and how to debug it.

Just my 2p worth.

Fri, 25 Mar 2011 at 16:55:30 GMT Link


4.Chris Shiflett said:

Hi Simon,

Nearly all developers I meet know how to override the default PHP session code with session_set_save_handler().

That's the beauty of words like many. I could have tried to actually guess how many PHP developers don't understand how sessions work, but that's irrelevant, so I didn't.

Fri, 25 Mar 2011 at 17:31:07 GMT Link


5.Till Klamp├Ąckel said:

This is some code I use to manually decode:

<?php
 
public static function decode($data)
 
{
 
    $vars = preg_split(
 
        '/([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff^|]*)\|/',
 
        $data,
 
        -1,
 
        PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
 
    );
    $result = array();
 
    for ($i=0; $i<=(count($vars)/2); $i=$i+2) {
 
        if (!isset($vars[$i])) {
 
            continue;
 
        }
 
        $key = $vars[$i];
 
        $val = $vars[$i+1];
        $result[$key] = unserialize($val);
 
    }
 
    return $result;
 
}

I think I found it in the PHP manual a few years back.

Sat, 26 Mar 2011 at 02:06:53 GMT Link


6.Cristiano Diniz da Silva said:

A good example of how this works is the Debug Kit plugin of CakePHP that among a bunch of things allows you to access the Session via an interface and it is integrated with cake session handler that allows you to easily setup the Session through config.

Anyway, I'm just setting cake as an example, but what Chris points out here is definitely a tool that you should build or have.

Mon, 28 Mar 2011 at 13:29:14 GMT Link


7.David Jones said:

Nice article, i'll be honest i tend to work with the basic php session functionality or my sessions are handled by a certain framework that i regularly use. So i don't very often go any further than the session_start() and $_SESSION but its a nice insight into how to debug sessions and backtrack your way to a problem.

Tue, 29 Mar 2011 at 13:58:24 GMT Link


8.Keith Humm said:

Hmm, while there are a couple of good techniques in here, I have to interject.

Is there any reason why you're not advocating the use of a debugger here, such as xdebug or Zend? Strikes me that it's by far the best way to capture and analyse this data...

Thu, 31 Mar 2011 at 20:53:17 GMT Link


9.Mahalie Stackpole said:

Fantastic! I am a PHP novice and really appreciate the big-picture lens and a practical example of how to understand and debug sessions.

Mon, 25 Apr 2011 at 21:26:03 GMT Link


10.Chris Shiflett said:

This is a comment via email from Arpad Ray, who doesn't use Twitter. Yet. :-)

Sadly it doesn't seem possible to adapt session_encode / session_decode to operate on arbitrary input because of the way the session serialization API was first written. (Serializers work on $_SESSION directly.) It may be feasible for PHP 6 but it would be a big internal BC break. It may be unwise to call session_write_close() from a session handler's destructor because other objects it uses might be destructed earlier. Shutdown functions are run before any destructors are called so they're always a safe option. Finally I'm hopeful that my patch to introduce object oriented session handlers will be adopted for PHP 5.4 - that would make debugging a lot easier since you could transparently extend whatever handler is currently in use, and still call the parent implementation.

Wed, 25 Jan 2012 at 14:49:13 GMT Link


Hello! What’s your name?

Want to comment? Please connect with Twitter to join the discussion.