Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stop PHP script from executing if client disconnects

I'm having the following problem.

Client from program sends an HTTPS action to server. Server gets the action and all params that are required. But then client disconnects while PHP script that is called is working.

The script echos back the response and the client doesn't get anything like it shouldn't. Because it is important action and client should always get the response I want to stop PHP script from executing if this scenario ever happens.

The entire script is in MySQL transaction so if it dies db rollback will occur and it's essential that it does.

I have put this code at the very end of script

ob_end_clean();

header("Connection: close");

ignore_user_abort();

ob_start();

echo $response->asXML();

$size = ob_get_length();

header("Content-Length: $size");

ob_end_flush();

flush();

if ( connection_aborted() )
        log('Connection aborted');
else
        log('Connection is fine');

But connection_aborted always returns 0 (NORMAL CONNECTION) so the script always executes. connection_status also doesn't help, so I don't know what else to try.

EDIT:

I figured it out that this never works only when the script is called with an AJAX request. When it is called with regular HTTP it shows that connection is dead. But when I call it with new XMLHttpRequest() it never figures it out that connection is closed.

It asynchronous request.

So I managed to narrow it down. But I still don't know how to fix it.

like image 526
madeye Avatar asked Aug 18 '14 12:08

madeye


1 Answers

After some experimentation, it seems you need to do multiple flushes (writes) to the connection to have PHP detect that the connection has aborted. However it seems you want to write your output as one big chunk of XML data. We can get around the problem of having no more data to send by using the chunked transfer encoding for HTTP requests.

ob_implicit_flush(true);
ignore_user_abort(true);

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

header('Content-Type: text/xml; charset=utf-8');
header('Transfer-Encoding: chunked');
header('Connection: close');

// ----- Do your MySQL processing here. -----

// Sleep so there's some time to abort the connection. Obviously this wouldn't be
// here in production code, but is here to simulate time to process the request.
sleep(3);

// Put XML content into $xml. If output buffering is used, dump contents to $xml.
//$xml = $response->asXML();
$xml = '<?xml version="1.0"?><test/>';
$size = strlen($xml);

// If connection is aborted before here, connection_status() will not return 0.

echo dechex($size), "\r\n", $xml, "\r\n"; // Write content chunk.

echo "0\r\n\r\n"; // Write closing chunk, detecting if connection closed.

if (0 !== connection_status()) {
    error_log('Connection aborted.');
    // Rollback MySQL transaction.
} else {
    error_log('Connection successful.');
    // Commit MySQL transaction.
}

ob_implicit_flush(true) tells PHP to make a flush after each echo statement. The connection status is checked after each chunk is sent with echo. If the connection is closed before the first statement, connection_status() should return a non-zero value.

You may need to use ini_set('zlib.output_compression', 0) at the top of the script to turn off output compression if compression is turned on in php.ini, otherwise it acts as another external buffer and connection_status() may always return 0.


EDIT: The Transfer-Encoding: chunked header modifies the way the browser expects to receive data in the HTTP response. The browser then expects to receive data in as chunks with the form [{Hexadecimal number of bytes in chunk}\r\n{data}\r\n]. So for example, you can send the string "This is an example string." as a chunk by echoing 1a\r\nThis is an example string.\r\n. The browser would only display the string, not 1a, and keep the connection open waiting until it receives an empty chunk, that is, 0\r\n\r\n.


EDIT: I modified the code in my answer so that it works as a standalone script, rather than rely on $response being defined by the OP's code. That is, I commented the line with $xml = $response->asXML(); and replaced it with $xml = '<?xml version="1.0"?><test/>'; as a proof of concept.

like image 162
Trowski Avatar answered Oct 12 '22 09:10

Trowski