Slack PHP API - Avoid Timeout error

旧时模样 提交于 2019-11-28 12:08:08
rcoup

You're doing all the right things, just need to change the order.

  1. Respond to the original request with a 200 OK response immediately. See this answer for details, but essentially:

    ignore_user_abort(true);
    ob_start();
    echo('{"response_type": "in_channel", "text": "Checking, please wait..."}');
    header($_SERVER["SERVER_PROTOCOL"] . " 200 OK");
    header("Content-Type: application/json");
    header('Content-Length: '.ob_get_length());
    ob_end_flush();
    ob_flush();
    flush();
    
  2. Then make the Yoda API request using curl, as you're doing

  3. Once you have the Yoda results, send them to Slack at $response_url using curl, as you're doing.

If you're using FPM then this is what you want - http://php.net/manual/en/function.fastcgi-finish-request.php

Your code would then look like this...

<?php
$response_url = $_POST["response_url"];
$term = rawurlencode($_POST["text"]);
error_log("POST: " . print_r($_POST, 1));

$response = ["response_type"=>"in_channel", "text"=>"Checking, please wait..."];
echo json_encode($response);
header("Content-Type: application/json");
fastcgi_finish_request();

$ch = curl_init();
...

From what I can see in the documentation, you're doing things mostly correctly. Just by echoing anything out, you're already passing a 200 OK message, so no need to do it explicitly. You should check to make sure this isn't a server problem though; is the URL being posted to valid? Not getting mangled by a rewrite rule along the way?

I've made some changes to your code below, including some debugging that will go to your error log (i.e. Apache's error log, by default.) Give it a try, and at the very least you'll have some more debugging details.

<?php
$response_url = $_POST["response_url"];
$term = rawurlencode($_POST["text"]);
error_log("POST: " . print_r($_POST, 1));

ob_end_clean();
ob_start();
$response = ["response_type"=>"in_channel", "text"=>"Checking, please wait..."];
echo json_encode($response);
header("Content-Type: application/json");
header("Content-Length: " . ob_get_size());
ob_end_flush();
flush();

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => "https://yoda.p.mashape.com/yoda?sentence=$term",
    CURLOPT_HTTPHEADER => ["X-Mashape-Key: deMeGoBfMvmshQSemozTqJEY9z0jp1eIhuAjsnx9cQAQsHUifD"],
    CURLOPT_RETURNTRANSFER => true
]);
$yodaresponse = curl_exec($ch);
curl_close($ch);
error_log("Yoda response: $yodaresponse");

$yodajson = json_encode([
    "response_type"=>"in_channel",
    "text"=>$yodaresponse
]);
$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL => $response_url,
    CURLOPT_POST => 1,
    CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => $yodajson
]);

$resp = curl_exec($ch);
curl_close($ch);
error_log("API response: $resp");

Another approach that will work is to use a curl request with a short timeout to spawn a second PHP script. Since my provider has put some restrictions on my PHP environment (e.g. no process spawning) this has been the only approach that has worked for me.

The first script will terminate shortly after and send an HTTP OK back to Slack. The second script will continue running, handle the time consuming processing (e.g. calling external APIs) and finally send the result as delayed response to the response_url.

1st script

This is the curl request in your first script:

<?php>
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, "second.php?redirect_url=$redirect_url");
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_TIMEOUT_MS, 400);     //just some very short timeout        
    curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
    curl_exec($ch);
    curl_close($ch);

    /* send short response back to user, e.g. "Processing your request..." */
?>

The length of the timeouts is arbitrary, however in my tests a very short timeout (e.g. 10ms) did not work.

You will also need to implement a way to transfer input data between the two scripts as illustrated with passing the request_url as URL parameter.

Finally for slash commands Slack requires you to send a short response back to the user.

2nd script

This is how your 2nd script looks like:

<?php
    ignore_user_abort(true);          //very important!
    usleep (500000);                  //to ensure 2nd script responds after 1st
    /* call external API */
    /* send response back to Slack using response_url */
?>

The statement ignore_user_abort(true); is mandatory to ensure your 2nd script keeps running after the curl timeout.

The usleep with 0.5 secs is to ensure that the 2nd script responds after the first, but not mandatory for this solution to work.

The example is based on one answer of the "Continue PHP execution after sending HTTP response" question.

Posting an answer as I don't have enough reputation to post comments...

I've had the same problem, and then I realized that Slack treats requests and responses differently. Specifically, HTTP request and response differ in their first line.

HTTP request example:

GET /hello.htm HTTP/1.1
User-Agent: Mozilla/4.0 (compatible; MSIE5.01; Windows NT)
Host: www.tutorialspoint.com
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: Keep-Alive

HTTP response example:

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache/2.2.14 (Win32)
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
Content-Length: 11
Content-Type: text/xml
Connection: Closed

hello there

If you can access raw bytes to be sent in PHP (never used PHP, so not familiar), just make it look like a response, rather than a request. Otherwise, send a response immediately, then do the work that you need, and send a request with the new message. This can be done in number of ways, one of which was outlined by @miken32, I opted out for invoking a background process in python.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!