20

I'm trying to create a fire and forget method in PHP so that I can POST data to a web server and not have wait for a response. I read that this could be achieved by using CURL like in the following code:

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
curl_exec($ch);
curl_close($ch);

However I don't think it works as I expect. For example if the URL I send the request to has an error it causes my script to throw an error as well. If it was fire and forget I would expect that not to happen.

Can anyone tell me whether I'm doing something wrong or offer an alternative suggestion. I'm using Windows locally and Linux for dev, staging and production environments.

UPDATE

I have found an alternative solution here: http://blog.markturansky.com/archives/205

I've cleaned it up into the code below:

function curl_post_async($url, $params = array())
{
    // create POST string   
    $post_params = array();
    foreach ($params as $key => &$val) 
    {
        $post_params[] = $key . '=' . urlencode($val);
    }
    $post_string = implode('&', $post_params);

    // get URL segments
    $parts = parse_url($url);

    // workout port and open socket
    $port = isset($parts['port']) ? $parts['port'] : 80;
    $fp = fsockopen($parts['host'], $port, $errno, $errstr, 30);

    // create output string
    $output  = "POST " . $parts['path'] . " HTTP/1.1\r\n";
    $output .= "Host: " . $parts['host'] . "\r\n";
    $output .= "Content-Type: application/x-www-form-urlencoded\r\n";
    $output .= "Content-Length: " . strlen($post_string) . "\r\n";
    $output .= "Connection: Close\r\n\r\n";
    $output .= isset($post_string) ? $post_string : '';

    // send output to $url handle
    fwrite($fp, $output);
    fclose($fp);
}

This one seems to work better for me.

Is it a valid solution?

diggersworld
  • 12,770
  • 24
  • 84
  • 119
  • What you are doing here, is just sending the request and closing the connection. So its not as async as you would think it is. You could fork the process, or open a new one. PHP is not that multitask capable. – EJTH Jan 30 '13 at 12:34
  • Are you using a webserver or CLI ? – EJTH Jan 30 '13 at 12:47
  • This is using a webserver. – diggersworld Jan 30 '13 at 12:55
  • Do you host yourself, and do you have access to system() command? – EJTH Jan 30 '13 at 13:30
  • We're self hosted using Amazon Web Services, so we control all the machine instances etc and can setup the servers to our needs. If I do not have access to `system()` I can request it to be enabled. – diggersworld Jan 30 '13 at 18:09
  • The "create POST string" part can simply be replaced with: $post_string = http_build_query($params); – AJenbo Oct 22 '19 at 11:19
  • Note: It may not be immediately obvious, but unlike with cURL, this method will not work with https / SSL. – Michael Hinds May 13 '21 at 16:30

2 Answers2

10

Yes, using sockets is the way to go if you don't care about the response from the URL you're calling. This is because socket connection can be terminated straight after sending the request without waiting and this is exactly what you're after - Fire and Forget.

Two notes though:

  1. It's no longer a cURL request, so it's worth renaming the function. :)
  2. It's definitely worth checking whether the socket could've been opened to prevent the script from complaining later when if fails:

    $fp = fsockopen($parts['host'], $port, $errno, $errstr, 30);
    
    if ( ! $fp)
    {
       return FALSE;
    }
    

It's worth linking to the original source of the fsocket() script you're now using:
http://w-shadow.com/blog/2007/10/16/how-to-run-a-php-script-in-the-background/

Michal M
  • 9,322
  • 8
  • 47
  • 63
  • 1
    somewhere I've seen `stream_set_blocking($fp, 0);` before `fwrite($fp, $data);` wouldn't this be necessary here as well? – velop May 25 '17 at 16:08
7

Here is a cleaned up version of diggersworld's code that also handles other HTTP methods then POST and throws meaningful exceptions if the function fails.

/**
 * Send a HTTP request, but do not wait for the response
 *
 * @param string $method The HTTP method
 * @param string $url The url (including query string)
 * @param array $params Added to the URL or request body depending on method
 */
public function sendRequest(string $method, string $url, array $params = []): void
{
    $parts = parse_url($url);
    if ($parts === false)
        throw new Exception('Unable to parse URL');
    $host = $parts['host'] ?? null;
    $port = $parts['port'] ?? 80;
    $path = $parts['path'] ?? '/';
    $query = $parts['query'] ?? '';
    parse_str($query, $queryParts);

    if ($host === null)
        throw new Exception('Unknown host');
    $connection = fsockopen($host, $port, $errno, $errstr, 30);
    if ($connection === false)
        throw new Exception('Unable to connect to ' . $host);
    $method = strtoupper($method);

    if (!in_array($method, ['POST', 'PUT', 'PATCH'], true)) {
        $queryParts = $params + $queryParts;
        $params = [];
    }

    // Build request
    $request  = $method . ' ' . $path;
    if ($queryParts) {
        $request .= '?' . http_build_query($queryParts);
    }
    $request .= ' HTTP/1.1' . "\r\n";
    $request .= 'Host: ' . $host . "\r\n";

    $body = http_build_query($params);
    if ($body) {
        $request .= 'Content-Type: application/x-www-form-urlencoded' . "\r\n";
        $request .= 'Content-Length: ' . strlen($body) . "\r\n";
    }
    $request .= 'Connection: Close' . "\r\n\r\n";
    $request .= $body;

    // Send request to server
    fwrite($connection, $request);
    fclose($connection);
}
AJenbo
  • 229
  • 2
  • 4