7

I have the following code:

ignore_user_abort(true);
while(!connection_aborted()) {
    // do stuff
}

and according to the PHP documentation, this should run until the connection is closed, but for some reason, it doesn't, instead it keeps running until the script times out. I've looked around online and some recommended adding

echo chr(0);
flush();

into the loop, but that doesn't seem to do anything either. Even worse, if I just leave it as

while(true) {
    // do stuff
}

PHP still continues to run the script after the client disconnects. Does anyone know how to get this working? Is there a php.ini setting that I'm missing somewhere?

If it matters, I'm running PHP 5.3.5. Thanks in advance!

kevmo314
  • 4,223
  • 4
  • 32
  • 43

4 Answers4

10

I'm a bit late to this party, but I just had this problem and got to the bottom of it. There are multiple things going on here -- a few of them mentioned here: PHP doesn't detect connection abort at all

The gist of it: In order for connection_aborted() to work, PHP needs to attempt to send data to the client.

Output Buffers

As noted, PHP will not detect the connection is dead until it tries to actually send data to the client. This is not as simple as doing an echo, because echo sends the data to any output buffers that may exist, and PHP will not attempt a real send until those buffers are full enough. I will not go into the details of output buffering, but it's worth mentioning that there can be multiple nested buffers.

At any rate, if you'd like to test connection_abort(), you must first end all buffers:

while (ob_get_level()){ ob_end_clean(); }

Now, anytime you want to test if the connection is aborted, you must attempt to send data to the client:

echo "Something.";
flush();

// returns expected value...
// ... but only if ignore_user_abort is false!
connection_aborted(); 

Ignore User Abort

This is a very important setting that determines what PHP will do when the above flush() is called, and the user has aborted the connection (eg: hit the STOP button in their browser).

If true, the script will run along merrily. flush() will do essentially nothing.

If false, as is the default setting, execution will immediately stop in the following manner:

  • If PHP is not already shutting down, it will begin its shutdown process.

  • If PHP is already shutting down, it will exit whatever shutdown function it is in and move on to the next.

Destructors

If you'd like to do stuff when the user aborts the connection, you need to do three things:

  1. Detect the user aborted the connection. This means you have to attempt to flush to the user periodically, as described further above. Clear all output buffers, echo, flush.

    a. If ignore_connection_aborted is true, you need to manually test connection_aborted() after each flush.

    b. If ignore_connection_aborted is false, a call to flush will cause the shutdown process to begin. You must then be especially careful not to cause flush from within your shutdown functions, or else PHP will immediate cease execution of that function and move on to the next shutdown function.

Putting it all together

Putting this all together, lets make an example that detects the user hitting "STOP" and does stuff.

class DestructTester {
    private $fileHandle;

    public function __construct($fileHandle){
        // fileHandle that we log to
        $this->fileHandle = $fileHandle;
        // call $this->onShutdown() when PHP is shutting down.
        register_shutdown_function(array($this, "onShutdown"));
    }

    public function onShutdown() {
        $isAborted = connection_aborted();
        fwrite($this->fileHandle, "PHP is shutting down. isAborted: $isAborted\n");

        // NOTE
        // If connection_aborted() AND ignore_user_abort = false, PHP will immediately terminate
        // this function when it encounters flush. This means your shutdown functions can end
        // prematurely if: connection is aborted, ignore_user_abort=false, and you try to flush().
        echo "Test.";
        flush();
        fwrite($this->fileHandle, "This was written after a flush.\n");
    }
    public function __destruct() {
        $isAborted = connection_aborted();
        fwrite($this->fileHandle, "DestructTester is getting destructed. isAborted: $isAborted\n");
    }
}

// Create a DestructTester
// It'll log to our file on PHP shutdown and __destruct().
$fileHandle = fopen("/path/to/destruct-tester-log.txt", "a+");
fwrite($fileHandle, "---BEGINNING TEST---\n");
$dt = new DestructTester($fileHandle);

// Set this value to see how the logs end up changing
// ignore_user_abort(true);

// Remove any buffers so that PHP attempts to send data on flush();
while (ob_get_level()){
    ob_get_contents();
    ob_end_clean();
}

