132

My script is called by server. From server I'll receive ID_OF_MESSAGE and TEXT_OF_MESSAGE.

In my script I'll handle incoming text and generate response with params: ANSWER_TO_ID and RESPONSE_MESSAGE.

The problem is that I'm sending response to incomming "ID_OF_MESSAGE", but server which send me message to handle will set his message as delivered to me (It means I can send him response to that ID), after receiving http response 200.

One of solution is to save message to database and make some cron which will be running each minute, but I need to generate response message immediately.

Is there some solution how to send to server http response 200 and than continue executing php script?

Thank you a lot

Rahul
  • 10,830
  • 4
  • 53
  • 88
user1700214
  • 1,375
  • 2
  • 9
  • 6

13 Answers13

231

Yes. You can do this:

ignore_user_abort(true);//not required
set_time_limit(0);

ob_start();
// do initial processing here
echo $response; // send the response
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
@ob_flush();
flush();
fastcgi_finish_request();//required for PHP-FPM (PHP > 5.3.3)

// now the request is sent to the browser, but the script is still running
// so, you can continue...

die(); //a must especially if set_time_limit=0 is used and the task ends
vcampitelli
  • 3,168
  • 1
  • 12
  • 13
  • 5
    Is it possible to do it with a keep-alive connection ? – Congelli501 Jul 09 '14 at 19:14
  • 4
    Excellent!! This is the only response to this question that actually works!!! 10p+ – Martin_Lakes Aug 01 '14 at 19:13
  • 6
    Awesome answer! The only thing I changed was `set_time_limit(0);`. You do probably want it to run for longer than the default 30 seconds, but indefinitely could cause problems if it goes into an infinite loop! I have a longer value set in my `php.ini` file. – CJ Dennis Aug 21 '14 at 14:25
  • 3
    is there a reason you use ob_flush after ob_end_flush? I understand the need for the flush function there at the end but I'm not sure why you would need ob_flush with ob_end_flush being called. – ars265 May 20 '15 at 13:40
  • 11
    Please note that if a content-encoding header is set to anything else than 'none' it could render this example useless as it would still let the user wait the full execution time (until timeout?). So to be absolutely sure it will work local ánd in the production environment, set the 'content-encoding' header to 'none': `header("Content-Encoding: none")` – Brian Jun 25 '15 at 07:04
  • 21
    Tip: I started using PHP-FPM, so I had to add `fastcgi_finish_request()` at the end – vcampitelli Sep 16 '15 at 13:18
  • 3
    RE: Keep-alive connection: You do not necessarily need to have close the connection, but then what will happen is the *next* asset requested on the same connection will be forced to wait. So you could deliver the HTML fast but then one of your JS or CSS files might load slowly, as the connection has to finish getting the response from PHP before it can get the next asset. So for that reason, closing the connection is a good idea so the browser doesn't have to wait for it to be freed up. – Nate Lampton Sep 17 '15 at 03:37
  • 2
    'ob_end_flush(); ob_flush();' produces a warning that there is nothing to flush. PHP documentation about 'ob_flush();' states: _This function does not destroy the output buffer like ob_end_flush() does._ – Delcon Aug 15 '16 at 11:34
  • 1
    why are both ob_end_flush and ob_flush needed. This is causing me issues when the same client calls another api. In the second api it only works if I add ob_start before initializing my variable initialization that is getting echoed – FBP Mar 21 '18 at 14:56
  • 2
    After ```ob_end_flush();```, ```ob_flush();``` is not needed any more. In fact, it will cause an error. Just ```ob_end_flush();``` is enough. – lhermann May 18 '18 at 11:31
  • Excellent answer. Works. – David Okwii Jun 14 '18 at 09:20
  • This was so helpful to me! Worked great for my Slack Bot slash command integration where I can send the response immediately to let them know it was received, then start working on calling other APIs and parsing data. – Sleepybear Aug 07 '18 at 19:18
  • This code may return empty response. I have used that and sometimes a white screen appears. I have moved `header('Connection: close');` after `flush()` and it is OK now. – Meysam Valueian Sep 09 '18 at 07:30
  • 2
    Note: this will not work if there are any other `ob_start()` calls in your code that have not been terminated with `ob_end_flush()`. To guarantee all buffers are flushed and ended, you can do: `while (ob_get_level() > 0) { ob_end_flush(); }`. – Kevin Borders Oct 12 '18 at 23:11
  • 1
    This only started working for me once the size of `$response` was sufficiently large, so I changed `echo $response;` to `echo str_pad($response, 4096);` to force a minimum size. (Background: I need to return an HTTP 200 to a web service within 3 seconds to acknowledge the POST request has been received. Technically the response doesn't have to contain any content.) – marcvangend Apr 15 '20 at 12:07
  • 1
    `ob_start()` is only needed if you don't know the length of the output. If you do, you can skip all of the `ob_*` calls and just set the headers, print the output, and call `flush()`. The browser will then display the page and your script can continue executing. `set_time_limit()` is only needed if your script will run longer than `max_execution_time` in the php.ini file, which is 30 sec. by default. – Russell G Oct 07 '20 at 14:30
  • 1
    @marcvangend: Thanks for the tip about `str_pad()`! I added a call to `ini_get("output_buffering")` to get the buffer size in case it's not set to the default 4096. – Russell G Oct 07 '20 at 16:58
  • 1
    @lhermann - I think it depends on server. I tried every combination of those three calls and the *only thing that worked* for me is having all three. As such, I'm restoring the original answer it's broken (for me) after the recent edit. – But those new buttons though.. Jul 23 '21 at 11:04
  • Could someone point me to what `end;` does. I could only find `end();` function in PHP manual which appears to be different. – Thomas Harris Oct 07 '21 at 12:09
  • I don't know why @hexalys wrote this. Maybe he was trying to go for `exit` / `die` instead of `end`, as it doesn't exist. Only the `end()` function, as you already realized. I've edited the question again and replaced it with a `die`. Thanks! – vcampitelli Oct 11 '21 at 12:42
  • @vcampitelli Indeed I meant exit; I usually use die() myself. Thanks for the correction. However you do not need @ob_flush(); ob_end_flush(); already implies ob_flush(); – hexalys Oct 12 '21 at 08:18
  • In my case I didn't need ANY of those flushing functions or any manually set headers. I just do `session_write_close()` and `fastcgi_finish_request()`. That's all I needed. `fastcgi_finish_request()` is even documented to already flush automatically. – jlh May 18 '22 at 13:00
74

I've seen a lot of responses on here that suggest using ignore_user_abort(true); but this code is not necessary. All this does is ensure your script continues executing before a response is sent in the event that the user aborts (by closing their browser or pressing escape to stop the request). But that's not what you're asking. You're asking to continue execution AFTER a response is sent. All you need is the following:

// Buffer all upcoming output...
ob_start();

// Send your response.
echo "Here be response";

// Get the size of the output.
$size = ob_get_length();

// Disable compression (in case content length is compressed).
header("Content-Encoding: none");

// Set the content length of the response.
header("Content-Length: {$size}");

// Close the connection.
header("Connection: close");

// Flush all output.
ob_end_flush();
@ob_flush();
flush();

// Close current session (if it exists).
if(session_id()) session_write_close();

// Start your background work here.
...

If you're concerned that your background work will take longer than PHP's default script execution time limit, then stick set_time_limit(0); at the top.

xxx
  • 1,153
  • 1
  • 11
  • 23
Kosta Kontos
  • 4,152
  • 7
  • 25
  • 28
  • 4
    Tried a LOT of different combinations, THIS is the one that works!!! Thanks Kosta Kontos!!! – Martin_Lakes Aug 05 '16 at 10:43
  • 2
    Works perfectly on apache 2, php 7.0.32 and ubuntu 16.04! Thanks! – KyleBunga Jan 26 '19 at 00:03
  • 1
    I've tried other solutions, and only this one worked for me. Order of lines is important as well. – Siniša Oct 01 '19 at 15:27
  • I can't for the life of me understand why `ob_flush()` is required here since it throws a notice `PHP Notice: ob_flush(): failed to flush buffer. No buffer to flush in php shell code on line 1`. Oddly, this doesn't work without it. I guess it's doing something after all... – But those new buttons though.. Jul 23 '21 at 11:20
  • @billynoah I think it's not needed. `ob_end_flush()` does the same as `ob_flush` except that it closes the buffer (therefore calling `ob_flush()` after `ob_end_flush()` produces the warning you came across. – Sebi2020 Sep 03 '21 at 18:25
  • @Sebi2020 - the whole routine doesn't work for me without it (php 7.4.3) - which is what makes it all the more confusing. There must be something going on behind the scenes in PHP beyond what's documented. – But those new buttons though.. Sep 03 '21 at 19:53
  • An observation: if you are looking to ensure that the timing of your script is the same every time, this is not the answer. Only the buffer up to the point where you call `ob_flush()` is sent, but the response is not sent until the script finished executing. Observed using php 8.1 on Ubuntu 22.04 – JoSSte Nov 14 '22 at 16:48
49

If you're using FastCGI processing or PHP-FPM, you can:

session_write_close(); //close the session
ignore_user_abort(true); //Prevent echo, print, and flush from killing the script
fastcgi_finish_request(); //this returns 200 to the user, and processing continues

// do desired processing ...
$expensiveCalulation = 1+1;
error_log($expensiveCalculation);

Source: https://www.php.net/manual/en/function.fastcgi-finish-request.php

PHP issue #68722: https://bugs.php.net/bug.php?id=68772

Pikamander2
  • 7,332
  • 3
  • 48
  • 69
DarkNeuron
  • 7,981
  • 2
  • 43
  • 48
27

I spent a few hours on this issue and I have come with this function which works on Apache and Nginx:

/**
 * respondOK.
 */
protected function respondOK()
{
    // check if fastcgi_finish_request is callable
    if (is_callable('fastcgi_finish_request')) {
        /*
         * This works in Nginx but the next approach not
         */
        session_write_close();
        fastcgi_finish_request();

        return;
    }

    ignore_user_abort(true);

    ob_start();
    $serverProtocole = filter_input(INPUT_SERVER, 'SERVER_PROTOCOL', FILTER_SANITIZE_STRING);
    header($serverProtocole.' 200 OK');
    header('Content-Encoding: none');
    header('Content-Length: '.ob_get_length());
    header('Connection: close');

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

You can call this function before your long processing.

Ehsan
  • 1,022
  • 11
  • 20
  • 2
    This is bloody lovely! After trying everything else above this is the only thing that worked with nginx. – spice Jan 06 '19 at 23:18
  • Your code is almost exactly the same as the code [here](https://keepcoding.ehsanabbasi.com/php/processing-php-after-sending-200-ok/) but your post is older :) +1 – Accountant م Feb 15 '19 at 22:41
  • be careful with `filter_input` function it returns NULL sometimes. see this [user contribution](http://php.net/manual/en/function.filter-input.php#77307) for detail – Accountant م Feb 16 '19 at 04:05
6

Modified the answer by @vcampitelli a bit. Don't think you need the close header. I was seeing duplicate close headers in Chrome.

<?php

ignore_user_abort(true);

ob_start();
echo '{}';
header($_SERVER["SERVER_PROTOCOL"] . " 202 Accepted");
header("Status: 202 Accepted");
header("Content-Type: application/json");
header('Content-Length: '.ob_get_length());
ob_end_flush();
ob_flush();
flush();

sleep(10);
Justin
  • 42,716
  • 77
  • 201
  • 296
  • 6
    I mentioned this in the original answer, but I'll say it here too. You do not necessarily need to have close the connection, but then what will happen is the next asset requested on the same connection will be forced to wait. So you could deliver the HTML fast but then one of your JS or CSS files might load slowly, as the connection has to finish getting the response from PHP before it can get the next asset. So for that reason, closing the connection is a good idea so the browser doesn't have to wait for it to be freed up. – Nate Lampton Sep 17 '15 at 03:42
5

I asked this question to Rasmus Lerdorf in April 2012, citing these articles:

I suggested the development of a new PHP built-in function to notify the platform that no further output (on stdout?) will be generated (such a function might take care of closing the connection). Rasmus Lerdorf responded:

See Gearman. You really really don't want your frontend Web servers doing backend processing like this.

I can see his point, and support his opinion for some applications/ loading scenarios! However, under some other scenarios, the solutions from vcampitelli et al, are good ones.

Matthew Slyman
  • 346
  • 3
  • 9
5

I can't install pthread and neither the previous solutions work for me. I found only the following solution to work (ref: https://stackoverflow.com/a/14469376/1315873):

<?php
ob_end_clean();
header("Connection: close");
ignore_user_abort(); // optional
ob_start();
echo ('Text the user will see');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush(); // Strange behaviour, will not work
flush();            // Unless both are called !
session_write_close(); // Added a line suggested in the comment
// Do processing here 
sleep(30);
echo('Text user will never see');
?>
Fil
  • 1,032
  • 13
  • 29
  • What version of php and what operating system are you using? Maybe something has changed with recent versions of php... – Fil Apr 05 '21 at 10:19
  • why do you put the `header("Connection: close");` before the output? everyone seems to have it after. does it not matter or is there an actually effect? – Zackattack Mar 13 '22 at 11:06
  • 1
    @Zackattack, if you're referring to the last echo message, it's only to check that the script continues its execution but for the client is already ended (thanks to the headers). After I ensured the last echo message doesn't appear on browser, I removed sleep and echo instructions in my script to perform the actual processing. If you want to show something for the user (that's the real output), you have to insert instructions between ob_start and ob_get_length. The order among the header instructions doesn't matter. – Fil Mar 14 '22 at 14:08
  • thanks for that. so I can put `header("Connection: close");` after `ob_start();` as well? – Zackattack Mar 14 '22 at 15:11
  • 2
    From php documentation on `ob_start` instruction: "_While output buffering is active no output is sent from the script (other than headers)_", so it doesn't change anything... I would put it before `ob_start()` just to not mistakenly think the header is buffered too as the positioning seems to imply. But it will work anyway even after `ob_start()` if you prefer. – Fil Mar 15 '22 at 14:58
4

I use the php function register_shutdown_function for this.

void register_shutdown_function ( callable $callback [, mixed $parameter [, mixed $... ]] )

http://php.net/manual/en/function.register-shutdown-function.php

Edit: The above is not working. It seems I was misled by some old documentation. The behaviour of register_shutdown_function has changed since PHP 4.1 link link

martti
  • 1,758
  • 1
  • 12
  • 10
  • This is not what is being asked for - this function basically just extends the script termination event and is still part of the output buffer. – fisk Jan 03 '16 at 14:48
  • 1
    Found it worth an upvote, cause it shows what does not work. – Trendfischer Sep 09 '16 at 13:06
  • 1
    Ditto - upvoting because I was expecting to see this as an answer, and useful to see that it doesn't work. – HappyDog Nov 14 '19 at 21:30
3

I have something that can compressed and send the response and let other php code to execute.

function sendResponse($response){
    $contentencoding = 'none';
    if(ob_get_contents()){
        ob_end_clean();
        if(ob_get_contents()){
            ob_clean();
        }
    }
    header('Connection: close');
    header("cache-control: must-revalidate");
    header('Vary: Accept-Encoding');
    header('content-type: application/json; charset=utf-8');
    ob_start();
    if(phpversion()>='4.0.4pl1' && extension_loaded('zlib') && GZIP_ENABLED==1 && !empty($_SERVER["HTTP_ACCEPT_ENCODING"]) && (strpos($_SERVER["HTTP_ACCEPT_ENCODING"], 'gzip') !== false) && (strstr($GLOBALS['useragent'],'compatible') || strstr($GLOBALS['useragent'],'Gecko'))){
        $contentencoding = 'gzip';
        ob_start('ob_gzhandler');
    }
    header('Content-Encoding: '.$contentencoding);
    if (!empty($_GET['callback'])){
        echo $_GET['callback'].'('.$response.')';
    } else {
        echo $response;
    }
    if($contentencoding == 'gzip') {
        if(ob_get_contents()){
            ob_end_flush(); // Flush the output from ob_gzhandler
        }
    }
    header('Content-Length: '.ob_get_length());
    // flush all output
    if (ob_get_contents()){
        ob_end_flush(); // Flush the outer ob_start()
        if(ob_get_contents()){
            ob_flush();
        }
        flush();
    }
    if (session_id()) session_write_close();
}
Mayur Shedage
  • 1,027
  • 2
  • 11
  • 19
2

in case of php file_get_contents use, connection close is not enough. php still wait for eof witch send by server.

my solution is to read 'Content-Length:'

here is sample :

response.php:

 <?php

ignore_user_abort(true);
set_time_limit(500);

ob_start();
echo 'ok'."\n";
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
ob_flush();
flush();
sleep(30);

Note the "\n" in response to close line, if not the fget read while wait eof.

read.php :

<?php
$vars = array(
    'hello' => 'world'
);
$content = http_build_query($vars);

fwrite($fp, "POST /response.php HTTP/1.1\r\n");
fwrite($fp, "Content-Type: application/x-www-form-urlencoded\r\n");
fwrite($fp, "Content-Length: " . strlen($content) . "\r\n");
fwrite($fp, "Connection: close\r\n");
fwrite($fp, "\r\n");

fwrite($fp, $content);

$iSize = null;
$bHeaderEnd = false;
$sResponse = '';
do {
    $sTmp = fgets($fp, 1024);
    $iPos = strpos($sTmp, 'Content-Length: ');
    if ($iPos !== false) {
        $iSize = (int) substr($sTmp, strlen('Content-Length: '));
    }
    if ($bHeaderEnd) {
        $sResponse.= $sTmp;
    }
    if (strlen(trim($sTmp)) == 0) {
        $bHeaderEnd = true;
    }
} while (!feof($fp) && (is_null($iSize) || !is_null($iSize) && strlen($sResponse) < $iSize));
$result = trim($sResponse);

As you can see this script dosent wait about eof if content length is reach.

hope it will help

Jérome S.
  • 127
  • 1
  • 7
1

There is another approach and its worthwhile considering if you don't want to tamper with the response headers. If you start a thread on another process the called function wont wait for its response and will return to the browser with a finalized http code. You will need to configure pthread.

class continue_processing_thread extends Thread 
{
     public function __construct($param1) 
     {
         $this->param1 = $param1
     }

     public function run() 
     {
        //Do your long running process here
     }
}

//This is your function called via an HTTP GET/POST etc
function rest_endpoint()
{
  //do whatever stuff needed by the response.

  //Create and start your thread. 
  //rest_endpoint wont wait for this to complete.
  $continue_processing = new continue_processing_thread($some_value);
  $continue_processing->start();

  echo json_encode($response)
}

Once we execute $continue_processing->start() PHP wont wait for the return result of this thread and therefore as far as rest_endpoint is considered. It is done.

Some links to help with pthreads

Good luck.

Community
  • 1
  • 1
Jonathan
  • 2,318
  • 7
  • 25
  • 44
0

I have an important addition to all the other great answers!
TL;DR:
add

echo str_repeat(' ', 1024);



In my use case I would like to mark the API Call as "Accepted" and don't make the client wait for finished processing.

Actually it feels right: The client should stop waiting for answer when it receives the "Connection: close" header, but in fact at least MY php does not send those headers yet. (tested with two different PHP-Servers, and via Browser+Insomnia client each)

There is some special behaviour that flush() would not send the first content if not at least a special amount of bytes were echoed, yet (in my case 1024 bytes). (probably some hotfix in case there is some leading or trailing whitespace in php files which are actually treated like an echo statement or something like that which would prevent later header() statements to take effect.)

To solve the problem one can send 1024 leading whitespace characters which should be ignored by JSON/XML/HTML interpreters.

So the full code looks like this:

ob_start();
echo str_repeat(' ', 1024);
echo $response; // send the response
header('Connection: close');
header('Content-Length: '.ob_get_length());
ob_end_flush();
@ob_flush();
flush();

(Here is some backup for my arguments, cant find the correct source rn: How to flush output after each `echo` call?)

Charliexyx
  • 127
  • 11
0

In addition to the answers, I returned a JSON string as response. I discovered the response is being truncated for unknown reason. The fix to the problem was adding extra space:

echo $json_response;
//the fix
echo str_repeat(' ', 10);
Wakeel
  • 4,272
  • 2
  • 13
  • 14