PHP Session Debugging
25 Mar 2011For 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, usehtmlentities()
orhtmlspecialchars()
. - Using
echo
means you can't debug Ajax requests, API requests, etc. - Although
open()
,read()
, andgc()
are executed (in this order) before the output stream is closed, this is not true ofwrite()
andclose()
.
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 thatprint_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 inread()
.
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.