A proxy can retrieve HTML successfully and still corrupt an image while copying it into a C++ buffer. Before changing the proxy protocol, check the receive callback and the object passed as its context.

CURLOPT_WRITEDATA supplies a pointer to the write callback. Without a custom callback, libcurl's default writer expects a FILE *. An address such as &response, where response is a std::string, only makes sense when your callback casts that context back to the intended type.

static size_t receive(char* data, size_t size,
                      size_t count, void* context) {
    const size_t bytes = size * count;
    auto* body = static_cast<std::string*>(context);
    try {
        body->append(data, bytes);
    } catch (...) {
        return 0;
    }
    return bytes;
}

Register both halves on the easy handle used for the transfer:

std::string response;
curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, receive);
curl_easy_setopt(handle, CURLOPT_WRITEDATA, &response);

The append call uses the delivered byte count. Callback data is not null-terminated, and an image can contain zero bytes; measuring it with strlen would lose part of the body. A response can also arrive through many callback invocations of different sizes.

Returning the consumed byte count accepts the chunk. Returning a different count aborts the transfer with a write error. The exception guard keeps an allocation failure from escaping through the C callback boundary; the caller must still check the transfer result before treating response as a complete file.

Keep the string alive until the transfer finishes. If an asynchronous socket write then forwards its contents, keep those bytes alive until that write completes as well. For large responses, a bounded streaming arrangement is preferable to accumulating the entire body.

Related articles