10

I am trying to use Slack Custom command and not pretty sure how to use delayed messages since the Yoda Speak External API takes more than 3 seconds to respond.

I have done the following:

  • Sent the slack command /Yoda in my case and received the reponse_url.
  • Used the following to post the following to the response URL.
$data_string = '{"response_type": "in_channel", "text":"Checking,please wait..."}' ;
$chs = curl_init();
curl_setopt($chs, CURLOPT_URL, $response_url);
curl_setopt($chs, CURLOPT_POST, true);
curl_setopt($chs, CURLOPT_POSTFIELDS, $data_string); 
curl_setopt($chs, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($chs, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($chs, CURLOPT_RETURNTRANSFER, true);
curl_setopt($chs, CURLOPT_POST, 1);
curl_setopt($chs, CURLOPT_HTTPHEADER, array('Content-Type:application/json'));
$results = curl_exec($chs);

enter image description here

  • Now, when I call the Yoda API, it gives the following error "Timeout was reached". I read about delayed responses but not sure how should I proceed from here.
$chsres = curl_init();
curl_setopt($chsres, CURLOPT_URL, "https://yoda.p.mashape.com/yoda?sentence=welcome+to+stack");
curl_setopt($chsres, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($chsres, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($chsres, CURLOPT_VERBOSE, true);
curl_setopt($chsres, CURLOPT_TIMEOUT, 45);
curl_setopt($chsres, CURLOPT_RETURNTRANSFER, true);
curl_setopt($chsres, CURLOPT_HTTPHEADER, array('Content-Type:application/json', "X-Mashape-Key:> deMeGoBfMvmshQSemozTqJEY9z0jp1eIhuAjsnx9cQAQsHUifD"));
$resultchsres = curl_exec($chsres);
echo $resultchsres;

Can someone please let me know how to get rid of the timeout error using delayed responses?

UPDATED CODE:

$response_url = $_POST['response_url'];
$text = $_POST['text'];

$term = str_replace(' ', '+', $text);

//https://paypal.slack.com/services/B0VQMHX8W#service_setup
//initial respond with 200OK for timeout
ignore_user_abort(true);
set_time_limit(0);
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();


    $chsres = curl_init();
    curl_setopt_array($chsres, array(
        CURLOPT_URL => "https://yoda.p.mashape.com/yoda?sentence=$term",
        CURLOPT_SSL_VERIFYPEER => FALSE,
        CURLOPT_SSL_VERIFYHOST => FALSE,
        CURLOPT_VERBOSE => true,
        CURLOPT_RETURNTRANSFER => FALSE,
        CURLOPT_HTTPHEADER => array('Content-Type:application/json', "X-Mashape-Key: deMeGoBfMvmshQSemozTqJEY9z0jp1eIhuAjsnx9cQAQsHUifD"),
        CURLOPT_RETURNTRANSFER => true
    ));
    $yodaresponse = curl_exec($chsres);

    $curl = curl_init();
    curl_setopt_array($curl, array(
        CURLOPT_URL => $response_url,
        CURLOPT_POST => 1,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POSTFIELDS => $yodaresponse
    ));

    $resp = curl_exec($curl);
    var_dump($resp);
    curl_close($curl);

I still get the same error "Darn – that slash command didn't work (error message: Timeout was reached). Manage the command at slash-command"

Vimalnath
  • 6,373
  • 2
  • 26
  • 47
  • 1
    Haven't tried delayed responses yet, but the documentation states you should reply with a `HTTP 200` (within the max of 3000ms), perhaps you can then reply to the same response url later. – Francesco de Guytenaere Mar 24 '16 at 08:39
  • @Ciccio that's the part I am not understanding I already sent "checking, please wait" which is a equivalent 200 ok response. I don't understand what you mean by "you can then reply to the same response url later", should I use the same `response_url` again? – Vimalnath Mar 24 '16 at 08:42
  • 2
    @Vimalnath You sending "checking, please wait" is not equivalent to a 200 OK response to slack. You **need** to respond to the request that Slack is sending to your server with 200 OK – Wilhelm Klopp Mar 25 '16 at 09:21
  • 1
    My understanding is that you need to complete and close the initial http request from slack to your bot with HTTP code 200. Then you bot can continue to run and process the data and later send his result back to the response_url. Just sending back a message while still keeping the initial http request from Slack open will not work. – Erik Kalkoken Mar 25 '16 at 14:36
  • @WilhelmKlopp so just `header("HTTP/1.1 200 OK");` code will do? – Vimalnath Mar 26 '16 at 11:52
  • @Vimalnath It works a little bit differently in Django, but yeah. Something like that should do it. (That's how I do it as well) – Wilhelm Klopp Mar 26 '16 at 12:36
  • @WilhelmKlopp I tried to set http header in my curl request to `response_url`, but does not seem to work, can you share the code that you have used in Django, probably I could understand it better looking at the code ? – Vimalnath Mar 28 '16 at 01:20
  • @Vimalnath Sure. [Here](https://github.com/xoneco/simple-poll/blob/master/main/views.py#L142) it is. But again: You don't solve this problem by CURLing anything. You **need** to respond to the request that Slack sends to **your** server with an HTTP response of status code 200. – Wilhelm Klopp Mar 28 '16 at 09:22
  • I never heard any feedback from the answer I posted. Just wondering if it was helpful or not? Interested to see what solves this problem. – miken32 May 09 '16 at 18:36

5 Answers5

8

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.
Community
  • 1
  • 1
rcoup
  • 5,372
  • 2
  • 32
  • 36
  • Thanks for your answer, can you check my updated code in the question? I still get same error , not sure if I missed anything! – Vimalnath Mar 31 '16 at 02:27
  • I'd suggest using the command-line curl to post to your slash command URL (eg. `curl -H "Content-Type: application/json" --data @body.json http://localhost:8000/slash`, maybe use http://requestb.in/ for the $response_url?), you should be able to see whether the 200 comes back to curl immediately. – rcoup Mar 31 '16 at 07:40
  • 1
    Looks like a sounds approach, but unfortunately does not seam to work. I tested it and it still runs into a timeout. Seams like Slack keeps the request open despite the `flush()`. @rcoup Did you test your approach successfully? – Erik Kalkoken Mar 31 '16 at 10:11
  • @ErikKalkoken does [this one](http://stackoverflow.com/a/15273676/2662) work as an alternate? I did test it, but it's on my home computer so I can't double-check until tomorrow. What PHP/Server are you running? – rcoup Mar 31 '16 at 22:31
  • @rcoup ya, still get timeout issues, I use XAMPP/linux – Vimalnath Apr 01 '16 at 01:39
  • 1
    @croup unfortunately I get the same result. My scripts run on altervista.org with PHP 5.4.40 and apache. – Erik Kalkoken Apr 01 '16 at 15:01
3

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.

Erik Kalkoken
  • 30,467
  • 8
  • 79
  • 114
  • How this will work with POST request? Assume I want to send post request and don't want to wait for response then? – Muhammad Hassaan Apr 20 '18 at 02:42
  • @Hassaan this works with both GET and POST requests. The principle is the same. you set a very small timeout and make sure the script you called is not aborted. – Erik Kalkoken Nov 02 '18 at 13:29
2

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();
...
Dan
  • 21
  • 1
  • Worked for me as well (fixed an out-of-order delivery issue I was seeing with the top ranked answer. – Tornado Jan 23 '19 at 21:07
1

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");
miken32
  • 42,008
  • 16
  • 111
  • 154
-1

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.