Last active
August 18, 2026 20:14
-
-
Save jay/e963eed374ce5fc1a5c8ce5b4230dbc7 to your computer and use it in GitHub Desktop.
Use libcurl to test and trace mime POSTs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* Use libcurl to test and trace mime POSTs. | |
| Usage: mimediag2 [datasize [readsize]] | |
| Issue: "Multi-part uploads never send more than one buffersize" | |
| https://github.com/curl/curl/issues/22586 | |
| Reporter says certain size mime POSTs are failing to complete on an embedded | |
| system. The default datasize and readsize (max size to read per callback call) | |
| can be changed by using command line arguments. | |
| * Copyright (C) 2026 Jay Satiro <raysatiro@yahoo.com> | |
| https://curl.haxx.se/docs/copyright.html | |
| https://gist.github.com/jay/e963eed374ce5fc1a5c8ce5b4230dbc7 | |
| */ | |
| #ifdef _WIN32 | |
| #define _CRT_SECURE_NO_WARNINGS | |
| #endif | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #include <errno.h> | |
| #ifndef _WIN32 | |
| #include <sys/time.h> | |
| #include <unistd.h> | |
| #endif | |
| #include <curl/curl.h> | |
| #ifdef _WIN32 | |
| #define SIZE_T_FMT "Iu" | |
| #define WAITMS(x) Sleep(x) | |
| #else | |
| #define SIZE_T_FMT "zu" | |
| #define WAITMS(x) usleep((x) * 1000) | |
| #endif | |
| #if defined(MULTI_POLL) || defined(MULTI_WAIT) || defined(MULTI_LEGACY) | |
| #ifndef TEST_MULTI | |
| #define TEST_MULTI | |
| #endif | |
| #endif | |
| /* AAAAAA data tracking for the mime part read by the read callback */ | |
| struct data { | |
| size_t datasize, readsize, position; | |
| }; | |
| size_t read_callback(char *ptr, size_t size, size_t nmemb, void *userdata) | |
| { | |
| struct data *d = (struct data *)userdata; | |
| size_t readsize = (size * nmemb); | |
| size_t remaining = d->datasize - d->position; | |
| fprintf(stderr, "BEGIN: read_callback requests %" SIZE_T_FMT " bytes\n", | |
| readsize); | |
| if(readsize > d->readsize) | |
| readsize = d->readsize; | |
| if(readsize > remaining) | |
| readsize = remaining; | |
| if(readsize) { | |
| memset(ptr, 'A', readsize); | |
| d->position += readsize; | |
| } | |
| fprintf(stderr, "END: read_callback got %" SIZE_T_FMT " bytes\n", readsize); | |
| return readsize; | |
| } | |
| int seek_callback(void *userdata, curl_off_t offset, int origin) | |
| { | |
| struct data *d = (struct data *)userdata; | |
| fprintf(stderr, "BEGIN: seek_callback requests offset " | |
| "%" CURL_FORMAT_CURL_OFF_T " from origin %s\n", | |
| offset, | |
| (origin == SEEK_CUR ? "SEEK_CUR" : | |
| origin == SEEK_END ? "SEEK_END" : | |
| origin == SEEK_SET ? "SEEK_SET" : | |
| "Unknown")); | |
| if(origin == SEEK_CUR) | |
| offset += (curl_off_t)d->position; | |
| else if(origin == SEEK_END) | |
| offset += (curl_off_t)d->datasize; | |
| else if(origin != SEEK_SET) { | |
| fprintf(stderr, "END: seek_callback FAILED, unknown origin: %d\n", origin); | |
| return CURL_SEEKFUNC_FAIL; | |
| } | |
| if(offset < 0 || offset > (curl_off_t)d->datasize) { | |
| fprintf(stderr, "END: seek_callback FAILED, " | |
| "new absolute position out of range: " | |
| "%" CURL_FORMAT_CURL_OFF_T "\n", offset); | |
| return CURL_SEEKFUNC_FAIL; | |
| } | |
| d->position = (size_t)offset; | |
| fprintf(stderr, "END: seek_callback new absolute position " | |
| "%" SIZE_T_FMT "\n", d->position); | |
| return CURL_SEEKFUNC_OK; | |
| } | |
| int main(int argc, char *argv[]) | |
| { | |
| CURLM *multi = NULL; | |
| CURL *curl; | |
| CURLcode result; | |
| curl_mime *mime; | |
| curl_mimepart *part; | |
| struct data d; | |
| int i, failed; | |
| char errbuf[CURL_ERROR_SIZE] = { 0, }; | |
| /* disable output buffering to lessen the chance of partial lines mixing */ | |
| setvbuf(stdout, NULL, _IONBF, 0); | |
| setvbuf(stderr, NULL, _IONBF, 0); | |
| /* default size of AAAAAA mime part read by the read callback */ | |
| d.readsize = 1024; | |
| d.datasize = d.readsize + 1; | |
| d.position = 0; | |
| printf("Usage: mimediag2 [datasize [readsize]]\n"); | |
| if(argc > 1) { | |
| if(!strcmp("-h", argv[1]) || !strcmp("--help", argv[1])) | |
| return 0; | |
| d.datasize = (size_t)atoi(argv[1]); | |
| if(argc > 2) | |
| d.readsize = (size_t)atoi(argv[2]); | |
| else | |
| d.readsize = d.datasize ? d.datasize : 1; | |
| } | |
| if(!d.readsize) { | |
| fprintf(stderr, "Error: readsize can't be 0\n"); | |
| return 1; | |
| } | |
| if(d.readsize < 100) { | |
| fprintf(stderr, "WARNING: your readsize is small enough to cause " | |
| "server i/o timeout (eg httpbin HTTP response code 400)\n"); | |
| } | |
| printf("Using datasize %" SIZE_T_FMT "\n", d.datasize); | |
| printf("Using readsize %" SIZE_T_FMT "\n", d.readsize); | |
| curl_global_trace("all"); | |
| curl_global_init(CURL_GLOBAL_ALL); | |
| atexit(curl_global_cleanup); | |
| printf("curl_version(): %s\n", curl_version()); | |
| curl = curl_easy_init(); | |
| curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf); | |
| curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); | |
| curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); | |
| curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); | |
| curl_easy_setopt(curl, CURLOPT_URL, "https://httpbin.io/post"); | |
| mime = curl_mime_init(curl); | |
| part = curl_mime_addpart(mime); | |
| curl_mime_name(part, "payload"); | |
| curl_mime_filename(part, "payload.bin"); | |
| curl_mime_type(part, "application/octet-stream"); | |
| curl_mime_data_cb(part, (curl_off_t)d.datasize, | |
| read_callback, seek_callback, NULL, &d); | |
| curl_easy_setopt(curl, CURLOPT_MIMEPOST, mime); | |
| #ifdef TEST_MULTI | |
| multi = curl_multi_init(); | |
| #endif | |
| /* run the transfer at least twice to test data and connection reuse */ | |
| for(i = 1, failed = 0; i <= 2 && !failed; ++i) { | |
| fprintf(stderr, "BEGIN: TRANSFER #%d\n", i); | |
| #ifdef TEST_MULTI | |
| { | |
| int still_running = 1; | |
| curl_multi_add_handle(multi, curl); | |
| while(still_running && !failed) { | |
| CURLMcode mc; | |
| mc = curl_multi_perform(multi, &still_running); | |
| if(mc) { | |
| fprintf(stderr, "Error: curl_multi_perform() failed, code %d: %s\n", | |
| mc, curl_multi_strerror(mc)); | |
| failed = 1; | |
| break; | |
| } | |
| if(!still_running) | |
| break; | |
| #ifdef MULTI_POLL | |
| { | |
| int numfds = 0; | |
| mc = curl_multi_poll(multi, NULL, 0, 1000, &numfds); | |
| if(mc) { | |
| fprintf(stderr, "Error: curl_multi_poll() failed, code %d: %s\n", | |
| mc, curl_multi_strerror(mc)); | |
| failed = 1; | |
| break; | |
| } | |
| fprintf(stderr, "curl_multi_poll returned numfds %d\n", numfds); | |
| } | |
| #elif defined(MULTI_WAIT) | |
| /* Test of curl_multi_wait. Normally we'd use curl_multi_poll without | |
| this additional code. */ | |
| { | |
| int numfds = 0; | |
| int repeats = 0; | |
| mc = curl_multi_wait(multi, NULL, 0, 1000, &numfds); | |
| if(mc) { | |
| fprintf(stderr, "Error: curl_multi_wait() failed, code %d: %s\n", | |
| mc, curl_multi_strerror(mc)); | |
| failed = 1; | |
| break; | |
| } | |
| fprintf(stderr, "curl_multi_wait returned numfds %d\n", numfds); | |
| /* 'numfds' being zero means either a timeout or no file descriptors | |
| to wait for. Try timeout on first occurrence, then assume no file | |
| descriptors and no file descriptors to wait for means wait for | |
| 100 milliseconds. */ | |
| if(!numfds) { | |
| repeats++; /* count number of repeated zero numfds */ | |
| if(repeats > 1) { | |
| fprintf(stderr, "sleeping 100ms\n"); | |
| WAITMS(100); /* sleep 100 milliseconds */ | |
| } | |
| } | |
| else | |
| repeats = 0; | |
| } | |
| #elif defined(MULTI_LEGACY) | |
| /* Test of select. Normally we'd use curl_multi_poll without this | |
| additional code. This code is from docs/examples/multi-legacy.c */ | |
| { | |
| struct timeval timeout; | |
| int rc; /* select() return code */ | |
| fd_set fdread; | |
| fd_set fdwrite; | |
| fd_set fdexcep; | |
| int maxfd = -1; | |
| long curl_timeo = -1; | |
| FD_ZERO(&fdread); | |
| FD_ZERO(&fdwrite); | |
| FD_ZERO(&fdexcep); | |
| /* set a suitable timeout to play around with */ | |
| timeout.tv_sec = 1; | |
| timeout.tv_usec = 0; | |
| mc = curl_multi_timeout(multi, &curl_timeo); | |
| if(mc) { | |
| fprintf(stderr, "Error: curl_multi_timeout() failed, " | |
| "code %d: %s\n", | |
| mc, curl_multi_strerror(mc)); | |
| failed = 1; | |
| break; | |
| } | |
| if(curl_timeo >= 0) { | |
| timeout.tv_sec = curl_timeo / 1000; | |
| if(timeout.tv_sec > 1) | |
| timeout.tv_sec = 1; | |
| else | |
| timeout.tv_usec = (int)(curl_timeo % 1000) * 1000; | |
| } | |
| /* get file descriptors from the transfers */ | |
| mc = curl_multi_fdset(multi, &fdread, &fdwrite, &fdexcep, &maxfd); | |
| if(mc) { | |
| fprintf(stderr, "Error: curl_multi_fdset() failed, " | |
| "code %d: %s\n", | |
| mc, curl_multi_strerror(mc)); | |
| failed = 1; | |
| break; | |
| } | |
| /* On success the value of maxfd is guaranteed to be >= -1. We call | |
| select(maxfd + 1, ...); specially in case of (maxfd == -1) there | |
| are no fds ready yet so we sleep 100ms, which is the minimum | |
| suggested value in the curl_multi_fdset() doc. */ | |
| if(maxfd == -1) { | |
| fprintf(stderr, "sleeping 100ms\n"); | |
| WAITMS(100); | |
| rc = 0; | |
| } | |
| else { | |
| /* Note that on some platforms 'timeout' may be modified by | |
| select(). If you need access to the original value save a copy | |
| beforehand. */ | |
| rc = select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout); | |
| } | |
| if(rc == -1) { | |
| int err = errno; | |
| fprintf(stderr, "Error: select failed, " | |
| "errno %d: %s\n", | |
| err, strerror(err)); | |
| failed = 1; | |
| break; | |
| } | |
| fprintf(stderr, "select returned %s\n", (rc ? "action" : "timeout")); | |
| } | |
| #else | |
| #error "define one of MULTI_POLL, MULTI_WAIT, MULTI_LEGACY to test multi" | |
| #endif | |
| } | |
| /* get the result of our lone transfer in the multi */ | |
| result = CURLE_FAILED_INIT; | |
| if(!failed) { | |
| for(;;) { | |
| int qc; | |
| struct CURLMsg *m = curl_multi_info_read(multi, &qc); | |
| if(!m) | |
| break; | |
| if(m->easy_handle == curl && m->msg == CURLMSG_DONE) { | |
| result = m->data.result; | |
| break; | |
| } | |
| } | |
| } | |
| curl_multi_remove_handle(multi, curl); | |
| } | |
| #else | |
| result = curl_easy_perform(curl); | |
| #endif | |
| if(!result && !failed) { | |
| long code = 0; | |
| double upload_size = 0, upload_speed = 0; | |
| curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &code); | |
| if(code != 200) { | |
| fprintf(stderr, "Error: Unexpected HTTP response code %ld != 200\n", | |
| code); | |
| failed = 1; | |
| } | |
| curl_easy_getinfo(curl, CURLINFO_SPEED_UPLOAD, &upload_speed); | |
| curl_easy_getinfo(curl, CURLINFO_SIZE_UPLOAD, &upload_size); | |
| fprintf(stderr, "Upload rate: %.0f KB/sec\n", upload_speed / 1024); | |
| /* upload_size should be greater than the mime part datasize because | |
| it includes the mime boundary markers */ | |
| if((size_t)upload_size <= d.datasize) { | |
| fprintf(stderr, "Error: Unexpected upload size %" SIZE_T_FMT | |
| " is less than or equal to datasize %" SIZE_T_FMT "\n", | |
| (size_t)upload_size, d.datasize); | |
| failed = 1; | |
| } | |
| } | |
| else { | |
| fprintf(stderr, "Error: (%d) %s\n", (int)result, | |
| errbuf[0] ? errbuf : curl_easy_strerror(result)); | |
| failed = 1; | |
| } | |
| fprintf(stderr, "END: TRANSFER #%d, %s\n", i, failed ? "FAILED" : "OK"); | |
| } | |
| curl_mime_free(mime); | |
| if(multi) | |
| curl_multi_remove_handle(multi, curl); | |
| curl_easy_cleanup(curl); | |
| if(multi) | |
| curl_multi_cleanup(multi); | |
| fprintf(stderr, "%s\n", failed ? "FAIL" : "PASS"); | |
| return failed ? 1 : 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment