From 0780de2625bf8bb3bcb0f88bbbc401b2750ec1bb Mon Sep 17 00:00:00 2001 From: Jay Satiro Date: Fri, 19 Sep 2025 22:12:05 -0400 Subject: [PATCH] examples: add an example for logging failed transfers - Add an example that demonstrates per-transfer verbose logging to memory. The transfer's log is written to disk only if the transfer fails. Closes https://github.com/curl/curl/pull/18668 --- docs/examples/.gitignore | 1 + docs/examples/Makefile.inc | 1 + docs/examples/log_failed_transfers.c | 335 +++++++++++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 docs/examples/log_failed_transfers.c diff --git a/docs/examples/.gitignore b/docs/examples/.gitignore index 6d14f24cd1..788eefe1ef 100644 --- a/docs/examples/.gitignore +++ b/docs/examples/.gitignore @@ -68,6 +68,7 @@ interface ipv6 keepalive localport +log_failed_transfers maxconnects multi-app multi-debugcallback diff --git a/docs/examples/Makefile.inc b/docs/examples/Makefile.inc index 3bb68f25aa..29c4e3e997 100644 --- a/docs/examples/Makefile.inc +++ b/docs/examples/Makefile.inc @@ -84,6 +84,7 @@ check_PROGRAMS = \ ipv6 \ keepalive \ localport \ + log_failed_transfers \ maxconnects \ multi-app \ multi-debugcallback \ diff --git a/docs/examples/log_failed_transfers.c b/docs/examples/log_failed_transfers.c new file mode 100644 index 0000000000..1ec6350510 --- /dev/null +++ b/docs/examples/log_failed_transfers.c @@ -0,0 +1,335 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.se/docs/copyright.html. + * + * You may opt to use, copy, modify, merge, publish, distribute and/or sell + * copies of the Software, and permit persons to whom the Software is + * furnished to do so, under the terms of the COPYING file. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + * SPDX-License-Identifier: curl + * + ***************************************************************************/ +/* + * Save failed transfer verbose log to disk + * + */ +/* + * + * This example demonstrates per-transfer verbose logging to memory. + * The transfer's log is written to disk only if the transfer fails. + * + */ + +#ifdef _WIN32 +#ifndef _CRT_SECURE_NO_WARNINGS +#define _CRT_SECURE_NO_WARNINGS +#endif +#include +#endif + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#define strcasecmp _stricmp +#define strncasecmp _strnicmp +#define unlink _unlink +#else +#include +#include +#endif + +struct mem { + /* 'buf' points to memory contents that is always zero terminated so that it + can be treated like a string if appropriate. 'recent' points to the most + recent data written to 'buf'. */ + char *buf, *recent; + /* 'len' and 'allocsize' are the length and allocated size of 'buf' */ + size_t len, allocsize; +}; + +struct transfer { + const char *url, *bodyfile, *logfile; + struct mem log; + FILE *bodyfp; + CURL *curl; +}; + +static void mem_reset(struct mem *mem) +{ + free(mem->buf); + mem->buf = NULL; + mem->recent = NULL; + mem->len = 0; + mem->allocsize = 0; +} + +/* expand free buffer space to needed size. return -1 or 'needed'. */ +static int mem_need(struct mem *mem, size_t needed) +{ + char *newbuf; + size_t newsize; + + if(needed > (unsigned)INT_MAX) + return -1; + + if(needed <= (mem->allocsize - mem->len)) + return (int)needed; + + /* min 4k makes reallocations much less frequent when lengths are small */ + newsize = needed < 4096 ? 4096 : needed; + + newsize += mem->len; + + if(newsize < mem->len || newsize > (unsigned)INT_MAX) + return -1; + + newbuf = realloc(mem->buf, newsize); + + if(!newbuf) + return -1; + + if(mem->recent && mem->buf != newbuf) + mem->recent = newbuf + (mem->recent - mem->buf); + + mem->buf = newbuf; + mem->allocsize = newsize; + + return (int)needed; +} + +static int mem_addn(struct mem *mem, const char *buf, size_t len) +{ + if(len + 1 < len || mem_need(mem, len + 1) < 0) + return -1; + mem->recent = mem->buf + mem->len; + memcpy(mem->recent, buf, len); + mem->len += len; + mem->buf[mem->len] = '\0'; + return (int)len; +} + +static int mem_add(struct mem *mem, const char *str) +{ + return mem_addn(mem, str, strlen(str)); +} + +#if defined(__GNUC__) || defined(__clang__) +__attribute__ ((format (printf, 2, 3))) +#endif +static int mem_addf(struct mem *mem, const char *format, ...) +{ + int i, x; + va_list va; + + /* we need about 100 chars or less to write 95% of lines */ + x = 128; + + /* first try: there's probably enough memory to write everything. + second try: there's definitely enough memory to write everything. */ + for(i = 0; i < 2; ++i) { + if(x < 0 || mem_need(mem, (size_t)x + 1) < 0) + break; + + va_start(va, format); + x = vsnprintf(mem->buf + mem->len, mem->allocsize - mem->len, format, va); + va_end(va); + + if(x >= 0 && (size_t)x < (mem->allocsize - mem->len)) { + mem->recent = mem->buf + mem->len; + mem->len += (size_t)x; + return x; + } + +#ifdef _WIN32 + /* Not all versions of Windows CRT vsnprintf are compliant with C99. Some + return -1 if buffer too small. Try _vscprintf to get the needed size. */ + if(!i && x < 0) { + va_start(va, format); + x = _vscprintf(format, va); + va_end(va); + } +#endif + } + + if(mem->buf) + mem->buf[mem->len] = '\0'; + return -1; +} + +static int mydebug(CURL *handle, curl_infotype type, + char *data, size_t size, void *userdata) +{ + struct transfer *t = (struct transfer *)userdata; + static const char s_infotype[CURLINFO_END][3] = { + "* ", "< ", "> ", "{ ", "} ", "{ ", "} " }; + + (void)handle; + + switch(type) { + case CURLINFO_TEXT: + case CURLINFO_HEADER_OUT: + case CURLINFO_HEADER_IN: + /* mem_addn is faster than passing large data as %s to mem_addf */ + mem_addn(&t->log, s_infotype[type], 2); + mem_addn(&t->log, data, size); + if(!size || data[size - 1] != '\n') + mem_add(&t->log, "\n"); + break; + default: + break; + } + + return 0; +} + +static size_t mywrite(char *ptr, size_t size, size_t nmemb, void *userdata) +{ + struct transfer *t = (struct transfer *)userdata; + + return fwrite(ptr, size, nmemb, t->bodyfp); +} + +int main(void) +{ + unsigned i; + int total_failed = 0; + char errbuf[CURL_ERROR_SIZE] = { 0, }; + struct transfer transfer[2]; + + memset(transfer, 0, sizeof(transfer)); + + transfer[0].url = "https://httpbin.org/get"; + transfer[0].bodyfile = "200.txt"; + transfer[0].logfile = "200_transfer_log.txt"; + + transfer[1].url = "https://httpbin.org/status/400"; + transfer[1].bodyfile = "400.txt"; + transfer[1].logfile = "400_transfer_log.txt"; + + if(curl_global_init(CURL_GLOBAL_DEFAULT)) { + fprintf(stderr, "curl_global_init failed\n"); + return 1; + } + + /* You could enable global tracing for extra verbosity when verbosity is + enabled for a transfer. */ +#if 0 + curl_global_trace("all"); +#endif + + for(i = 0; i < sizeof(transfer)/sizeof(transfer[0]); ++i) { + int failed = 0; + struct transfer *t = &transfer[i]; + + t->curl = curl_easy_init(); + + if(!t->curl) { + fprintf(stderr, "curl_easy_init failed\n"); + curl_global_cleanup(); + return 1; + } + + curl_easy_setopt(t->curl, CURLOPT_URL, t->url); + + /* Enable following redirects */ + curl_easy_setopt(t->curl, CURLOPT_FOLLOWLOCATION, 1L); + + /* Enable verbose logging to memory */ + curl_easy_setopt(t->curl, CURLOPT_VERBOSE, 1L); + curl_easy_setopt(t->curl, CURLOPT_DEBUGFUNCTION, mydebug); + curl_easy_setopt(t->curl, CURLOPT_DEBUGDATA, t); + + /* Enable writing the body to a file */ + curl_easy_setopt(t->curl, CURLOPT_WRITEFUNCTION, mywrite); + curl_easy_setopt(t->curl, CURLOPT_WRITEDATA, t); + + /* Enable immediate error on HTTP status codes >= 400 in most cases, + instead of downloading the body to a file */ + curl_easy_setopt(t->curl, CURLOPT_FAILONERROR, 1L); + + /* Enable detailed error messages */ + curl_easy_setopt(t->curl, CURLOPT_ERRORBUFFER, errbuf); + + mem_addf(&t->log, "Downloading %s to file %s\n", t->url, t->bodyfile); + printf("%s", t->log.recent); + + /* Create the body file */ + t->bodyfp = fopen(t->bodyfile, "wb"); + + if(t->bodyfp) { + /* Perform the transfer */ + CURLcode result = curl_easy_perform(t->curl); + + /* Save the body file */ + fclose(t->bodyfp); + t->bodyfp = NULL; + + if(result == CURLE_OK) { + /* You could retrieve more information about the transfer here via + curl_easy_getinfo and mark the transfer as failed if needed. */ + mem_addf(&t->log, "Transfer successful.\n"); + fprintf(stderr, "%s", t->log.recent); + failed = 0; + } + else { + mem_addf(&t->log, "Transfer failed: (%d) %s\n", result, + (errbuf[0] ? errbuf : curl_easy_strerror(result))); + fprintf(stderr, "%s", t->log.recent); + failed = 1; + } + } + else { + mem_addf(&t->log, "Failed to create body output file %s: %s\n", + t->bodyfile, strerror(errno)); + fprintf(stderr, "%s", t->log.recent); + failed = 1; + } + + if(failed) { + FILE *fp = fopen(t->logfile, "wb"); + + if(fp && t->log.len == fwrite(t->log.buf, 1, t->log.len, fp)) + fprintf(stderr, "Transfer log written to %s\n", t->logfile); + else { + fprintf(stderr, "Failed to write transfer log to %s: %s\n", + t->logfile, strerror(errno)); + } + + if(fp) + fclose(fp); + + /* Depending on how the transfer failed a body file may or may not have + been written, and you may or may not want it. */ + unlink(t->bodyfile); + + ++total_failed; + } + + mem_reset(&t->log); + + curl_easy_cleanup(t->curl); + + t->curl = NULL; + + printf("\n"); + } + + return total_failed ? 1 : 0; +} -- 2.47.3