-
-
Notifications
You must be signed in to change notification settings - Fork 7k
Description
I did this
I compiled and ran the attached C++ test program (see below) which demonstrates a MIME upload using curl_multi and a custom read callback. The callback returns CURL_READFUNC_PAUSE when data is not ready, and the transfer is unpaused later from another thread using curl_easy_pause(easy, CURLPAUSE_SEND_CONT). The program uses a background thread for the multi loop and a foreground thread to provide data and trigger unpause.
C++ reproducible
/*
* Demonstration of curl bug: MIME callbacks not retriggered after unpause during 100-continue
*
* This program demonstrates an issue where curl_multi does not properly retry MIME read
* callbacks after they are unpaused during the 100-continue flow.
*
* Setup:
* - Background thread runs curl_multi_poll/perform loop
* - MIME read callback pauses when data isn't ready yet
* - Foreground thread provides data and notifies background thread to unpause the transfer
* - Bug: After unpause, curl never calls the MIME callback again, causing a hang
*
* Workaround:
* 1. Add "Expect:" header to disable 100-continue, or
* 2. Before calling curl_easy_pause(...CURLPAUSE_SEND_CONT), also call curl_easy_pause(...CURLPAUSE_SEND)
*
* To compile:
* g++ -o curl_mime_pause_bug curl_mime_pause_bug.cpp -lcurl -lpthread
* cl curl_mime_pause_bug.cpp /link libcurl.lib
*
* To run:
* ./curl_mime_pause_bug
*
* Expected behavior: Request completes successfully
* Actual behavior: Request hangs for around 15s after receiving "100 Continue"
*/
#include <curl/curl.h>
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <cstring>
#include <chrono>
// Shared state between threads
struct StreamState {
std::mutex mutex;
std::condition_variable cv;
char buffer[1024];
size_t buffer_len = 0;
bool want_data = false;
bool data_ready = false;
};
struct MultiContext {
CURL* easy = nullptr;
CURLM* multi = nullptr;
std::atomic<bool> should_stop{ false };
std::atomic<bool> should_unpause{ false };
};
// MIME read callback - runs in background thread
size_t mime_read_callback(char* buffer, size_t size, size_t nitems, void* userdata) {
StreamState* state = static_cast<StreamState*>(userdata);
std::unique_lock<std::mutex> lock(state->mutex);
std::cout << "[Background] MIME callback called, size=" << (size * nitems) << std::endl;
// If we have data ready, return it
if (state->data_ready) {
size_t to_copy = std::min(state->buffer_len, size * nitems);
memcpy(buffer, state->buffer, to_copy);
state->buffer_len -= to_copy;
state->data_ready = false;
std::cout << "[Background] MIME callback returning " << to_copy << " bytes" << std::endl;
return to_copy;
}
// No data ready - pause and request data
std::cout << "[Background] MIME callback pausing (no data ready)" << std::endl;
state->want_data = true;
state->cv.notify_all(); // Wake up foreground thread to provide data
return CURL_READFUNC_PAUSE;
}
// Background thread that runs curl_multi loop
void multi_thread_func(MultiContext* ctx) {
std::cout << "[Background] Multi thread started" << std::endl;
while (!ctx->should_stop.load()) {
// Poll for activity (120 second timeout)
int numfds = 0;
std::cout << "[Background] Calling curl_multi_poll..." << std::endl;
CURLMcode mc = curl_multi_poll(ctx->multi, nullptr, 0, 120000, &numfds);
if (mc != CURLM_OK) {
std::cerr << "[Background] curl_multi_poll error: " << curl_multi_strerror(mc) << std::endl;
break;
}
// Process unpause tasks
if (ctx->should_unpause.exchange(false)) {
std::cout << "[Background] Unpausing CURL handle" << std::endl;
// Workaround 2: Uncomment below
// curl_easy_pause(ctx->easy, CURLPAUSE_SEND);
CURLcode res = curl_easy_pause(ctx->easy, CURLPAUSE_SEND_CONT);
std::cout << "[Background] curl_easy_pause returned: " << res << std::endl;
}
// Perform transfers
int running = 0;
std::cout << "[Background] Calling curl_multi_perform..." << std::endl;
mc = curl_multi_perform(ctx->multi, &running);
if (mc != CURLM_OK) {
std::cerr << "[Background] curl_multi_perform error: " << curl_multi_strerror(mc) << std::endl;
break;
}
std::cout << "[Background] After perform: running=" << running << std::endl;
// Check for completed transfers
int msgs_in_queue;
CURLMsg* msg;
while ((msg = curl_multi_info_read(ctx->multi, &msgs_in_queue))) {
if (msg->msg == CURLMSG_DONE) {
std::cout << "[Background] Transfer completed with result: "
<< curl_easy_strerror(msg->data.result) << std::endl;
ctx->should_stop = true;
}
}
if (running == 0) {
std::cout << "[Background] No more running transfers" << std::endl;
break;
}
}
std::cout << "[Background] Multi thread exiting" << std::endl;
}
// Simulate async data provider in foreground thread
void provide_data(CURL* curl, StreamState* state, MultiContext* ctx) {
std::cout << "[Foreground] Waiting for data request..." << std::endl;
{
std::unique_lock<std::mutex> lock(state->mutex);
state->cv.wait(lock, [state] { return state->want_data; });
state->want_data = false;
}
std::cout << "[Foreground] Got data request, simulating data fetch..." << std::endl;
// Provide data
{
std::lock_guard<std::mutex> lock(state->mutex);
const char* data = "Hello, this is test data for MIME upload!";
state->buffer_len = strlen(data);
memcpy(state->buffer, data, state->buffer_len);
state->data_ready = true;
}
std::cout << "[Foreground] Data ready, requesting unpause..." << std::endl;
// Request unpause in background thread
ctx->should_unpause.store(true);
// Wake up the multi thread
std::cout << "[Foreground] Waking up background thread with curl_multi_wakeup..." << std::endl;
curl_multi_wakeup(ctx->multi);
std::cout << "[Foreground] Waiting for second callback (if it happens)..." << std::endl;
{
std::unique_lock<std::mutex> lock(state->mutex);
// Wait up to 5 seconds for second pause
if (state->cv.wait_for(lock, std::chrono::seconds(5), [state]
{
std::cout << "[Foreground] Checking if paused again: " << state->want_data << std::endl;
return state->want_data;
})) {
std::cout << "[Foreground] Second callback paused, sending EOF" << std::endl;
state->buffer_len = 0;
state->data_ready = true;
state->want_data = false;
// Request unpause again for EOF
ctx->should_unpause.store(true);
curl_multi_wakeup(ctx->multi);
}
else {
std::cout << "[Foreground] WARNING: Second callback never paused! This is the bug." << std::endl;
std::cout << "[Foreground] The transfer is likely hung." << std::endl;
}
}
}
int main() {
std::cout << "=== Curl MIME Pause Bug Demonstration ===" << std::endl;
std::cout << "Testing: curl_multi with paused MIME callbacks during 100-continue" << std::endl;
std::cout << std::endl;
curl_global_trace("ALL");
curl_global_init(CURL_GLOBAL_DEFAULT);
// Create easy handle
CURL* curl = curl_easy_init();
if (!curl) {
std::cerr << "Failed to create CURL handle" << std::endl;
return 1;
}
// Setup stream state
StreamState state;
// Create MIME structure
curl_mime* mime = curl_mime_init(curl);
curl_mimepart* part = curl_mime_addpart(mime);
curl_mime_name(part, "file");
curl_mime_filename(part, "test.txt");
curl_mime_type(part, "text/plain");
curl_mime_data_cb(part, -1, mime_read_callback, nullptr, nullptr, &state);
// Configure easy handle
// Use httpbin.org which supports 100-continue
curl_easy_setopt(curl, CURLOPT_URL, "http://httpbin.org/post");
curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime);
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
// Workaround 1: Uncomment below
// curl_slist* headers = curl_slist_append(nullptr, "Expect:");
// curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
// Create multi handle
MultiContext ctx;
ctx.multi = curl_multi_init();
curl_multi_add_handle(ctx.multi, curl);
ctx.easy = curl;
// Start background thread
std::cout << "[Main] Starting background thread..." << std::endl;
std::thread bg_thread(multi_thread_func, &ctx, &state);
// Provide data from foreground thread
std::cout << "[Main] Starting foreground data provider..." << std::endl;
provide_data(curl, &state, &ctx);
// Wait for completion with timeout
std::cout << "\n[Main] Waiting for background thread to complete (10 second timeout)..." << std::endl;
auto start = std::chrono::steady_clock::now();
while (!ctx.should_stop.load()) {
auto elapsed = std::chrono::steady_clock::now() - start;
if (elapsed > std::chrono::seconds(10)) {
std::cout << "[Main] TIMEOUT! Transfer did not complete." << std::endl;
std::cout << "[Main] This confirms the bug: curl doesn't retry MIME callback after unpause." << std::endl;
ctx.should_stop = true;
curl_multi_wakeup(ctx.multi);
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
bg_thread.join();
// Cleanup
std::cout << "\n[Main] Cleaning up..." << std::endl;
curl_multi_remove_handle(ctx.multi, curl);
curl_multi_cleanup(ctx.multi);
curl_mime_free(mime);
curl_easy_cleanup(curl);
curl_global_cleanup();
std::cout << "\n=== Test Complete ===" << std::endl;
return 0;
}Background
The issue was discovered in nyquest project (specifically this PR and this CI job). It aims to bridge Rust async environments and curl multi handles by running the multi poll loop in a background thread, similar to the threading model in the sample snippet. In order to serve multiple concurrent requests efficiently within a single thread, the pausing/unpausing mechanism plays a critical role here to unblock the thread even when there are slow readers.
I expected the following
After unpausing the transfer, I expected libcurl to call the MIME read callback again so the upload could continue and complete successfully, without hanging.
curl/libcurl version
libcurl 8.14.1
operating system
Windows 11
Ubuntu seems to have the same issue as in this GitHub Action job https://github.com/bdbai/nyquest/actions/runs/18243145568/job/51947690553