// Let's loop for 10 seconds
//   If ignore_user_abort=true:
//      This will continue to run regardless.
//   If ignore_user_abort=false:
//      This will immediate terminate when the user disconnects and PHP tries to flush();
//      PHP will begin its shutdown process.
// In either case, connection_aborted() should subsequently return "true" after the user
// has disconnected (hit STOP button in browser), AND after PHP has attempted to flush().
$numSleeps = 0;
while ($numSleeps++ < 10) {
    $connAbortedStr = connection_aborted() ? "YES" : "NO";
    $str = "Slept $numSleeps times. Connection aborted: $connAbortedStr";
    echo "$str<br>";
    // If ignore_user_abort = false, script will terminate right here.
    // Shutdown functions will being.
    // Otherwise, script will continue for all 10 loops and then shutdown.
    flush();

    $connAbortedStr = connection_aborted() ? "YES" : "NO";
    fwrite($fileHandle, "flush()'d $numSleeps times. Connection aborted is now: $connAbortedStr\n");
    sleep(1);
}
echo "DONE SLEEPING!<br>";
die;

The comments explain everything. You can fiddle with ignore_user_abort and look at the logs to see how this changes things.

I hope this helps anyone having trouble with connection_abort, register_shutdown_function, and __destruct.

JS_Riddler
  • 1,443
  • 2
  • 22
  • 32
  • Thanks for all the information, but it still does not work. – Avatar Feb 20 '22 at 23:54
  • @Avatar What, exactly, is not working? – JS_Riddler Feb 23 '22 at 21:58
  • The gist is what I needed to understand. I was using SSE and the PHP script ran indefinitely when no data was available and the client disconnected. I have added some stray data and now it stops on disconnect! – Puspam Jun 20 '23 at 06:47
2

Try using ob_flush(); just before flush(); and some browsers just won't update the page before some data is added.

Try doing something like

<? php
// preceding scripts

ignore_user_abort(true);

$i = 0;

while(!connection_aborted())
{ $i++;
  echo $i;

  echo str_pad('',4096); // yes i know this will increase the overhead but that can be reduced afterwords

  ob_flush();

  flush();

  usleep(30000); // see what happens when u run this on my WAMP this runs perfectly
}

// Ending scripts
?>

Google Chrome has issues with this code, actually; it doesn't support streaming very nicely.

1

Try:

    ignore_user_abort(true);

    echo "Testing connection handling";

    while (1) {
            if (connection_status() != CONNECTION_NORMAL)
                    break;
            sleep(1);
            echo "test";
            flush();
    }
j.w.r
  • 4,136
  • 2
  • 27
  • 29
  • Nope, didn't appear to work either. It's worth noting though that when I run that in my browser, I don't see "test" appear every second. In other words, it doesn't look like the script is flushing, leading me to believe it might be a php.ini setting I'm missing? – kevmo314 Jun 17 '11 at 04:49
  • What is the `output_buffering` setting set to in your php.ini? – j.w.r Jun 17 '11 at 04:52
  • 1
    output_buffering = 4096 Should I try setting it to "Off"? – kevmo314 Jun 17 '11 at 04:53
  • Yeah, I think you'll probably need to restart your web server software after making that change. – j.w.r Jun 17 '11 at 04:55
  • Looks like that worked, so it *was* a php.ini setting that was screwing me up. Thanks! – kevmo314 Jun 17 '11 at 04:58
  • 3
    -1 This is not the best answer here. As the OP mentioned it, the php.ini was the cause of the issue here. The best answer is at https://stackoverflow.com/a/58775947/609862 in this thread from @JS_Riddler. It explains the underlying issue that prevents the `connection_aborted()` from working as expected – asiby Feb 17 '20 at 08:07
0

Buffering seems to cause issues depending on your server settings.

I tried disabling the buffer with ob_end_clean but that wasn't enough, I had to send some data to cause the buffer to fully flush out. Here is the final code that ended up working for me.

set_time_limit(0); // run the delay as long as the user stays connected
ignore_user_abort(false);
ob_end_clean();
echo "\n";
while ($delay-- > 0 && !connection_aborted())
{
    echo str_repeat("\r", 1000) . "<!--sleep-->\n";
    flush();
    sleep(1);
}
ob_start();