基于libcurl的 C/C++应用在发起HTTP POST请求时返回错误的响应

后端开发 2026-07-10

我使用以下这段代码从服务器下载数据:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
#include "../persistent/settings.h"
#include "network-helper.h"

#define HTTP_REQUEST_MAX_LENGTH     2048

struct MemoryStruct {
    char *memory;
    size_t size;
};

static size_t writeCb(void *contents, size_t size, size_t nmemb, void *userp)
{
    size_t realsize = size * nmemb;
    struct MemoryStruct *mem = (struct MemoryStruct *)userp;

    char *ptr = (char *)realloc(mem->memory, mem->size + realsize + 1);
    if(!ptr)
    {
        /* out of memory! */
        printf("not enough memory (realloc returned NULL)\n");
        return 0;
    }

    mem->memory = ptr;
    memcpy(&(mem->memory[mem->size]), contents, realsize);
    mem->size += realsize;
    mem->memory[mem->size] = 0;
    printf("Curl callback:\r\n")
    for (size_t i = 0; i < realsize; i++)
    {
        printf("%02X", contents[i]);
    }
    printf("\r\n");
    return realsize;
}

int16_t httpPostRequest(char *endpoint, char * additionalFormData, uint8_t * response, uint16_t maxResponseLength)
{
    CURL *curl;
    CURLcode res;
    size_t responseLength = 0;

    struct MemoryStruct chunk;
    static char url[256];
    static char request[HTTP_REQUEST_MAX_LENGTH];
    memset(url, 0x00, sizeof(url));
    // serverUrls.main, authId, token are elsewhere
    sprintf(url, "https://%s%s", serverUrls.main, endpoint);
    memset(request, 0x00, sizeof(request));
    sprintf(request, "authId=%s&token=%s%s", authId, token, additionalFormData);

    printf("httpPostRequest, url = %s, request = %s\r\n", url, request);

    res = curl_global_init(CURL_GLOBAL_ALL);
    if(res)
    {
        printf("failed to initialize libcurl\r\n");
        return (int)res;
    }

    chunk.memory = (char *)malloc(1);  /* grown as needed by realloc above */
    chunk.size = 0;    /* no data at this point */

    curl = curl_easy_init();
    if(curl)
    {
        curl_easy_setopt(curl, CURLOPT_URL, url);

        /* send all data to this function  */
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writeCb);

        /* we pass our 'chunk' struct to the callback function */
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *)&chunk);

        /* some servers do not like requests that are made without a user-agent
        field, so we provide one */
        curl_easy_setopt(curl, CURLOPT_USERAGENT, "RPI-HTTP-Client/Curl");

        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request);

        /* if we do not provide POSTFIELDSIZE, libcurl calls strlen() by itself */
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)strlen(request));

        /* Perform the request, res gets the return code */
        res = curl_easy_perform(curl);
        /* Check for errors */
        if(res != CURLE_OK)
        {
            printf("curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        }
        else
        {
            responseLength = chunk.size;
            memcpy(response, chunk.memory, chunk.size < maxResponseLength ? chunk.size : maxResponseLength);
        }

        /* always cleanup */
        curl_easy_cleanup(curl);
    }

    free(chunk.memory);
    curl_global_cleanup();

    return responseLength;
}

这样的代码在Ubuntu 24.04上编译时似乎按预期工作。但是在树莓派上编译时,当下载的数据大于485字节时会产生错误的结果(可能更大,但测试过的最大有效长度是485)。

以下是一些示例结果:

// print out inside of writeCb, both length and content are the same as expected
Curl callback, chunksize = 787
13037C43198101000100313630692D434C2D23310000000000000000000000000116000202016810CC109411D80E0CFE640032005802B40008071815403801EC76D56900681000000000000000000000A977D5690000021600070700FF002C012C013200CEFF000001005802B40008071815403801EC76D56900FF0000000000000000000000A977D569000101280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000102280013130150005A00E80314000000050003002C01780008071815080701ED76D56900500000000000000000000000A977D5690001032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000201280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000202280013130133004600E80314000000050003002C013C0008071815080701ED76D56900330000000000000000000000A977D5690002032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000301280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000302280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690003032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000401280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000402280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690004032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D56900
// print out of response after calling httpPostRequest function
72019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000402280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690004032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D56900F6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000102280013130150005A00E80314000000050003002C01780008071815080701ED76D56900500000000000000000000000A977D5690001032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000201280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000202280013130133004600E80314000000050003002C013C0008071815080701ED76D56900330000000000000000000000A977D5690002032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000301280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000302280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690003032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D569000401280001010172019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000402280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690004032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D56900

当我比较这两次打印输出时,在调用 int16_t httpPostRequest(char *endpoint, char * additionalFormData, uint8_t * response, uint16_t maxResponseLength) 之后的响应打印中,结尾的155B数据块出现了两次,一次在它们应在的位置的末尾,一次覆盖了数据的起始部分。

72019001DC0540013CF6030005002C013C0008071815080701ED76D56900720100000000000000000000A977D569000402280013130132004600E80314000000050003002C013C0008071815080701ED76D56900320000000000000000000000A977D5690004032800141401E803E803E80300000000320001002C01B40008071815080701ED76D56900E80300000000000000000000A977D56900

一种可能的原因是应用程序先下载了一个632B的数据块,然后再下载一个155B的数据块,这很可能是代码某处的一个错误;这个后续的数据块被复制了两次,一次复制后又覆盖了数据的起始部分。

printf("Curl callback:\r\n")writeCb 内部的每次对 httpPostRequest 调用中只调用一次。这似乎表明 writeCb 也只被调用一次,应用程序一次性下载了整个787B。

我有点困惑,这到底是我的一个bug,还是某处存在未定义行为?

解决方案

正如 [John Bollinger] 所建议的,真正的问题并不在 httpPostRequestcurl 之外。curl获取的响应随后被复制到一个新结构中:

DownloadedObject downloadedObject;
memcpy((void *)&downloadedObject, response, responseLength);

此后,response 的内容发生了变化,结尾的155B数据块被复制到了开头。

我忽略了 sizeof(DownloadedObject) = 571 有时可能小于 responseLength(在本例中为787)的可能性。这在桌面上运行Ubuntu 24.04时不会造成问题,但在RAM有限的树莓派上或某些与编译器相关的未知行为中就会表现出来。

问题通过调整 struct DownloadedObject 的定义以容纳从服务器下载的最大可能大小来修复,并添加了如下的保护措施:

memcpy((void *)&downloadedObject, response,\
           responseLength < sizeof(DownloadedObject) ? \ 
           responseLength : sizeof(DownloadedObject));

我本应先尝试构造一个最小可复现的测试代码,以免浪费大家的时间。抱歉。

备选方案

httpPostRequest 在拷贝长度被 chunk.size < maxResponseLength ? chunk.size : maxResponseLength 限制时返回的长度是错误的。在这种情况下它应该返回 maxResponseLength。你可以把代码简化为:

            responseLength = chunk.size < maxResponseLength ? chunk.size : maxResponseLength;
            memcpy(response, chunk.memory, responseLength);

请注意,这只是解决了缓冲区溢出的问题。它会导致返回的响应被截断,如果响应中的额外字节里包含重要信息,可能会成为问题。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章