3

I am trying to continue a PHP Script after the page/connection is closed.

Users will POLL the script in every 1 hour, I want to return some json output and want to continue the script in the background. I am using a shared host and I cannot use cron job.

Here is what I've tried.

ob_start();

ignore_user_abort();

echo "JSON_OUTPUT GOES HERE";

$ob_length = ob_get_length();

header("Content-Type : text/plain",TRUE);
header("Content-Length : $ob_length",TRUE);
header("Connection : Close",TRUE);

flush();
ob_flush();
ob_end_flush();

sleep(3);

echo "You cant see me..";

exit();

I am using Codeigniter framework, But its not working on my live server. It waits 3 seconds and then outputting You cant see me.. too.

Please help me.

Note

Project is hosted in LINUX/WINDOWS/WAMP-SERVER shared hosts.

8
  • a first error that i can see i that header() function must be called before any actual output is sent, either by normal HTML tags, blank lines in a file, or from PHP php manual Commented Apr 30, 2013 at 8:10
  • @lucky88 OP's already using output buffering. Commented Apr 30, 2013 at 8:12
  • 2
    Why not run a PHP script (or even better, a shell script or something better suited for daemon-like tasks) in the background using cron, and have the poll script simply query its progress? Feels much more elegant. Commented Apr 30, 2013 at 8:28
  • @Pekka웃 Reason:Shared Godaddy server. Commented Apr 30, 2013 at 8:30
  • Isn't Godaddy limiting your scripts' maximum running time? What is the script doing in the background? Commented Apr 30, 2013 at 8:32

4 Answers 4

11

After some research i got it work, Sometime it may be useful to some others.

function closeOutput($stringToOutput){   
        set_time_limit(0);
        ignore_user_abort(true);
        header("Connection: close\r\n");
        header("Content-Encoding: none\r\n");  
        ob_start();          
        echo $stringToOutput;   
        $size = ob_get_length();   
        header("Content-Length: $size",TRUE);  
        ob_end_flush();
        ob_flush();
        flush();   
} 

You can use it like

$outputContent = 'Contentent Goes Here...';

closeOutput( $outputContent );

sleep(5);

//do some background works ...

exit();
Sign up to request clarification or add additional context in comments.

3 Comments

Perfect... Other solutions out there were not working for me. I guess the \r\n, the content-encoding header and other small but important changes made the difference.
Tried A LOT of other solutions. The \r\n seems to be the nail here, +1
Note that this hack breaks HTTP Keep-Alive and will not work with HTTP/2.
2

First, don't use space after Connection and before : it should be Header: value not Header : value. Second, Connection: close don't force browser to stop getting current response and display blank page. Here http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html chapter 14.10 it states: Connection: close in either the request or the response header fields indicates that the connection SHOULD NOT be considered 'persistent' (section 8.1) after the current request/response is complete

So how can you try if your code works:

ignore_user_abort();
header("Content-Type: text/plain; charset=UTF-8");

// just to try show following echo immediately, working depends on server configuration
while (@ob_end_flush()); 

echo date('Y-m-d H:i:s'), PHP_EOL;

echo "JSON_OUTPUT GOES HERE", PHP_EOL;

sleep(10); // 10 seconds so you can close browser tab before

// this file should be created after 10 seconds, even after you closed browser tab
// also check if permissions to write to __DIR__ are set for apache.
file_put_contents(__DIR__ . '/tmp.txt', "Text after 10 sec");

exit;

Open this php file in browser and after 2-3 seconds close tab (even if you don't see anything on screen), wait a little longer and check if file is created. It's working on my linux machine.

2 Comments

Then why should i need to use the header function ?? User need to feel that the connection is closed and he can proceed to another thing,also this is a private URL and it can only be accessed through one of our application.
Showing information "connection closed go do things" in the middle of script can be highly server dependent. There are factors like server configuration or output compression or even browser can buffer output from script. So showing it before end of script can be really hard to impossible. Read here: pl1.php.net/manual/en/function.flush.php
2

Because of this cool possibility, which Red posted, I've written a small utility class which provides a queue where you can add Closures for later execution:

<?php

namespace Company\Project\Utilities;

/**
 * Class ContinueUtility
 *
 * @package Company\Project\Utilities
 */
class ContinueUtility {
/**
 * Stored tasks
 * @var array
 */
static protected $tasks = array();

/** Constant for new line in HTTP Header */
const HEADER_NEW_LINE = "\r\n";

/**
 * Add task (closure/function) to queue, with set arguments
 *
 * @param \Closure $task
 * @param array $arguments
 * @return void
 */
public static function addTask(\Closure $task, array $arguments = array()) {
    self::$tasks[] = array(
        'closure' => $task,
        'arguments' => $arguments
    );
}

/**
 * Returns TRUE if tasks has been set, otherwise FALSE
 *
 * @return boolean
 */
public static function hasTasks() {
    return !empty(self::$tasks);
}

/**
 * Clear all previous set tasks
 *
 * @return void
 */
protected static function clearTasks() {
    self::$tasks = array();
}

/**
 * Execute all previous set tasks
 *
 * @return void
 */
protected static function executeTasks() {
    foreach (self::$tasks as $task) {
        call_user_func_array($task['closure'], $task['arguments']);
    }
}

/**
 * Execute and clear all previous set tasks
 *
 * @return void
 */
public static function executeAndClearTasks() {
    self::executeTasks();
    self::clearTasks();
}

/**
 * Closes the HTTP connection to client immediately and outputs given string.
 *
 * @param string $instantOutput
 * @return void
 */
public static function closeConnection($instantOutput = '') {
    set_time_limit(0);
    ignore_user_abort(TRUE);
    header('Connection: close' . self::HEADER_NEW_LINE);
    header('Content-Encoding: none' . self::HEADER_NEW_LINE);
    ob_start();
    echo $instantOutput;
    $size = ob_get_length();
    header('Content-Length: ' . $size, TRUE);
    ob_end_flush();
    ob_flush();
    flush();
}
}

This is how you add new tasks to queue:

use Company\Project\Utilities\ContinueUtility;

$a = 4;
$b = 5;
ContinueUtility::addTask(function($a, $b){
    sleep(5);
    $c = a + b;
    file_put_contents(__DIR__ . '/whatever.log', $a . '+' . $b . '=' . $c);
}, array(
    $a, $b
));

And this is how you trigger execution of all previous added tasks:

ContinueUtility::closeConnection('Ready.');
ContinueUtility::executeAndClearTasks();

1 Comment

It works perfectly on a project where I'm using it currently. I send push notifications to smartphones and however for APNS (apple) it takes sometimes 30 seconds until the message is out. That means, that the HTTP request will not be completed until these 30 seconds. Now the request is instantly done and the notification will be send in the background :)
0

If you are using PHP-FPM, a cleaner and more versatile solution would be to simply execute fastcgi_finish_request();

From PHP.net's documentation

This function flushes all response data to the client and finishes the request. This allows for time consuming tasks to be performed without leaving the connection to the client open.

This is how Symfony handles its onTerminate.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.