Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

curl responses mixed up with pcnt_fork #10633

Open
mrsuh opened this issue Feb 20, 2023 · 6 comments
Open

curl responses mixed up with pcnt_fork #10633

mrsuh opened this issue Feb 20, 2023 · 6 comments

Comments

@mrsuh
Copy link

mrsuh commented Feb 20, 2023

Description

Hi!
I have a PHP daemon with two forks. Each fork makes an HTTP requests to a remote server.
Sometimes(often) responses in forks are mixed up.

How to reproduce

Remote server

php -v
PHP 8.1.16 (cli) (built: Feb 14 2023 18:59:41) (NTS gcc x86_64)
Copyright (c) The PHP Group
Zend Engine v4.1.16, Copyright (c) Zend Technologies

server/index.php

<?php

switch ($_SERVER['REQUEST_URI']) {
    case '/200':
        http_response_code(200);
        break;
    case '/404':
        http_response_code(404);
        break;
    default:
        http_response_code(500);
}
symfony serve --no-tls --port=9999 --dir=server
 [OK] Web server listening
      The Web server is using PHP CGI 8.1.16
      http://127.0.0.1:9999
[Web Server ] Feb 15 15:02:25 |DEBUG  | PHP    Reloading PHP versions
[Web Server ] Feb 15 15:02:25 |DEBUG  | PHP    Using PHP version 8.1.16 (from default version in $PATH)
[Web Server ] Feb 15 15:02:25 |INFO   | PHP    listening path="/usr/bin/php-cgi" php="8.1.16" port=11238
[Web Server ] Feb 15 15:02:28 |INFO   | SERVER GET  (200) /200 ip=""
[Web Server ] Feb 15 15:02:28 |INFO   | SERVER GET  (200) /200
[Web Server ] Feb 15 15:02:28 |WARN   | SERVER GET  (404) /404
[Web Server ] Feb 15 15:02:29 |INFO   | SERVER GET  (200) /200
[Web Server ] Feb 15 15:02:29 |WARN   | SERVER GET  (404) /404
[Web Server ] Feb 15 15:02:30 |WARN   | SERVER GET  (404) /404
[Web Server ] Feb 15 15:02:30 |INFO   | SERVER GET  (200) /200
[Web Server ] Feb 15 15:02:31 |WARN   | SERVER GET  (404) /404
[Web Server ] Feb 15 15:02:31 |INFO   | SERVER GET  (200) /200
curl 'http://{remote_server}:9999/200' -I
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
X-Powered-By: PHP/8.1.16

curl 'http://{remote_server}:9999/404' -I
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=UTF-8
X-Powered-By: PHP/8.1.16

Local server

php -v
PHP 8.1.0 (cli) (built: Dec  2 2021 12:37:35) (ZTS)
Copyright (c) The PHP Group
Zend Engine v4.1.0, Copyright (c) Zend Technologies

client/test.php

<?php

$url200 = 'http://{remote_server}:9999/200';
$url404 = 'http://{remote_server}:9999/404';

function request(\CurlMultiHandle $multi, string $url): int
{
    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_NOBODY, true);

    curl_multi_add_handle($multi, $ch);
    do {
        $status = curl_multi_exec($multi, $active);
        if ($active) {
            curl_multi_select($multi);
        }
    } while ($active && $status === CURLM_OK);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_multi_remove_handle($multi, $ch);

    return (int)$httpCode;
}

$multi = curl_multi_init();

request($multi, $url200);// the request before pcntl_fork is important to reproduce error

$pid = pcntl_fork();
if ($pid === -1) {
    exit(1);
}

if ($pid) {
    $status = 0;
    while (pcntl_wait($status, WNOHANG | WUNTRACED) === 0) {
        $httpCode = request($multi, $url200);
        if ($httpCode !== 200) {
            printf("GET /200 - %d\n", $httpCode);
            break;
        }
        sleep(1);
    }
    posix_kill($pid, SIGKILL);
} else {
    while (true) {
        $httpCode = request($multi, $url404);
        if ($httpCode !== 404) {
            printf("GET /404 - %d\n", $httpCode);
            break;
        }

        sleep(1);
    }
}

Resulted in this output:

GET /404 - 200
GET /200 - 0 //timeout

But I expected this output instead:

GET /404 - 404
GET /200 - 200

Error only reproduces with a remote(not local) server.
I tested it with two different remote servers.

PHP Version

8.1.0

Operating System

macOs BigSur 11.7.4

@KapitanOczywisty
Copy link

I wonder If curl is trying to reuse the same connection in different forks: can you check verbose logs?

curl_setopt($ch, CURLOPT_VERBOSE, true);
$streamVerboseHandle = fopen('php://temp', 'w+');
curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);

And after exec:

rewind($streamVerboseHandle);
echo stream_get_contents($streamVerboseHandle);

@mrsuh
Copy link
Author

mrsuh commented Feb 20, 2023

I changed the request function to this:

