Skip to content

ws: write callback invoked twice for the same frame after CURLE_WRITE_ERROR (multi interface) #22627

Description

@swt2c

I did this

Debian discovered this while running the pycurl tests against libcurl 8.22.0-rc2.

The test opens a WebSocket transfer with curl_multi_* and a WRITEFUNCTION that, on receiving a specific frame, calls curl_ws_send() (to reply) and then deliberately returns a short write (fewer bytes than it was handed) to abort the transfer with CURLE_WRITE_ERROR.

Port of the pycurl test to C:

#include <stdio.h>
#include <string.h>
#include <curl/curl.h>

static int cb_calls = 0;
static int sent_calls = 0;

static size_t write_cb(char *ptr, size_t size, size_t nmemb, void *userdata)
{
    CURL *easy = (CURL *)userdata;
    size_t len = size * nmemb;

    fprintf(stderr, "[client] write_cb invoked, len=%zu data=%.*s\n",
            len, (int)len, ptr);
    cb_calls++;

    if (len == 2 && memcmp(ptr, "hi", 2) == 0) {
        size_t sent = 0;
        CURLcode res = curl_ws_send(easy, "ack", 3, &sent, 0, CURLWS_BINARY);
        fprintf(stderr, "[client] ws_send returned res=%d sent=%zu (call #%d)\n",
                res, sent, ++sent_calls);
        /* Deliberately report a short write to abort the transfer. */
        return 0;
    }
    return len;
}

int main(int argc, char **argv)
{
    const char *url = argc > 1 ? argv[1] : "ws://127.0.0.1:9999/greet-and-echo-reply";

    curl_global_trace("ws,write,all");
    curl_global_init(CURL_GLOBAL_DEFAULT);

    CURL *easy = curl_easy_init();
    CURLM *multi = curl_multi_init();

    curl_easy_setopt(easy, CURLOPT_URL, url);
    curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(easy, CURLOPT_WRITEDATA, easy);
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);

    curl_multi_add_handle(multi, easy);

    int still_running = 1;
    while (still_running) {
        CURLMcode mc = curl_multi_perform(multi, &still_running);
        if (mc != CURLM_OK) break;
        if (still_running) {
            curl_multi_wait(multi, NULL, 0, 100, NULL);
        }
    }

    int msgq = 0;
    CURLMsg *msg;
    while ((msg = curl_multi_info_read(multi, &msgq))) {
        if (msg->msg == CURLMSG_DONE) {
            fprintf(stderr, "[client] transfer done, result=%d (%s)\n",
                    msg->data.result, curl_easy_strerror(msg->data.result));
        }
    }

    fprintf(stderr, "\n=== SUMMARY: write_cb called %d times, ws_send called %d times ===\n",
            cb_calls, sent_calls);

    curl_multi_remove_handle(multi, easy);
    curl_easy_cleanup(easy);
    curl_multi_cleanup(multi);
    curl_global_cleanup();
    return 0;
}

With libcurl 8.22.0-rc2, the write_cb is invoked twice.

I expected the following

write_cb is only invoked once.

Bisection reveals the double callback started with b940cb1.

curl/libcurl version

curl 8.22.0-DEV (Linux) libcurl/8.22.0-DEV OpenSSL/3.5.7 zlib/1.3.1.zlib-ng brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5 nghttp2/1.68.0
Release-Date: [unreleased]
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns mqtt mqtts pop3 pop3s rtsp smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz PSL SSL threadsafe UnixSockets zstd

operating system

Linux 7.1.6-201.fc44.x86_64 #1 SMP PREEMPT_DYNAMIC x86_64 GNU/Linux (Fedora)
gcc (GCC) 16.1.1 20260515 (Red Hat 16.1.1-2)
OpenSSL 3.5.7

Activity

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions