Verbose operations

Okay, we just showed how to get the error as a human readable text as that is an excellent help to figure out what went wrong in a particular transfer and often explains why it can be done like that or what the problem is for the moment.
The next lifesaver when writing libcurl applications that everyone needs to know about and needs to use extensively, at least while developing libcurl applications or debugging libcurl itself, is to enable verbose mode with CURLOPT_VERBOSE:
CURLcode ret = curl_easy_setopt(handle, CURLOPT_VERBOSE, 1L);
When libcurl is told to be verbose it outputs transfer-related details and information to stderr while the transfer is ongoing. This is awesome to figure out why things fail and to learn exactly what libcurl does when you ask it different things. You can redirect the output elsewhere by changing stderr with CURLOPT_STDERR or you can get even more info in a fancier way with the debug callback (explained further in a later section).

Trace everything

Verbose is certainly fine, but sometimes you need more. libcurl also offers a trace callback that in addition to showing you all the stuff the verbose mode does, it also passes on all data sent and received so that your application gets a full trace of everything.
The sent and received data passed to the trace callback is given to the callback in its unencrypted form, which can be handy when working with TLS or SSH based protocols when capturing the data off the network for debugging is not practical.
When you set the CURLOPT_DEBUGFUNCTION option, you still need to have CURLOPT_VERBOSE enabled but with the trace callback set libcurl uses that callback instead of its internal handling.
The trace callback should match a prototype like this:
int my_trace(CURL *handle, curl_infotype type, char *data, size_t size,
void *user);
handle is the easy handle it concerns, type describes the particular data passed to the callback (data in/out, header in/out, TLS data in/out and text), data is a pointer pointing to the data being size number of bytes. user is the custom pointer you set with CURLOPT_DEBUGDATA.
The data pointed to by data is not null terminated, but is exactly of the size as told by the size argument.
The callback must return 0 or libcurl considers it an error and aborts the transfer.
On the curl website, we host an example called debug.c that includes a simple trace function to get inspiration from.
There are also additional details in the CURLOPT_DEBUGFUNCTION man page.

Transfer and connection identifiers

As the trace information flow passed to the debug callback is a continuous stream even though your application might make libcurl use a large number of separate connections and different transfers, there are times when you want to see to which specific transfers or connections the various information belong to. To better understand the trace output.
You can then get the transfer and connection identifiers from within the callback:
curl_off_t conn_id;
curl_off_t xfer_id;
res = curl_easy_getinfo(curl, CURLINFO_CONN_ID, &conn_id);
res = curl_easy_getinfo(curl, CURLINFO_XFER_ID, &xfer_id);
They are two separate identifiers because connections can be reused and multiple transfers can use the same connection. Using these identifiers (numbers really), you can see which logs are associated with which transfers and connections.

Trace more

If the default amount of tracing data passed to the debug callback is not enough. Like when you suspect and want to debug a problem in a more fundamental lower protocol level, libcurl provides the curl_global_trace() function for you.
With this function you tell libcurl to also include detailed logging about components that it otherwise does not include by default. Such as details about TLS, HTTP/2 or HTTP/3 protocol bits.
The curl_global_trace() functions takes an argument where you specify a string holding a comma-separated list with the areas you want it to trace. For example, include TLS and HTTP/2 details:
/* log details of HTTP/2 and SSL handling */
The exact set of options varies, but here are some ones to try:
show everything possible
TLS protocol exchange details
HTTP/2 frame information
HTTP/3 frame information
additional ones in future versions
Doing a quick run with all is often a good way to get to see which specific areas that are shown, as then you can do follow-up runs with more specific areas set.