function request(\CurlMultiHandle $multi, string $url): int
{
    $ch = curl_init();

    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_TIMEOUT, 5);
    curl_setopt($ch, CURLOPT_HEADER, true);
    curl_setopt($ch, CURLOPT_NOBODY, true);
    curl_setopt($ch, CURLOPT_VERBOSE, true);
    $streamVerboseHandle = fopen('php://temp', 'w+');
    curl_setopt($ch, CURLOPT_STDERR, $streamVerboseHandle);

    curl_multi_add_handle($multi, $ch);
    do {
        $status = curl_multi_exec($multi, $active);
        if ($active) {
            curl_multi_select($multi);
        }
    } while ($active && $status === CURLM_OK);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    rewind($streamVerboseHandle);
    echo stream_get_contents($streamVerboseHandle);
    curl_multi_remove_handle($multi, $ch);

    return (int)$httpCode;
}

output:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
X-Powered-By: PHP/8.1.16
Date: Mon, 20 Feb 2023 10:49:26 GMT

*   Trying {remote_server}:9999...
* Connected to {remote_server} ({remote_server}) port 9999 (#0)
> HEAD /200 HTTP/1.1
Host: {remote_server}:9999
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
< X-Powered-By: PHP/8.1.16
< Date: Mon, 20 Feb 2023 10:49:26 GMT
<
* Connection #0 to host {remote_server} left intact
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
X-Powered-By: PHP/8.1.16
Date: Mon, 20 Feb 2023 10:49:26 GMT

* Found bundle for host {remote_server}: 0x56478f9ac6d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host {remote_server}
* Connected to {remote_server} ({remote_server}) port 9999 (#0)
> HEAD /404 HTTP/1.1
Host: {remote_server}:9999
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Content-Type: text/html; charset=UTF-8
< X-Powered-By: PHP/8.1.16
< Date: Mon, 20 Feb 2023 10:49:26 GMT
<
* Excess found: excess = 129 url = /404 (zero-length body)
* Connection #0 to host {remote_server} left intact
GET /404 - 200
* Found bundle for host {remote_server}: 0x56478f9ac6d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) with host {remote_server}
* Connected to {remote_server} ({remote_server}) port 9999 (#0)
> HEAD /200 HTTP/1.1
Host: {remote_server}:9999
Accept: */*

* Operation timed out after 5001 milliseconds with 0 bytes received
* Closing connection 0
GET /200 - 0

@KapitanOczywisty
Copy link

Yup, that exactly what is happening, one instance is consuming both responses.

I don't know enough about curl to say if it's a bug or misuse or won't fix. Probably curl connection bundles should be dropped while forking.

@iluuu1994
Copy link
Member

@adoy Can you help here?

@adoy
Copy link
Member

adoy commented Mar 23, 2023

@iluuu1994 I'll try to have a look and see what I can find later this week.

@mrsuh
Copy link
Author

mrsuh commented Apr 6, 2023

Hi guys.
I reproduce the error in C and with a local server!

server.js

const http = require('http');

http.createServer(function(request, response) {
    let url = new URL(request.url, 'http://0.0.0.0:8888');
    switch (true) {
        case url.pathname.startsWith('/200'):
            response.statusCode = 200;
            response.end();
            break;
        case url.pathname.startsWith('/404'):
            response.statusCode = 404;
            response.end();
            break
        default:
            response.statusCode = 500;
            response.end();
    }

}).listen(8888, (err) => {
    if (err) {
        return console.log('something bad happened', err);
    }
    console.log(`server is listening on 8888`);
});

main.c

#include <stdlib.h>
#include <stdio.h>
#include <curl/curl.h>
#include <curl/multi.h>
#include <unistd.h>

size_t curl_write_function(void *ptr, size_t size, size_t nmemb, void *stream)
{
    return size * nmemb;
}

int request(CURLM *multi, const char *url)
{
    CURL *curl = curl_easy_init();

    curl_easy_setopt(curl, CURLOPT_URL, url);
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 1);
    curl_easy_setopt(curl, CURLOPT_HEADER, 1L);
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
    curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_function);
    curl_multi_add_handle(multi, curl);
    int still_running;
    do {
        CURLMcode mc = curl_multi_perform(multi, &still_running);
        if(!mc && still_running) {
            curl_multi_poll(multi, NULL, 0, 200, NULL);
        }
    } while (still_running);
    long response_code;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
    curl_multi_remove_handle(multi, curl);
    curl_easy_cleanup(curl);

    return (int)response_code;
}

int main()
{
    const char* url200 = "http://127.0.0.1:8888/200";
    const char* url404 = "http://127.0.0.1:8888/404";

    CURLM *multi = curl_multi_init();

    request(multi, url200);// the request before fork() is important to reproduce the error

    pid_t pid = fork();
    if(pid == -1) {
        return 1;
    }

    if (pid == 0) {
        while(1) {
            int response_code = request(multi, url200);
            if(response_code != 200) {
                printf("GET /200 - %d\n", response_code);
                break;
            }
        }
    } else {
        while(1) {
            int response_code = request(multi, url404);
            if(response_code != 404) {
                printf("GET /404 - %d\n", response_code);
                break;
            }
            sleep(1);
        }
    }

    return 0;
}

start the server:

node server.js

reproduce the error:

gcc main.c -o main -lcurl
./main
GET /200 - 404
GET /404 - 200

I hope this helps you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants