Stop PHP script from executing if client disconnects

I had the following problem.

The client from the program sends an HTTPS action to the server. The server receives the action and all the necessary parameters. But then the client disconnects, and the PHP script that is being called works.

script echos returns a response, and the client does not receive anything like this, should not. Since this is an important action, and the client should always receive a response, I want to stop the execution of the PHP script if this script ever happens.

The whole script is in a MySQL transaction, so if it dies, a db rollback will occur, and this is important.

I put this code at the very end of the 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 is always executed. connection_status doesn't help either, so I don't know what else to try.

EDIT:

I realized that this never works only when calling a script with an AJAX request. When it is called with plain HTTP, it shows that the connection is dead. But when I call it new XMLHttpRequest() , it never realizes that the connection is closed.

This is an asynchronous request.

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

+7
ajax php mysql connection
source share
3 answers

After some experimentation, it seems you need to do a few dumps (records) to the connection so that PHP detects that the connection is interrupted. However, it seems that you want to write your output as one large piece of XML data. We can get around the problem of the lack of additional data to send 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 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 flash after each echo statement. The connection status is checked after sending each fragment using echo . If the connection is closed before the first statement, connection_status() should return a nonzero value.

You may need to use ini_set('zlib.output_compression', 0) at the top of the script to disable output compression if compression is enabled in php.ini, otherwise it acts like another external buffer, and connection_status() can always return 0 >.


EDIT: The Transfer-Encoding: chunked header changes the way the browser expects to receive data in an HTTP response. Then the browser expects to receive data in the form of pieces with the form [{Hexadecimal number of bytes in chunk}\r\n{data}\r\n] . For example, you can send the string “This is an example string” as a fragment, echoing 1a\r\nThis is an example string.\r\n . The browser will only display the string, not 1a , and keep the connection open until it receives an empty piece, that is, 0\r\n\r\n .


EDIT: I changed the code in my answer to work as a stand-alone script, rather than relying on the $response defined by the OP code. That is, I commented out the line with $xml = $response->asXML(); and replaced it with $xml = '<?xml version="1.0"?><test/>'; as proof of concept.

+4
source share

PHP can detect that a connection is only interrupted when it tries to send any output to the browser. Try to output some whitespace, flush() output, then check connection_status() . You may need to output enough characters to get a buffer, in my case 64 kb worked.

 ob_end_flush(); flush(); sleep(10); ignore_user_abort(true); echo str_repeat(' ', 1024*64); flush(); if ( connection_status() != 0 ) die(); 

Note that I added ignore_user_abort(true) , without it, execution will end at the output of the script, that is, to echo or flush() .

+1
source share

I think you should not rely on the connection_aborted () function: there are some user reports ( https://stackoverflow.com/a/16577/... , An alternative to the PHP connection_aborted () function , that it is unreliable. Also consider situations when a network timeout occurs.

I propose another idea, which, in my opinion, is more reliable. It is simple: send XML data to the client, and then wait a while for confirmation: the client sends another HTTP request with confirmation when the data was received. After this confirmation, you can complete the transaction. In case of timeout - rollback.

This idea requires minor changes in server php and client code.

Changed your PHP code (test1.php):

 ob_end_clean(); header("Connection: close"); ignore_user_abort(true); ob_start(); echo $response->asXML(); $size = ob_get_length(); header("Content-Length: $size"); // *** added lines *** $serial = rand(); $memcache_obj = memcache_connect('localhost', 11211); $memcache_obj->set("test1.$serial", 0); header("X-test1-serial: $serial"); // *** ob_end_flush(); flush(); // *** changed code *** $ok = 0; $tmout = 10*1000; // timeout in milliseconds // wait until confirmation flag changes to 1 or timeout occurs while (!($ok = intval($memcache_obj->get("test1.$serial")))) { if (connection_aborted()) break; // then no wait if (($tmout = $tmout - 10) <= 0) break; usleep(10000); // wait 10 milliseconds } $memcache_obj->delete("test1.$serial"); if ($ok) { error_log('User informed'); // commit transaction } else { error_log('Timeout'); // rollback transaction } 

Code that processes client confirmation (user_ack.php):

 <?php header("Content-type: text/plain; charset=utf-8"); $serial = intval($_GET['serial']); $memcache_obj = memcache_connect('localhost', 11211); $memcache_obj->replace("test1.$serial", 1); echo $serial; 

And finally, the client code (test1.html) to check:

 <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Test connection</title> <script type="text/javascript"> function user_ack(serial) { var httpReq = new XMLHttpRequest(); httpReq.open("GET", "user_ack.php?serial=" + encodeURIComponent(serial), true); httpReq.onreadystatechange = function () { if (httpReq.readyState == 4) { if (httpReq.status == 200) { // confirmation successful document.getElementById("ack").textContent = httpReq.responseText; } } } httpReq.send(""); } function get_xml() { var httpReq = new XMLHttpRequest(); httpReq.open("POST", "test1.php", true); httpReq.onreadystatechange = function () { if (httpReq.readyState == 4) { if (httpReq.status == 200) { // send confirmation var serial = httpReq.getResponseHeader("X-test1-serial"); user_ack(serial); // ... process XML ... document.getElementById("xml").textContent = httpReq.responseText; } } } httpReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); httpReq.send("id=abc"); // parameters to test1.php } </script> </head> <body> <p>XML from server:</p> <div id="xml"></div> <p>Confirmation serial: <span id="ack"></span></p> <p><input type="button" value="XML download test" onclick="get_xml();"/></p> </body> </html> 

I use memcached to store the value of the confirmation flag for the connection between test1.php and user_ack.php. This value name must be unique for each connection, so I use $serial = rand(); to generate a name: "test1.$serial" . When the created confirmation flag has a value of 0. After confirmation of user_ack.php change the value to 1.

Since user_ack.php must be the name of the flag value, the client sends an acknowledgment with a $ serial value. The $ serial value is received by the form server in a custom HTTP header: X-test1-serial when receiving XML.

I use memcache for simplicity. The MySQL table used for this purpose can also be used.

+1
source share

All Articles