From 663f3bb511b3609ce6d6c015e71067bba4950c65 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 11:05:27 +0100 Subject: [PATCH 001/121] feat(transport): add HTTP retry logic with exponential backoff Co-Authored-By: Claude Opus 4.6 --- include/sentry.h | 9 + src/sentry_options.c | 12 + src/sentry_options.h | 1 + src/transports/sentry_http_transport.c | 291 +++++++++++++++++++++++-- 4 files changed, 297 insertions(+), 16 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 25b472aaa..23f202d7c 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2258,6 +2258,15 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs( SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( const sentry_options_t *opts); +/** + * Sets the maximum number of HTTP retry attempts for transient network errors. + * Set to 0 to disable retries (default). + */ +SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retries( + sentry_options_t *opts, int http_retries); +SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retries( + const sentry_options_t *opts); + /** * Enables or disables custom attributes parsing for structured logging. * diff --git a/src/sentry_options.c b/src/sentry_options.c index d9b4e05d2..d8376f0d5 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -876,6 +876,18 @@ sentry_options_set_handler_strategy( #endif // SENTRY_PLATFORM_LINUX +void +sentry_options_set_http_retries(sentry_options_t *opts, int http_retries) +{ + opts->http_retries = http_retries; +} + +int +sentry_options_get_http_retries(const sentry_options_t *opts) +{ + return opts->http_retries; +} + void sentry_options_set_propagate_traceparent( sentry_options_t *opts, int propagate_traceparent) diff --git a/src/sentry_options.h b/src/sentry_options.h index 50aeb057b..1c9a96f45 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -72,6 +72,7 @@ struct sentry_options_s { void *traces_sampler_data; size_t max_spans; bool enable_logs; + int http_retries; // takes the first varg as a `sentry_value_t` object containing attributes // if no custom attributes are to be passed, use `sentry_value_new_object()` bool logs_with_attributes; diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 24b1ba566..71bb01c2e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -3,14 +3,18 @@ #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" +#include "sentry_path.h" #include "sentry_ratelimiter.h" #include "sentry_string.h" #include "sentry_transport.h" +#include "sentry_utils.h" +#include "sentry_uuid.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" #endif +#include #include #define ENVELOPE_MIME "application/x-sentry-envelope" @@ -20,6 +24,9 @@ # define MAX_HTTP_HEADERS 3 #endif +#define RETRY_BACKOFF_BASE_MS 900000 +#define RETRY_STARTUP_DELAY_MS 100 + typedef struct { sentry_dsn_t *dsn; char *user_agent; @@ -29,6 +36,10 @@ typedef struct { int (*start_client)(void *, const sentry_options_t *); sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); + sentry_bgworker_t *bgworker; + sentry_path_t *retry_dir; + sentry_path_t *cache_dir; + int http_retries; } http_transport_state_t; #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -182,6 +193,242 @@ sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) sentry_free(req); } +static void retry_process_task(void *_check_backoff, void *_state); + +static bool +retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, + const char **uuid_out) +{ + char *end; + uint64_t ts = strtoull(filename, &end, 10); + if (*end != '-') { + return false; + } + + const char *count_str = end + 1; + long count = strtol(count_str, &end, 10); + if (*end != '-') { + return false; + } + + const char *uuid_start = end + 1; + size_t tail_len = strlen(uuid_start); + // 36 chars UUID + ".envelope" + if (tail_len != 36 + 9 || strcmp(uuid_start + 36, ".envelope") != 0) { + return false; + } + + *ts_out = ts; + *count_out = (int)count; + *uuid_out = uuid_start; + return true; +} + +static uint64_t +retry_backoff_ms(int count) +{ + int shift = count < 3 ? count : 3; + return (uint64_t)RETRY_BACKOFF_BASE_MS << shift; +} + +static int +compare_retry_paths(const void *a, const void *b) +{ + const sentry_path_t *const *pa = a; + const sentry_path_t *const *pb = b; + return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); +} + +static int +http_send_request( + http_transport_state_t *state, sentry_prepared_http_request_t *req) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + if (!state->send_func(state->client, req, &resp)) { + sentry_free(resp.retry_after); + sentry_free(resp.x_sentry_rate_limits); + return -1; + } + + if (resp.x_sentry_rate_limits) { + sentry__rate_limiter_update_from_header( + state->ratelimiter, resp.x_sentry_rate_limits); + } else if (resp.retry_after) { + sentry__rate_limiter_update_from_http_retry_after( + state->ratelimiter, resp.retry_after); + } else if (resp.status_code == 429) { + sentry__rate_limiter_update_from_429(state->ratelimiter); + } + + sentry_free(resp.retry_after); + sentry_free(resp.x_sentry_rate_limits); + return resp.status_code; +} + +static void +retry_write_envelope( + http_transport_state_t *state, const sentry_envelope_t *envelope) +{ + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + if (sentry_uuid_is_nil(&event_id)) { + return; + } + + uint64_t now = sentry__monotonic_time(); + char uuid_str[37]; + sentry__internal_uuid_as_string(&event_id, uuid_str); + + char filename[128]; + snprintf(filename, sizeof(filename), "%llu-00-%s.envelope", + (unsigned long long)now, uuid_str); + + sentry_path_t *path = sentry__path_join_str(state->retry_dir, filename); + if (path) { + int rv = sentry_envelope_write_to_path(envelope, path); + (void)rv; + sentry__path_free(path); + } + + sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, NULL, + (void *)(intptr_t)1, RETRY_BACKOFF_BASE_MS); +} + +static void +retry_process_task(void *_check_backoff, void *_state) +{ + int check_backoff = (int)(intptr_t)_check_backoff; + http_transport_state_t *state = _state; + + sentry_pathiter_t *piter = sentry__path_iter_directory(state->retry_dir); + if (!piter) { + return; + } + + size_t path_count = 0; + size_t path_cap = 16; + sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (!paths) { + sentry__pathiter_free(piter); + return; + } + + const sentry_path_t *p; + while ((p = sentry__pathiter_next(piter)) != NULL) { + const char *fname = sentry__path_filename(p); + uint64_t ts; + int count; + const char *uuid_start; + if (!retry_parse_filename(fname, &ts, &count, &uuid_start)) { + continue; + } + if (path_count == path_cap) { + path_cap *= 2; + sentry_path_t **tmp + = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (!tmp) { + break; + } + memcpy(tmp, paths, path_count * sizeof(sentry_path_t *)); + sentry_free(paths); + paths = tmp; + } + paths[path_count++] = sentry__path_clone(p); + } + sentry__pathiter_free(piter); + + if (path_count > 1) { + qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); + } + + uint64_t now = sentry__monotonic_time(); + bool files_remain = false; + + for (size_t i = 0; i < path_count; i++) { + const char *fname = sentry__path_filename(paths[i]); + uint64_t ts; + int count; + const char *uuid_start; + retry_parse_filename(fname, &ts, &count, &uuid_start); + + if (check_backoff && (now - ts) < retry_backoff_ms(count)) { + files_remain = true; + continue; + } + + sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); + if (!envelope) { + sentry__path_remove(paths[i]); + continue; + } + + sentry_prepared_http_request_t *req = sentry__prepare_http_request( + envelope, state->dsn, state->ratelimiter, state->user_agent); + int status_code; + if (!req) { + status_code = 0; + } else { + status_code = http_send_request(state, req); + sentry__prepared_http_request_free(req); + } + sentry_envelope_free(envelope); + + if (status_code < 0) { + if (count + 1 >= state->http_retries) { + if (state->cache_dir) { + sentry_path_t *dst + = sentry__path_join_str(state->cache_dir, fname); + if (dst) { + sentry__path_rename(paths[i], dst); + sentry__path_free(dst); + } else { + sentry__path_remove(paths[i]); + } + } else { + sentry__path_remove(paths[i]); + } + } else { + char new_filename[128]; + snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", + (unsigned long long)now, count + 1, uuid_start); + sentry_path_t *new_path + = sentry__path_join_str(state->retry_dir, new_filename); + if (new_path) { + sentry__path_rename(paths[i], new_path); + sentry__path_free(new_path); + } + files_remain = true; + } + } else if (status_code >= 200 && status_code < 300) { + if (state->cache_dir) { + sentry_path_t *dst + = sentry__path_join_str(state->cache_dir, fname); + if (dst) { + sentry__path_rename(paths[i], dst); + sentry__path_free(dst); + } else { + sentry__path_remove(paths[i]); + } + } else { + sentry__path_remove(paths[i]); + } + } else { + sentry__path_remove(paths[i]); + } + } + + for (size_t i = 0; i < path_count; i++) { + sentry__path_free(paths[i]); + } + sentry_free(paths); + + if (files_remain) { + sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, + NULL, (void *)(intptr_t)1, RETRY_BACKOFF_BASE_MS); + } +} + static void http_transport_state_free(void *_state) { @@ -192,6 +439,8 @@ http_transport_state_free(void *_state) sentry__dsn_decref(state->dsn); sentry_free(state->user_agent); sentry__rate_limiter_free(state->ratelimiter); + sentry__path_free(state->retry_dir); + sentry__path_free(state->cache_dir); sentry_free(state); } @@ -207,23 +456,12 @@ http_send_task(void *_envelope, void *_state) return; } - sentry_http_response_t resp; - memset(&resp, 0, sizeof(resp)); + int status_code = http_send_request(state, req); + sentry__prepared_http_request_free(req); - if (state->send_func(state->client, req, &resp)) { - if (resp.x_sentry_rate_limits) { - sentry__rate_limiter_update_from_header( - state->ratelimiter, resp.x_sentry_rate_limits); - } else if (resp.retry_after) { - sentry__rate_limiter_update_from_http_retry_after( - state->ratelimiter, resp.retry_after); - } else if (resp.status_code == 429) { - sentry__rate_limiter_update_from_429(state->ratelimiter); - } + if (status_code < 0 && state->retry_dir) { + retry_write_envelope(state, envelope); } - sentry_free(resp.retry_after); - sentry_free(resp.x_sentry_rate_limits); - sentry__prepared_http_request_free(req); } static int @@ -244,7 +482,27 @@ http_transport_start(const sentry_options_t *options, void *transport_state) } } - return sentry__bgworker_start(bgworker); + int rv = sentry__bgworker_start(bgworker); + if (rv != 0) { + return rv; + } + + if (options->http_retries > 0) { + state->http_retries = options->http_retries; + state->retry_dir + = sentry__path_join_str(options->database_path, "retry"); + if (state->retry_dir) { + sentry__path_create_dir_all(state->retry_dir); + } + if (options->cache_keep) { + state->cache_dir + = sentry__path_join_str(options->database_path, "cache"); + } + sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, + (void *)(intptr_t)0, RETRY_STARTUP_DELAY_MS); + } + + return 0; } static int @@ -316,6 +574,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) http_transport_state_free(state); return NULL; } + state->bgworker = bgworker; sentry_transport_t *transport = sentry_transport_new(http_transport_send_envelope); From 52a273e4b32c3e489dba21194e90f32721e2bf7f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:38:49 +0100 Subject: [PATCH 002/121] fix(retry): filter startup scan by timestamp, create cache dir The deferred startup retry scan (100ms delay) could pick up files written by the current session. Filter by startup_time so only previous-session files are processed. Also ensure the cache directory exists when cache_keep is enabled, since sentry__process_old_runs only creates it conditionally. Co-Authored-By: Claude Opus 4.6 --- examples/example.c | 3 + src/CMakeLists.txt | 2 + src/sentry_retry.c | 263 +++++++++++++++++++ src/sentry_retry.h | 36 +++ src/transports/sentry_http_transport.c | 231 +++-------------- tests/test_integration_http.py | 293 +++++++++++++++++++++ tests/unit/CMakeLists.txt | 1 + tests/unit/test_retry.c | 341 +++++++++++++++++++++++++ tests/unit/tests.inc | 6 + 9 files changed, 982 insertions(+), 194 deletions(-) create mode 100644 src/sentry_retry.c create mode 100644 src/sentry_retry.h create mode 100644 tests/unit/test_retry.c diff --git a/examples/example.c b/examples/example.c index 8fc7db7b3..5c2a0a85b 100644 --- a/examples/example.c +++ b/examples/example.c @@ -659,6 +659,9 @@ main(int argc, char **argv) sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); // 5 days sentry_options_set_cache_max_items(options, 5); } + if (has_arg(argc, argv, "http-retry")) { + sentry_options_set_http_retries(options, 5); + } if (has_arg(argc, argv, "enable-metrics")) { sentry_options_set_enable_metrics(options, true); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 218a9b012..326fd3ceb 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -34,6 +34,8 @@ sentry_target_sources_cwd(sentry sentry_process.h sentry_ratelimiter.c sentry_ratelimiter.h + sentry_retry.c + sentry_retry.h sentry_ringbuffer.c sentry_ringbuffer.h sentry_sampling_context.h diff --git a/src/sentry_retry.c b/src/sentry_retry.c new file mode 100644 index 000000000..fbb8dba89 --- /dev/null +++ b/src/sentry_retry.c @@ -0,0 +1,263 @@ +#include "sentry_retry.h" +#include "sentry_alloc.h" +#include "sentry_envelope.h" +#include "sentry_utils.h" + +#include +#include + +struct sentry_retry_s { + sentry_path_t *retry_dir; + sentry_path_t *cache_dir; + int max_retries; + uint64_t startup_time; +}; + +sentry_retry_t * +sentry__retry_new( + sentry_path_t *retry_dir, sentry_path_t *cache_dir, int max_retries) +{ + sentry_retry_t *retry = SENTRY_MAKE(sentry_retry_t); + if (!retry) { + return NULL; + } + retry->retry_dir = sentry__path_clone(retry_dir); + retry->cache_dir = cache_dir ? sentry__path_clone(cache_dir) : NULL; + retry->max_retries = max_retries; + return retry; +} + +void +sentry__retry_free(sentry_retry_t *retry) +{ + if (!retry) { + return; + } + sentry__path_free(retry->retry_dir); + sentry__path_free(retry->cache_dir); + sentry_free(retry); +} + +void +sentry__retry_set_startup_time(sentry_retry_t *retry, uint64_t startup_time) +{ + retry->startup_time = startup_time; +} + +bool +sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, + int *count_out, const char **uuid_out) +{ + char *end; + uint64_t ts = strtoull(filename, &end, 10); + if (*end != '-') { + return false; + } + + const char *count_str = end + 1; + long count = strtol(count_str, &end, 10); + if (*end != '-') { + return false; + } + + const char *uuid_start = end + 1; + size_t tail_len = strlen(uuid_start); + // 36 chars UUID (with dashes) + ".envelope" + if (tail_len != 36 + 9 || strcmp(uuid_start + 36, ".envelope") != 0) { + return false; + } + + *ts_out = ts; + *count_out = (int)count; + *uuid_out = uuid_start; + return true; +} + +uint64_t +sentry__retry_backoff_ms(int count) +{ + int shift = count < 3 ? count : 3; + return (uint64_t)SENTRY_RETRY_BACKOFF_BASE_MS << shift; +} + +static int +compare_retry_paths(const void *a, const void *b) +{ + const sentry_path_t *const *pa = a; + const sentry_path_t *const *pb = b; + return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); +} + +void +sentry__retry_write_envelope( + sentry_retry_t *retry, const sentry_envelope_t *envelope) +{ + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + if (sentry_uuid_is_nil(&event_id)) { + return; + } + + uint64_t now = sentry__monotonic_time(); + char uuid_str[37]; + sentry_uuid_as_string(&event_id, uuid_str); + + char filename[128]; + snprintf(filename, sizeof(filename), "%llu-00-%s.envelope", + (unsigned long long)now, uuid_str); + + sentry_path_t *path = sentry__path_join_str(retry->retry_dir, filename); + if (path) { + (void)sentry_envelope_write_to_path(envelope, path); + sentry__path_free(path); + } +} + +sentry_path_t ** +sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) +{ + *count_out = 0; + + sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); + if (!piter) { + return NULL; + } + + size_t path_cap = 16; + sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (!paths) { + sentry__pathiter_free(piter); + return NULL; + } + + size_t path_count = 0; + uint64_t now = startup ? 0 : sentry__monotonic_time(); + + const sentry_path_t *p; + while ((p = sentry__pathiter_next(piter)) != NULL) { + const char *fname = sentry__path_filename(p); + uint64_t ts; + int count; + const char *uuid_start; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + continue; + } + if (startup) { + if (retry->startup_time > 0 && ts >= retry->startup_time) { + continue; + } + } else if ((now - ts) < sentry__retry_backoff_ms(count)) { + continue; + } + if (path_count == path_cap) { + path_cap *= 2; + sentry_path_t **tmp + = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (!tmp) { + break; + } + memcpy(tmp, paths, path_count * sizeof(sentry_path_t *)); + sentry_free(paths); + paths = tmp; + } + paths[path_count++] = sentry__path_clone(p); + } + sentry__pathiter_free(piter); + + if (path_count > 1) { + qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); + } + + *count_out = path_count; + return paths; +} + +void +sentry__retry_free_paths(sentry_path_t **paths, size_t count) +{ + if (!paths) { + return; + } + for (size_t i = 0; i < count; i++) { + sentry__path_free(paths[i]); + } + sentry_free(paths); +} + +void +sentry__retry_handle_result( + sentry_retry_t *retry, const sentry_path_t *path, int status_code) +{ + const char *fname = sentry__path_filename(path); + uint64_t ts; + int count; + const char *uuid_start; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + sentry__path_remove(path); + return; + } + + if (status_code < 0) { + if (count + 1 >= retry->max_retries) { + if (retry->cache_dir) { + sentry_path_t *dst + = sentry__path_join_str(retry->cache_dir, fname); + if (dst) { + sentry__path_rename(path, dst); + sentry__path_free(dst); + } else { + sentry__path_remove(path); + } + } else { + sentry__path_remove(path); + } + } else { + uint64_t now = sentry__monotonic_time(); + char new_filename[128]; + snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", + (unsigned long long)now, count + 1, uuid_start); + sentry_path_t *new_path + = sentry__path_join_str(retry->retry_dir, new_filename); + if (new_path) { + sentry__path_rename(path, new_path); + sentry__path_free(new_path); + } + } + } else if (status_code >= 200 && status_code < 300) { + if (retry->cache_dir) { + sentry_path_t *dst = sentry__path_join_str(retry->cache_dir, fname); + if (dst) { + sentry__path_rename(path, dst); + sentry__path_free(dst); + } else { + sentry__path_remove(path); + } + } else { + sentry__path_remove(path); + } + } else { + sentry__path_remove(path); + } +} + +bool +sentry__retry_has_files(const sentry_retry_t *retry) +{ + sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); + if (!piter) { + return false; + } + + const sentry_path_t *p; + while ((p = sentry__pathiter_next(piter)) != NULL) { + const char *fname = sentry__path_filename(p); + uint64_t ts; + int count; + const char *uuid_start; + if (sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + sentry__pathiter_free(piter); + return true; + } + } + sentry__pathiter_free(piter); + return false; +} diff --git a/src/sentry_retry.h b/src/sentry_retry.h new file mode 100644 index 000000000..07a11d93a --- /dev/null +++ b/src/sentry_retry.h @@ -0,0 +1,36 @@ +#ifndef SENTRY_RETRY_H_INCLUDED +#define SENTRY_RETRY_H_INCLUDED + +#include "sentry_boot.h" +#include "sentry_path.h" + +#define SENTRY_RETRY_BACKOFF_BASE_MS 900000 +#define SENTRY_RETRY_STARTUP_DELAY_MS 100 + +typedef struct sentry_retry_s sentry_retry_t; + +sentry_retry_t *sentry__retry_new( + sentry_path_t *retry_dir, sentry_path_t *cache_dir, int max_retries); +void sentry__retry_free(sentry_retry_t *retry); + +void sentry__retry_write_envelope( + sentry_retry_t *retry, const sentry_envelope_t *envelope); + +void sentry__retry_set_startup_time( + sentry_retry_t *retry, uint64_t startup_time); + +sentry_path_t **sentry__retry_scan( + sentry_retry_t *retry, bool startup, size_t *count_out); +void sentry__retry_free_paths(sentry_path_t **paths, size_t count); + +void sentry__retry_handle_result( + sentry_retry_t *retry, const sentry_path_t *path, int status_code); + +bool sentry__retry_has_files(const sentry_retry_t *retry); + +uint64_t sentry__retry_backoff_ms(int count); + +bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, + int *count_out, const char **uuid_out); + +#endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 71bb01c2e..3d5cee33c 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -5,10 +5,10 @@ #include "sentry_options.h" #include "sentry_path.h" #include "sentry_ratelimiter.h" +#include "sentry_retry.h" #include "sentry_string.h" #include "sentry_transport.h" #include "sentry_utils.h" -#include "sentry_uuid.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" @@ -24,9 +24,6 @@ # define MAX_HTTP_HEADERS 3 #endif -#define RETRY_BACKOFF_BASE_MS 900000 -#define RETRY_STARTUP_DELAY_MS 100 - typedef struct { sentry_dsn_t *dsn; char *user_agent; @@ -37,9 +34,7 @@ typedef struct { sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); sentry_bgworker_t *bgworker; - sentry_path_t *retry_dir; - sentry_path_t *cache_dir; - int http_retries; + sentry_retry_t *retry; } http_transport_state_t; #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -195,50 +190,6 @@ sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) static void retry_process_task(void *_check_backoff, void *_state); -static bool -retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, - const char **uuid_out) -{ - char *end; - uint64_t ts = strtoull(filename, &end, 10); - if (*end != '-') { - return false; - } - - const char *count_str = end + 1; - long count = strtol(count_str, &end, 10); - if (*end != '-') { - return false; - } - - const char *uuid_start = end + 1; - size_t tail_len = strlen(uuid_start); - // 36 chars UUID + ".envelope" - if (tail_len != 36 + 9 || strcmp(uuid_start + 36, ".envelope") != 0) { - return false; - } - - *ts_out = ts; - *count_out = (int)count; - *uuid_out = uuid_start; - return true; -} - -static uint64_t -retry_backoff_ms(int count) -{ - int shift = count < 3 ? count : 3; - return (uint64_t)RETRY_BACKOFF_BASE_MS << shift; -} - -static int -compare_retry_paths(const void *a, const void *b) -{ - const sentry_path_t *const *pa = a; - const sentry_path_t *const *pb = b; - return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); -} - static int http_send_request( http_transport_state_t *state, sentry_prepared_http_request_t *req) @@ -268,95 +219,19 @@ http_send_request( } static void -retry_write_envelope( - http_transport_state_t *state, const sentry_envelope_t *envelope) -{ - sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); - if (sentry_uuid_is_nil(&event_id)) { - return; - } - - uint64_t now = sentry__monotonic_time(); - char uuid_str[37]; - sentry__internal_uuid_as_string(&event_id, uuid_str); - - char filename[128]; - snprintf(filename, sizeof(filename), "%llu-00-%s.envelope", - (unsigned long long)now, uuid_str); - - sentry_path_t *path = sentry__path_join_str(state->retry_dir, filename); - if (path) { - int rv = sentry_envelope_write_to_path(envelope, path); - (void)rv; - sentry__path_free(path); - } - - sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, NULL, - (void *)(intptr_t)1, RETRY_BACKOFF_BASE_MS); -} - -static void -retry_process_task(void *_check_backoff, void *_state) +retry_process_task(void *_startup, void *_state) { - int check_backoff = (int)(intptr_t)_check_backoff; + int startup = (int)(intptr_t)_startup; http_transport_state_t *state = _state; - sentry_pathiter_t *piter = sentry__path_iter_directory(state->retry_dir); - if (!piter) { - return; - } - - size_t path_count = 0; - size_t path_cap = 16; - sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); - if (!paths) { - sentry__pathiter_free(piter); + if (!state->retry) { return; } - const sentry_path_t *p; - while ((p = sentry__pathiter_next(piter)) != NULL) { - const char *fname = sentry__path_filename(p); - uint64_t ts; - int count; - const char *uuid_start; - if (!retry_parse_filename(fname, &ts, &count, &uuid_start)) { - continue; - } - if (path_count == path_cap) { - path_cap *= 2; - sentry_path_t **tmp - = sentry_malloc(path_cap * sizeof(sentry_path_t *)); - if (!tmp) { - break; - } - memcpy(tmp, paths, path_count * sizeof(sentry_path_t *)); - sentry_free(paths); - paths = tmp; - } - paths[path_count++] = sentry__path_clone(p); - } - sentry__pathiter_free(piter); - - if (path_count > 1) { - qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); - } - - uint64_t now = sentry__monotonic_time(); - bool files_remain = false; - - for (size_t i = 0; i < path_count; i++) { - const char *fname = sentry__path_filename(paths[i]); - uint64_t ts; - int count; - const char *uuid_start; - retry_parse_filename(fname, &ts, &count, &uuid_start); - - if (check_backoff && (now - ts) < retry_backoff_ms(count)) { - files_remain = true; - continue; - } + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(state->retry, startup, &count); + for (size_t i = 0; i < count; i++) { sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); if (!envelope) { sentry__path_remove(paths[i]); @@ -374,58 +249,14 @@ retry_process_task(void *_check_backoff, void *_state) } sentry_envelope_free(envelope); - if (status_code < 0) { - if (count + 1 >= state->http_retries) { - if (state->cache_dir) { - sentry_path_t *dst - = sentry__path_join_str(state->cache_dir, fname); - if (dst) { - sentry__path_rename(paths[i], dst); - sentry__path_free(dst); - } else { - sentry__path_remove(paths[i]); - } - } else { - sentry__path_remove(paths[i]); - } - } else { - char new_filename[128]; - snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", - (unsigned long long)now, count + 1, uuid_start); - sentry_path_t *new_path - = sentry__path_join_str(state->retry_dir, new_filename); - if (new_path) { - sentry__path_rename(paths[i], new_path); - sentry__path_free(new_path); - } - files_remain = true; - } - } else if (status_code >= 200 && status_code < 300) { - if (state->cache_dir) { - sentry_path_t *dst - = sentry__path_join_str(state->cache_dir, fname); - if (dst) { - sentry__path_rename(paths[i], dst); - sentry__path_free(dst); - } else { - sentry__path_remove(paths[i]); - } - } else { - sentry__path_remove(paths[i]); - } - } else { - sentry__path_remove(paths[i]); - } + sentry__retry_handle_result(state->retry, paths[i], status_code); } - for (size_t i = 0; i < path_count; i++) { - sentry__path_free(paths[i]); - } - sentry_free(paths); + sentry__retry_free_paths(paths, count); - if (files_remain) { + if (sentry__retry_has_files(state->retry)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)1, RETRY_BACKOFF_BASE_MS); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_MS); } } @@ -439,8 +270,7 @@ http_transport_state_free(void *_state) sentry__dsn_decref(state->dsn); sentry_free(state->user_agent); sentry__rate_limiter_free(state->ratelimiter); - sentry__path_free(state->retry_dir); - sentry__path_free(state->cache_dir); + sentry__retry_free(state->retry); sentry_free(state); } @@ -459,8 +289,10 @@ http_send_task(void *_envelope, void *_state) int status_code = http_send_request(state, req); sentry__prepared_http_request_free(req); - if (status_code < 0 && state->retry_dir) { - retry_write_envelope(state, envelope); + if (status_code < 0 && state->retry) { + sentry__retry_write_envelope(state->retry, envelope); + sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, + NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_MS); } } @@ -488,18 +320,29 @@ http_transport_start(const sentry_options_t *options, void *transport_state) } if (options->http_retries > 0) { - state->http_retries = options->http_retries; - state->retry_dir + sentry_path_t *retry_dir = sentry__path_join_str(options->database_path, "retry"); - if (state->retry_dir) { - sentry__path_create_dir_all(state->retry_dir); + if (retry_dir) { + sentry__path_create_dir_all(retry_dir); + sentry_path_t *cache_dir = NULL; + if (options->cache_keep) { + cache_dir + = sentry__path_join_str(options->database_path, "cache"); + if (cache_dir) { + sentry__path_create_dir_all(cache_dir); + } + } + state->retry = sentry__retry_new( + retry_dir, cache_dir, options->http_retries); + sentry__path_free(cache_dir); + sentry__path_free(retry_dir); } - if (options->cache_keep) { - state->cache_dir - = sentry__path_join_str(options->database_path, "cache"); + if (state->retry) { + sentry__retry_set_startup_time( + state->retry, sentry__monotonic_time()); + sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, + (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); } - sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, - (void *)(intptr_t)0, RETRY_STARTUP_DELAY_MS); } return 0; diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index ec008315a..f14c1645a 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -838,3 +838,296 @@ def test_native_crash_http(cmake, httpserver): assert_minidump(envelope) assert_breadcrumb(envelope) assert_attachment(envelope) + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_on_network_error(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + # unreachable port triggers CURLE_COULDNT_CONNECT + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "capture-event"], + env=env_unreachable, + ) + + assert retry_dir.exists() + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 1 + assert "-00-" in str(retry_files[0].name) + + # retry on next run with working server + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "no-setup"], + env=env_reachable, + ) + assert waiting.result + + assert len(httpserver.log) == 1 + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + assert_meta(envelope, integration="inproc") + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_multiple_attempts(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run(tmp_path, "sentry_example", ["log", "http-retry", "capture-event"], env=env) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 1 + assert "-00-" in str(retry_files[0].name) + + run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 1 + assert "-01-" in str(retry_files[0].name) + + run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 1 + assert "-02-" in str(retry_files[0].name) + + # exhaust remaining retries (max 5) + for i in range(3): + run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + + # discarded after max retries (cache_keep not enabled) + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 0 + + cache_dir = tmp_path.joinpath(".sentry-native/cache") + cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + assert len(cache_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_with_cache_keep(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "cache-keep", "capture-event"], + env=env_unreachable, + ) + + assert retry_dir.exists() + assert len(list(retry_dir.glob("*.envelope"))) == 1 + + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "cache-keep", "no-setup"], + env=env_reachable, + ) + assert waiting.result + + assert len(list(retry_dir.glob("*.envelope"))) == 0 + assert len(list(cache_dir.glob("*.envelope"))) == 1 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_cache_keep_max_attempts(cmake): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "cache-keep", "capture-event"], + env=env, + ) + + assert retry_dir.exists() + assert len(list(retry_dir.glob("*.envelope"))) == 1 + + for _ in range(5): + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "cache-keep", "no-setup"], + env=env, + ) + + assert len(list(retry_dir.glob("*.envelope"))) == 0 + assert cache_dir.exists() + assert len(list(cache_dir.glob("*.envelope"))) == 1 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_http_error_discards_envelope(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( + "Internal Server Error", status=500 + ) + + with httpserver.wait(timeout=10) as waiting: + run(tmp_path, "sentry_example", ["log", "capture-event"], env=env) + assert waiting.result + + # HTTP errors discard, not retry + retry_files = list(retry_dir.glob("*.envelope")) if retry_dir.exists() else [] + assert len(retry_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_rate_limit_discards_envelope(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( + "Rate Limited", status=429, headers={"retry-after": "60"} + ) + + with httpserver.wait(timeout=10) as waiting: + run(tmp_path, "sentry_example", ["log", "capture-event"], env=env) + assert waiting.result + + # 429 discards, not retry + retry_files = list(retry_dir.glob("*.envelope")) if retry_dir.exists() else [] + assert len(retry_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_multiple_success(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "capture-multiple"], + env=env_unreachable, + ) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 10 + + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + for _ in range(10): + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( + "OK" + ) + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "no-setup"], + env=env_reachable, + ) + assert waiting.result + + assert len(httpserver.log) == 10 + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_multiple_network_error(cmake): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "capture-multiple"], + env=env, + ) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 10 + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "no-setup"], + env=env, + ) + + # all envelopes retried, all bumped to retry 1 + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 10 + retry_1 = [f for f in retry_files if "-01-" in f.name] + assert len(retry_1) == 10 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_multiple_rate_limit(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + retry_dir = tmp_path.joinpath(".sentry-native/retry") + + unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "capture-multiple"], + env=env_unreachable, + ) + + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 10 + + # rate limit response followed by discards for the rest (rate limiter + # kicks in after the first 429) + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_request("/api/123456/envelope/").respond_with_data( + "Rate Limited", status=429, headers={"retry-after": "60"} + ) + + run( + tmp_path, + "sentry_example", + ["log", "http-retry", "no-setup"], + env=env_reachable, + ) + + # first envelope gets 429, rest are discarded by rate limiter + retry_files = list(retry_dir.glob("*.envelope")) + assert len(retry_files) == 0 diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index fd16affca..3b3036259 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -43,6 +43,7 @@ add_executable(sentry_test_unit test_path.c test_process.c test_ratelimiter.c + test_retry.c test_ringbuffer.c test_sampling.c test_scope.c diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c new file mode 100644 index 000000000..74d5a9481 --- /dev/null +++ b/tests/unit/test_retry.c @@ -0,0 +1,341 @@ +#include "sentry_envelope.h" +#include "sentry_path.h" +#include "sentry_retry.h" +#include "sentry_session.h" +#include "sentry_testsupport.h" +#include "sentry_utils.h" +#include "sentry_uuid.h" + +#include +#include + +static int +count_envelope_files(const sentry_path_t *dir) +{ + int count = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(dir); + const sentry_path_t *file; + while (iter && (file = sentry__pathiter_next(iter)) != NULL) { + if (sentry__path_ends_with(file, ".envelope")) { + count++; + } + } + sentry__pathiter_free(iter); + return count; +} + +static int +find_envelope_attempt(const sentry_path_t *dir) +{ + sentry_pathiter_t *iter = sentry__path_iter_directory(dir); + const sentry_path_t *file; + while (iter && (file = sentry__pathiter_next(iter)) != NULL) { + if (!sentry__path_ends_with(file, ".envelope")) { + continue; + } + const char *name = sentry__path_filename(file); + uint64_t ts; + int attempt; + const char *uuid; + if (sentry__retry_parse_filename(name, &ts, &attempt, &uuid)) { + sentry__pathiter_free(iter); + return attempt; + } + } + sentry__pathiter_free(iter); + return -1; +} + +static void +write_retry_file(const sentry_path_t *retry_path, uint64_t timestamp, + int retry_count, const sentry_uuid_t *event_id) +{ + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry_value_t event = sentry__value_new_event_with_id(event_id); + sentry__envelope_add_event(envelope, event); + + char uuid_str[37]; + sentry_uuid_as_string(event_id, uuid_str); + char filename[80]; + snprintf(filename, sizeof(filename), "%llu-%02d-%s.envelope", + (unsigned long long)timestamp, retry_count, uuid_str); + + sentry_path_t *path = sentry__path_join_str(retry_path, filename); + (void)sentry_envelope_write_to_path(envelope, path); + sentry__path_free(path); + sentry_envelope_free(envelope); +} + +static sentry_envelope_t * +make_test_envelope(sentry_uuid_t *event_id) +{ + *event_id = sentry_uuid_new_v4(); + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry_value_t event = sentry__value_new_event_with_id(event_id); + sentry__envelope_add_event(envelope, event); + return envelope; +} + +SENTRY_TEST(retry_throttle) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-throttle"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); + TEST_ASSERT(!!retry); + + sentry_uuid_t ids[4]; + for (int i = 0; i < 4; i++) { + sentry_envelope_t *envelope = make_test_envelope(&ids[i]); + sentry__retry_write_envelope(retry, envelope); + sentry_envelope_free(envelope); + } + + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); + + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 4); + + for (size_t i = 0; i < count; i++) { + sentry__retry_handle_result(retry, paths[i], 200); + } + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); +} + +SENTRY_TEST(retry_result) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-result"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 2); + TEST_ASSERT(!!retry); + + sentry_uuid_t event_id; + sentry_envelope_t *envelope = make_test_envelope(&event_id); + + // 1. Write envelope (simulates network error → save for retry) + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); + + // 2. Success (200) → removes from retry dir + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], 200); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + // 3. Write again + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + + // 4. Rate limited (429) → removes + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], 429); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + // 5. Write again, then discard (0) → removes + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], 0); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + // 6. Network error twice → bumps count + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); + + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], -1); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); + + // 7. Network error again → exceeds max_retries=2, removed + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], -1); + sentry__retry_free_paths(paths, count); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + sentry_envelope_free(envelope); + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); +} + +SENTRY_TEST(retry_session) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_release(options, "test@1.0.0"); + sentry_init(options); + + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-session"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 2); + TEST_ASSERT(!!retry); + + sentry_session_t *session = sentry__session_new(); + TEST_ASSERT(!!session); + sentry_envelope_t *envelope = sentry__envelope_new(); + TEST_ASSERT(!!envelope); + sentry__envelope_add_session(envelope, session); + + // Session-only envelopes have no event_id → should not be written + sentry__retry_write_envelope(retry, envelope); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + + sentry_envelope_free(envelope); + sentry__session_free(session); + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); + sentry_close(); +} + +SENTRY_TEST(retry_cache) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache/retry"); + sentry_path_t *cache_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache/cache"); + sentry__path_remove_all(retry_path); + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(retry_path); + sentry__path_create_dir_all(cache_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, cache_path, 5); + TEST_ASSERT(!!retry); + + // Create a retry file at the max retry count (4, with max_retries=5) + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(retry_path, sentry__monotonic_time(), 4, &event_id); + + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); + + // Network error on a file at count=4 with max_retries=5 → moves to cache + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + sentry__retry_handle_result(retry, paths[0], -1); + sentry__retry_free_paths(paths, count); + + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_remove_all(cache_path); + sentry__path_free(retry_path); + sentry__path_free(cache_path); +} + +SENTRY_TEST(retry_backoff) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-backoff"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); + TEST_ASSERT(!!retry); + + uint64_t now = sentry__monotonic_time(); + uint64_t base = SENTRY_RETRY_BACKOFF_BASE_MS; + + // retry 0 with old timestamp: eligible (base backoff expired) + sentry_uuid_t id1 = sentry_uuid_new_v4(); + write_retry_file(retry_path, now - base, 0, &id1); + + // retry 1 with recent timestamp: not yet eligible (needs 2*base) + sentry_uuid_t id2 = sentry_uuid_new_v4(); + write_retry_file(retry_path, now, 1, &id2); + + // retry 1 with old timestamp: eligible (2*base backoff expired) + sentry_uuid_t id3 = sentry_uuid_new_v4(); + write_retry_file(retry_path, now - 2 * base, 1, &id3); + + // retry 2 with old-ish timestamp: needs 4*base but only 2*base old + sentry_uuid_t id4 = sentry_uuid_new_v4(); + write_retry_file(retry_path, now - 2 * base, 2, &id4); + + // Startup scan (no backoff check): all 4 files returned + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 4); + sentry__retry_free_paths(paths, count); + + // With backoff check: only eligible ones (id1 and id3) + paths = sentry__retry_scan(retry, false, &count); + TEST_CHECK_INT_EQUAL(count, 2); + sentry__retry_free_paths(paths, count); + + // Verify backoff_ms calculation + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(0), base); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(1), base * 2); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(2), base * 4); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(3), base * 8); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(4), base * 8); + + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); +} + +SENTRY_TEST(retry_no_duplicate_rescan) +{ + sentry_path_t *retry_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + + sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 3); + TEST_ASSERT(!!retry); + + sentry_uuid_t event_id; + sentry_envelope_t *envelope = make_test_envelope(&event_id); + sentry__retry_write_envelope(retry, envelope); + + // First scan returns the file + size_t count = 0; + sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 1); + + // Handle as success → removes from retry dir + sentry__retry_handle_result(retry, paths[0], 200); + sentry__retry_free_paths(paths, count); + + // Second scan returns nothing + paths = sentry__retry_scan(retry, true, &count); + TEST_CHECK_INT_EQUAL(count, 0); + sentry__retry_free_paths(paths, count); + + TEST_CHECK(!sentry__retry_has_files(retry)); + + sentry_envelope_free(envelope); + sentry__retry_free(retry); + sentry__path_remove_all(retry_path); + sentry__path_free(retry_path); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 9c5abab0f..0b8ceefc8 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -191,6 +191,12 @@ XX(read_envelope_from_file) XX(read_write_envelope_to_file_null) XX(read_write_envelope_to_invalid_path) XX(recursive_paths) +XX(retry_backoff) +XX(retry_cache) +XX(retry_no_duplicate_rescan) +XX(retry_result) +XX(retry_session) +XX(retry_throttle) XX(ringbuffer_append) XX(ringbuffer_append_invalid_decref_value) XX(ringbuffer_append_null_decref_value) From e230f8bc55f950f59581ee422052fd6054f21a74 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:51:01 +0100 Subject: [PATCH 003/121] fix(retry): use wall clock time instead of monotonic time Monotonic time is process-relative and doesn't work across restarts. Retry envelope timestamps need to persist across sessions, so use time() (seconds since epoch) for file timestamps, startup_time, and backoff comparison. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 9 +++++---- src/transports/sentry_http_transport.c | 4 ++-- tests/unit/test_retry.c | 21 +++++++++++++-------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index fbb8dba89..5b5307a86 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -5,6 +5,7 @@ #include #include +#include struct sentry_retry_s { sentry_path_t *retry_dir; @@ -97,7 +98,7 @@ sentry__retry_write_envelope( return; } - uint64_t now = sentry__monotonic_time(); + uint64_t now = (uint64_t)time(NULL); char uuid_str[37]; sentry_uuid_as_string(&event_id, uuid_str); @@ -130,7 +131,7 @@ sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) } size_t path_count = 0; - uint64_t now = startup ? 0 : sentry__monotonic_time(); + uint64_t now = startup ? 0 : (uint64_t)time(NULL); const sentry_path_t *p; while ((p = sentry__pathiter_next(piter)) != NULL) { @@ -145,7 +146,7 @@ sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) if (retry->startup_time > 0 && ts >= retry->startup_time) { continue; } - } else if ((now - ts) < sentry__retry_backoff_ms(count)) { + } else if ((now - ts) < sentry__retry_backoff_ms(count) / 1000) { continue; } if (path_count == path_cap) { @@ -211,7 +212,7 @@ sentry__retry_handle_result( sentry__path_remove(path); } } else { - uint64_t now = sentry__monotonic_time(); + uint64_t now = (uint64_t)time(NULL); char new_filename[128]; snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", (unsigned long long)now, count + 1, uuid_start); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 3d5cee33c..28d5cc4ca 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -16,6 +16,7 @@ #include #include +#include #define ENVELOPE_MIME "application/x-sentry-envelope" #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -338,8 +339,7 @@ http_transport_start(const sentry_options_t *options, void *transport_state) sentry__path_free(retry_dir); } if (state->retry) { - sentry__retry_set_startup_time( - state->retry, sentry__monotonic_time()); + sentry__retry_set_startup_time(state->retry, (uint64_t)time(NULL)); sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 74d5a9481..dd2718901 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -230,7 +230,7 @@ SENTRY_TEST(retry_cache) // Create a retry file at the max retry count (4, with max_retries=5) sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry_path, sentry__monotonic_time(), 4, &event_id); + write_retry_file(retry_path, (uint64_t)time(NULL), 4, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); @@ -262,8 +262,8 @@ SENTRY_TEST(retry_backoff) sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); TEST_ASSERT(!!retry); - uint64_t now = sentry__monotonic_time(); - uint64_t base = SENTRY_RETRY_BACKOFF_BASE_MS; + uint64_t now = (uint64_t)time(NULL); + uint64_t base = SENTRY_RETRY_BACKOFF_BASE_MS / 1000; // retry 0 with old timestamp: eligible (base backoff expired) sentry_uuid_t id1 = sentry_uuid_new_v4(); @@ -293,11 +293,16 @@ SENTRY_TEST(retry_backoff) sentry__retry_free_paths(paths, count); // Verify backoff_ms calculation - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(0), base); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(1), base * 2); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(2), base * 4); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(3), base * 8); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff_ms(4), base * 8); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(0), SENTRY_RETRY_BACKOFF_BASE_MS); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(1), SENTRY_RETRY_BACKOFF_BASE_MS * 2); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(2), SENTRY_RETRY_BACKOFF_BASE_MS * 4); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(3), SENTRY_RETRY_BACKOFF_BASE_MS * 8); + TEST_CHECK_UINT64_EQUAL( + sentry__retry_backoff_ms(4), SENTRY_RETRY_BACKOFF_BASE_MS * 8); sentry__retry_free(retry); sentry__path_remove_all(retry_path); From 3810f2b925e439f4d5523ed0f52f988e05311537 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 12:54:09 +0100 Subject: [PATCH 004/121] ref(retry): define backoff base in seconds Rename SENTRY_RETRY_BACKOFF_BASE_MS to SENTRY_RETRY_BACKOFF_BASE_S and sentry__retry_backoff_ms to sentry__retry_backoff, since file timestamps are now in seconds. The bgworker delay sites multiply by 1000 to convert to the milliseconds it expects. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 6 +++--- src/sentry_retry.h | 4 ++-- src/transports/sentry_http_transport.c | 4 ++-- tests/unit/test_retry.c | 17 ++++++----------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 5b5307a86..0e4058a22 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -75,10 +75,10 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, } uint64_t -sentry__retry_backoff_ms(int count) +sentry__retry_backoff(int count) { int shift = count < 3 ? count : 3; - return (uint64_t)SENTRY_RETRY_BACKOFF_BASE_MS << shift; + return (uint64_t)SENTRY_RETRY_BACKOFF_BASE_S << shift; } static int @@ -146,7 +146,7 @@ sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) if (retry->startup_time > 0 && ts >= retry->startup_time) { continue; } - } else if ((now - ts) < sentry__retry_backoff_ms(count) / 1000) { + } else if ((now - ts) < sentry__retry_backoff(count)) { continue; } if (path_count == path_cap) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 07a11d93a..4aa360d0c 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -4,7 +4,7 @@ #include "sentry_boot.h" #include "sentry_path.h" -#define SENTRY_RETRY_BACKOFF_BASE_MS 900000 +#define SENTRY_RETRY_BACKOFF_BASE_S 900 #define SENTRY_RETRY_STARTUP_DELAY_MS 100 typedef struct sentry_retry_s sentry_retry_t; @@ -28,7 +28,7 @@ void sentry__retry_handle_result( bool sentry__retry_has_files(const sentry_retry_t *retry); -uint64_t sentry__retry_backoff_ms(int count); +uint64_t sentry__retry_backoff(int count); bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 28d5cc4ca..3f41f2984 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -257,7 +257,7 @@ retry_process_task(void *_startup, void *_state) if (sentry__retry_has_files(state->retry)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_MS); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); } } @@ -293,7 +293,7 @@ http_send_task(void *_envelope, void *_state) if (status_code < 0 && state->retry) { sentry__retry_write_envelope(state->retry, envelope); sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_MS); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); } } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index dd2718901..7ec7c4046 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -263,7 +263,7 @@ SENTRY_TEST(retry_backoff) TEST_ASSERT(!!retry); uint64_t now = (uint64_t)time(NULL); - uint64_t base = SENTRY_RETRY_BACKOFF_BASE_MS / 1000; + uint64_t base = SENTRY_RETRY_BACKOFF_BASE_S; // retry 0 with old timestamp: eligible (base backoff expired) sentry_uuid_t id1 = sentry_uuid_new_v4(); @@ -293,16 +293,11 @@ SENTRY_TEST(retry_backoff) sentry__retry_free_paths(paths, count); // Verify backoff_ms calculation - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(0), SENTRY_RETRY_BACKOFF_BASE_MS); - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(1), SENTRY_RETRY_BACKOFF_BASE_MS * 2); - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(2), SENTRY_RETRY_BACKOFF_BASE_MS * 4); - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(3), SENTRY_RETRY_BACKOFF_BASE_MS * 8); - TEST_CHECK_UINT64_EQUAL( - sentry__retry_backoff_ms(4), SENTRY_RETRY_BACKOFF_BASE_MS * 8); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(1), base * 2); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(2), base * 4); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(3), base * 8); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 8); sentry__retry_free(retry); sentry__path_remove_all(retry_path); From a910f324dcd4bff080e02f210105418edfd401c9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:03:50 +0100 Subject: [PATCH 005/121] ref(retry): replace scan+free_paths with foreach callback API Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 27 +++---- src/sentry_retry.h | 5 +- src/transports/sentry_http_transport.c | 54 ++++++------- tests/unit/test_retry.c | 101 +++++++++++++------------ 4 files changed, 93 insertions(+), 94 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 0e4058a22..88bb9714e 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -113,21 +113,20 @@ sentry__retry_write_envelope( } } -sentry_path_t ** -sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) +void +sentry__retry_foreach(sentry_retry_t *retry, bool startup, + bool (*callback)(const sentry_path_t *path, void *data), void *data) { - *count_out = 0; - sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); if (!piter) { - return NULL; + return; } size_t path_cap = 16; sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); if (!paths) { sentry__pathiter_free(piter); - return NULL; + return; } size_t path_count = 0; @@ -168,17 +167,13 @@ sentry__retry_scan(sentry_retry_t *retry, bool startup, size_t *count_out) qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); } - *count_out = path_count; - return paths; -} - -void -sentry__retry_free_paths(sentry_path_t **paths, size_t count) -{ - if (!paths) { - return; + for (size_t i = 0; i < path_count; i++) { + if (!callback(paths[i], data)) { + break; + } } - for (size_t i = 0; i < count; i++) { + + for (size_t i = 0; i < path_count; i++) { sentry__path_free(paths[i]); } sentry_free(paths); diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 4aa360d0c..15adaf8b3 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -19,9 +19,8 @@ void sentry__retry_write_envelope( void sentry__retry_set_startup_time( sentry_retry_t *retry, uint64_t startup_time); -sentry_path_t **sentry__retry_scan( - sentry_retry_t *retry, bool startup, size_t *count_out); -void sentry__retry_free_paths(sentry_path_t **paths, size_t count); +void sentry__retry_foreach(sentry_retry_t *retry, bool startup, + bool (*callback)(const sentry_path_t *path, void *data), void *data); void sentry__retry_handle_result( sentry_retry_t *retry, const sentry_path_t *path, int status_code); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 3f41f2984..c01c25744 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -189,8 +189,6 @@ sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) sentry_free(req); } -static void retry_process_task(void *_check_backoff, void *_state); - static int http_send_request( http_transport_state_t *state, sentry_prepared_http_request_t *req) @@ -219,41 +217,43 @@ http_send_request( return resp.status_code; } -static void -retry_process_task(void *_startup, void *_state) +static bool +retry_send_cb(const sentry_path_t *path, void *_state) { - int startup = (int)(intptr_t)_startup; http_transport_state_t *state = _state; - if (!state->retry) { - return; + sentry_envelope_t *envelope = sentry__envelope_from_path(path); + if (!envelope) { + sentry__path_remove(path); + return true; } - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(state->retry, startup, &count); + sentry_prepared_http_request_t *req = sentry__prepare_http_request( + envelope, state->dsn, state->ratelimiter, state->user_agent); + int status_code; + if (!req) { + status_code = 0; + } else { + status_code = http_send_request(state, req); + sentry__prepared_http_request_free(req); + } + sentry_envelope_free(envelope); - for (size_t i = 0; i < count; i++) { - sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); - if (!envelope) { - sentry__path_remove(paths[i]); - continue; - } + sentry__retry_handle_result(state->retry, path, status_code); + return true; +} - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - envelope, state->dsn, state->ratelimiter, state->user_agent); - int status_code; - if (!req) { - status_code = 0; - } else { - status_code = http_send_request(state, req); - sentry__prepared_http_request_free(req); - } - sentry_envelope_free(envelope); +static void +retry_process_task(void *_startup, void *_state) +{ + int startup = (int)(intptr_t)_startup; + http_transport_state_t *state = _state; - sentry__retry_handle_result(state->retry, paths[i], status_code); + if (!state->retry) { + return; } - sentry__retry_free_paths(paths, count); + sentry__retry_foreach(state->retry, startup, retry_send_cb, state); if (sentry__retry_has_files(state->retry)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 7ec7c4046..5e88d9731 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -76,6 +76,29 @@ make_test_envelope(sentry_uuid_t *event_id) return envelope; } +typedef struct { + sentry_retry_t *retry; + int status_code; + size_t count; +} retry_test_ctx_t; + +static bool +handle_result_cb(const sentry_path_t *path, void *_ctx) +{ + retry_test_ctx_t *ctx = _ctx; + ctx->count++; + sentry__retry_handle_result(ctx->retry, path, ctx->status_code); + return true; +} + +static bool +count_cb(const sentry_path_t *path, void *_count) +{ + (void)path; + (*(size_t *)_count)++; + return true; +} + SENTRY_TEST(retry_throttle) { sentry_path_t *retry_path @@ -95,14 +118,9 @@ SENTRY_TEST(retry_throttle) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 4); - - for (size_t i = 0; i < count; i++) { - sentry__retry_handle_result(retry, paths[i], 200); - } - sentry__retry_free_paths(paths, count); + retry_test_ctx_t ctx = { retry, 200, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 4); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); sentry__retry_free(retry); @@ -129,11 +147,9 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); // 2. Success (200) → removes from retry dir - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], 200); - sentry__retry_free_paths(paths, count); + retry_test_ctx_t ctx = { retry, 200, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 3. Write again @@ -141,19 +157,17 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); // 4. Rate limited (429) → removes - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], 429); - sentry__retry_free_paths(paths, count); + ctx = (retry_test_ctx_t) { retry, 429, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 5. Write again, then discard (0) → removes sentry__retry_write_envelope(retry, envelope); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], 0); - sentry__retry_free_paths(paths, count); + ctx = (retry_test_ctx_t) { retry, 0, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 6. Network error twice → bumps count @@ -161,18 +175,16 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], -1); - sentry__retry_free_paths(paths, count); + ctx = (retry_test_ctx_t) { retry, -1, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); // 7. Network error again → exceeds max_retries=2, removed - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], -1); - sentry__retry_free_paths(paths, count); + ctx = (retry_test_ctx_t) { retry, -1, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); sentry_envelope_free(envelope); @@ -236,11 +248,9 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // Network error on a file at count=4 with max_retries=5 → moves to cache - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - sentry__retry_handle_result(retry, paths[0], -1); - sentry__retry_free_paths(paths, count); + retry_test_ctx_t ctx = { retry, -1, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); @@ -283,14 +293,13 @@ SENTRY_TEST(retry_backoff) // Startup scan (no backoff check): all 4 files returned size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); + sentry__retry_foreach(retry, true, count_cb, &count); TEST_CHECK_INT_EQUAL(count, 4); - sentry__retry_free_paths(paths, count); // With backoff check: only eligible ones (id1 and id3) - paths = sentry__retry_scan(retry, false, &count); + count = 0; + sentry__retry_foreach(retry, false, count_cb, &count); TEST_CHECK_INT_EQUAL(count, 2); - sentry__retry_free_paths(paths, count); // Verify backoff_ms calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); @@ -319,18 +328,14 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry__retry_write_envelope(retry, envelope); // First scan returns the file - size_t count = 0; - sentry_path_t **paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 1); - - // Handle as success → removes from retry dir - sentry__retry_handle_result(retry, paths[0], 200); - sentry__retry_free_paths(paths, count); + retry_test_ctx_t ctx = { retry, 200, 0 }; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); // Second scan returns nothing - paths = sentry__retry_scan(retry, true, &count); - TEST_CHECK_INT_EQUAL(count, 0); - sentry__retry_free_paths(paths, count); + ctx.count = 0; + sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 0); TEST_CHECK(!sentry__retry_has_files(retry)); From 8b8921bc2d7b866fd827bb535f91415e6a36bc3f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:14:51 +0100 Subject: [PATCH 006/121] feat(transport): set 15s request timeout for curl and winhttp Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_curl.c | 1 + src/transports/sentry_http_transport_winhttp.c | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index b0c967f5c..417c24cfe 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -189,6 +189,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index 0997d3562..e3f003a18 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -134,6 +134,8 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 1; } + WinHttpSetTimeouts(client->session, 15000, 15000, 15000, 15000); + return 0; } From 46d2a9e25988e02b2f887a60365ab85b3c4c2e88 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:21:26 +0100 Subject: [PATCH 007/121] fix(retry): avoid duplicate delayed retry tasks on startup Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 30 ++++---------------------- src/sentry_retry.h | 4 +--- src/transports/sentry_http_transport.c | 4 +--- tests/unit/test_retry.c | 2 -- 4 files changed, 6 insertions(+), 34 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 88bb9714e..86a77175e 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -113,20 +113,20 @@ sentry__retry_write_envelope( } } -void +size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, bool (*callback)(const sentry_path_t *path, void *data), void *data) { sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); if (!piter) { - return; + return 0; } size_t path_cap = 16; sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); if (!paths) { sentry__pathiter_free(piter); - return; + return 0; } size_t path_count = 0; @@ -177,6 +177,7 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, sentry__path_free(paths[i]); } sentry_free(paths); + return path_count; } void @@ -234,26 +235,3 @@ sentry__retry_handle_result( sentry__path_remove(path); } } - -bool -sentry__retry_has_files(const sentry_retry_t *retry) -{ - sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); - if (!piter) { - return false; - } - - const sentry_path_t *p; - while ((p = sentry__pathiter_next(piter)) != NULL) { - const char *fname = sentry__path_filename(p); - uint64_t ts; - int count; - const char *uuid_start; - if (sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { - sentry__pathiter_free(piter); - return true; - } - } - sentry__pathiter_free(piter); - return false; -} diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 15adaf8b3..5ee2c6dc7 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -19,14 +19,12 @@ void sentry__retry_write_envelope( void sentry__retry_set_startup_time( sentry_retry_t *retry, uint64_t startup_time); -void sentry__retry_foreach(sentry_retry_t *retry, bool startup, +size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, bool (*callback)(const sentry_path_t *path, void *data), void *data); void sentry__retry_handle_result( sentry_retry_t *retry, const sentry_path_t *path, int status_code); -bool sentry__retry_has_files(const sentry_retry_t *retry); - uint64_t sentry__retry_backoff(int count); bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index c01c25744..d94f68349 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -253,9 +253,7 @@ retry_process_task(void *_startup, void *_state) return; } - sentry__retry_foreach(state->retry, startup, retry_send_cb, state); - - if (sentry__retry_has_files(state->retry)) { + if (sentry__retry_foreach(state->retry, startup, retry_send_cb, state)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 5e88d9731..922dae1c1 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -337,8 +337,6 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry__retry_foreach(retry, true, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 0); - TEST_CHECK(!sentry__retry_has_files(retry)); - sentry_envelope_free(envelope); sentry__retry_free(retry); sentry__path_remove_all(retry_path); From 4f3d62533207741188bc28da59573934c6e2c1cb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 13:38:57 +0100 Subject: [PATCH 008/121] ref(retry): take options in sentry__retry_new, own path construction Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 29 +++++- src/sentry_retry.h | 3 +- src/transports/sentry_http_transport.c | 28 +----- tests/unit/test_retry.c | 122 ++++++++++++++++--------- 4 files changed, 109 insertions(+), 73 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 86a77175e..87f2d2c46 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -1,6 +1,7 @@ #include "sentry_retry.h" #include "sentry_alloc.h" #include "sentry_envelope.h" +#include "sentry_options.h" #include "sentry_utils.h" #include @@ -15,16 +16,34 @@ struct sentry_retry_s { }; sentry_retry_t * -sentry__retry_new( - sentry_path_t *retry_dir, sentry_path_t *cache_dir, int max_retries) +sentry__retry_new(const sentry_options_t *options) { + if (options->http_retries <= 0 || !options->database_path) { + return NULL; + } + sentry_path_t *retry_dir + = sentry__path_join_str(options->database_path, "retry"); + if (!retry_dir) { + return NULL; + } + sentry_path_t *cache_dir = NULL; + if (options->cache_keep) { + cache_dir = sentry__path_join_str(options->database_path, "cache"); + } + sentry_retry_t *retry = SENTRY_MAKE(sentry_retry_t); if (!retry) { + sentry__path_free(cache_dir); + sentry__path_free(retry_dir); return NULL; } - retry->retry_dir = sentry__path_clone(retry_dir); - retry->cache_dir = cache_dir ? sentry__path_clone(cache_dir) : NULL; - retry->max_retries = max_retries; + retry->retry_dir = retry_dir; + retry->cache_dir = cache_dir; + retry->max_retries = options->http_retries; + sentry__path_create_dir_all(retry->retry_dir); + if (retry->cache_dir) { + sentry__path_create_dir_all(retry->cache_dir); + } return retry; } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 5ee2c6dc7..10bf7e1c4 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -9,8 +9,7 @@ typedef struct sentry_retry_s sentry_retry_t; -sentry_retry_t *sentry__retry_new( - sentry_path_t *retry_dir, sentry_path_t *cache_dir, int max_retries); +sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); void sentry__retry_write_envelope( diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index d94f68349..6848dcb7e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -318,29 +318,11 @@ http_transport_start(const sentry_options_t *options, void *transport_state) return rv; } - if (options->http_retries > 0) { - sentry_path_t *retry_dir - = sentry__path_join_str(options->database_path, "retry"); - if (retry_dir) { - sentry__path_create_dir_all(retry_dir); - sentry_path_t *cache_dir = NULL; - if (options->cache_keep) { - cache_dir - = sentry__path_join_str(options->database_path, "cache"); - if (cache_dir) { - sentry__path_create_dir_all(cache_dir); - } - } - state->retry = sentry__retry_new( - retry_dir, cache_dir, options->http_retries); - sentry__path_free(cache_dir); - sentry__path_free(retry_dir); - } - if (state->retry) { - sentry__retry_set_startup_time(state->retry, (uint64_t)time(NULL)); - sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, - (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); - } + state->retry = sentry__retry_new(options); + if (state->retry) { + sentry__retry_set_startup_time(state->retry, (uint64_t)time(NULL)); + sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, + (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); } return 0; diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 922dae1c1..101c1d9e2 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -101,14 +101,20 @@ count_cb(const sentry_path_t *path, void *_count) SENTRY_TEST(retry_throttle) { - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-throttle"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-throttle"); + sentry_options_set_http_retries(options, 5); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { sentry_envelope_t *envelope = make_test_envelope(&ids[i]); @@ -124,20 +130,27 @@ SENTRY_TEST(retry_throttle) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } SENTRY_TEST(retry_result) { - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-result"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 2); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-result"); + sentry_options_set_http_retries(options, 2); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry_uuid_t event_id; sentry_envelope_t *envelope = make_test_envelope(&event_id); @@ -189,25 +202,32 @@ SENTRY_TEST(retry_result) sentry_envelope_free(envelope); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } SENTRY_TEST(retry_session) { - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_release(options, "test@1.0.0"); - sentry_init(options); + SENTRY_TEST_OPTIONS_NEW(init_options); + sentry_options_set_dsn(init_options, "https://foo@sentry.invalid/42"); + sentry_options_set_release(init_options, "test@1.0.0"); + sentry_init(init_options); - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-session"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 2); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-session"); + sentry_options_set_http_retries(options, 2); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry_session_t *session = sentry__session_new(); TEST_ASSERT(!!session); sentry_envelope_t *envelope = sentry__envelope_new(); @@ -221,25 +241,30 @@ SENTRY_TEST(retry_session) sentry_envelope_free(envelope); sentry__session_free(session); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); sentry_close(); } SENTRY_TEST(retry_cache) { - sentry_path_t *retry_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache/retry"); - sentry_path_t *cache_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache/cache"); - sentry__path_remove_all(retry_path); - sentry__path_remove_all(cache_path); - sentry__path_create_dir_all(retry_path); - sentry__path_create_dir_all(cache_path); - - sentry_retry_t *retry = sentry__retry_new(retry_path, cache_path, 5); + sentry_path_t *db_path + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache"); + sentry__path_remove_all(db_path); + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-cache"); + sentry_options_set_http_retries(options, 5); + sentry_options_set_cache_keep(options, 1); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); + // Create a retry file at the max retry count (4, with max_retries=5) sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, (uint64_t)time(NULL), 4, &event_id); @@ -256,22 +281,28 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); - sentry__path_remove_all(cache_path); sentry__path_free(retry_path); sentry__path_free(cache_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } SENTRY_TEST(retry_backoff) { - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-backoff"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 5); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-backoff"); + sentry_options_set_http_retries(options, 5); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + uint64_t now = (uint64_t)time(NULL); uint64_t base = SENTRY_RETRY_BACKOFF_BASE_S; @@ -301,7 +332,7 @@ SENTRY_TEST(retry_backoff) sentry__retry_foreach(retry, false, count_cb, &count); TEST_CHECK_INT_EQUAL(count, 2); - // Verify backoff_ms calculation + // Verify backoff calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(1), base * 2); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(2), base * 4); @@ -309,18 +340,23 @@ SENTRY_TEST(retry_backoff) TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 8); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } SENTRY_TEST(retry_no_duplicate_rescan) { - sentry_path_t *retry_path + sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(db_path); - sentry_retry_t *retry = sentry__retry_new(retry_path, NULL, 3); + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_database_path( + options, SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); + sentry_options_set_http_retries(options, 3); + sentry_retry_t *retry = sentry__retry_new(options); + sentry_options_free(options); TEST_ASSERT(!!retry); sentry_uuid_t event_id; @@ -339,6 +375,6 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry_envelope_free(envelope); sentry__retry_free(retry); - sentry__path_remove_all(retry_path); - sentry__path_free(retry_path); + sentry__path_remove_all(db_path); + sentry__path_free(db_path); } From 199bd3047fb40b164578af93dc21de99b716b2f5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 14:19:41 +0100 Subject: [PATCH 009/121] ref(retry): set startup_time at creation, remove setter Move startup_time initialization into sentry__retry_new and remove the unnecessary sentry__retry_set_startup_time indirection. Tests now use write_retry_file with timestamps well in the past to match production behavior where retry files are from previous sessions. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 7 +- src/sentry_retry.h | 3 - src/transports/sentry_http_transport.c | 1 - tests/unit/test_retry.c | 98 +++++++++++--------------- 4 files changed, 44 insertions(+), 65 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 87f2d2c46..d7280ac7c 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -40,6 +40,7 @@ sentry__retry_new(const sentry_options_t *options) retry->retry_dir = retry_dir; retry->cache_dir = cache_dir; retry->max_retries = options->http_retries; + retry->startup_time = (uint64_t)time(NULL); sentry__path_create_dir_all(retry->retry_dir); if (retry->cache_dir) { sentry__path_create_dir_all(retry->cache_dir); @@ -58,12 +59,6 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } -void -sentry__retry_set_startup_time(sentry_retry_t *retry, uint64_t startup_time) -{ - retry->startup_time = startup_time; -} - bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out) diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 10bf7e1c4..56bf99966 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -15,9 +15,6 @@ void sentry__retry_free(sentry_retry_t *retry); void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); -void sentry__retry_set_startup_time( - sentry_retry_t *retry, uint64_t startup_time); - size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, bool (*callback)(const sentry_path_t *path, void *data), void *data); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 6848dcb7e..caeff28aa 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -320,7 +320,6 @@ http_transport_start(const sentry_options_t *options, void *transport_state) state->retry = sentry__retry_new(options); if (state->retry) { - sentry__retry_set_startup_time(state->retry, (uint64_t)time(NULL)); sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 101c1d9e2..9f829daa0 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -66,16 +66,6 @@ write_retry_file(const sentry_path_t *retry_path, uint64_t timestamp, sentry_envelope_free(envelope); } -static sentry_envelope_t * -make_test_envelope(sentry_uuid_t *event_id) -{ - *event_id = sentry_uuid_new_v4(); - sentry_envelope_t *envelope = sentry__envelope_new(); - sentry_value_t event = sentry__value_new_event_with_id(event_id); - sentry__envelope_add_event(envelope, event); - return envelope; -} - typedef struct { sentry_retry_t *retry; int status_code; @@ -115,17 +105,17 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { - sentry_envelope_t *envelope = make_test_envelope(&ids[i]); - sentry__retry_write_envelope(retry, envelope); - sentry_envelope_free(envelope); + ids[i] = sentry_uuid_new_v4(); + write_retry_file(retry_path, old_ts, 0, &ids[i]); } TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 4); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -151,56 +141,52 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - sentry_uuid_t event_id; - sentry_envelope_t *envelope = make_test_envelope(&event_id); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + sentry_uuid_t event_id = sentry_uuid_new_v4(); - // 1. Write envelope (simulates network error → save for retry) - sentry__retry_write_envelope(retry, envelope); + // 1. Success (200) → removes from retry dir + write_retry_file(retry_path, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); - // 2. Success (200) → removes from retry dir retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); - // 3. Write again - sentry__retry_write_envelope(retry, envelope); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - - // 4. Rate limited (429) → removes + // 2. Rate limited (429) → removes + write_retry_file(retry_path, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 429, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); - // 5. Write again, then discard (0) → removes - sentry__retry_write_envelope(retry, envelope); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + // 3. Discard (0) → removes + write_retry_file(retry_path, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 0, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); - // 6. Network error twice → bumps count - sentry__retry_write_envelope(retry, envelope); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + // 4. Network error → bumps count + write_retry_file(retry_path, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); - // 7. Network error again → exceeds max_retries=2, removed + // 5. Network error at max count → exceeds max_retries=2, removed + sentry__path_remove_all(retry_path); + sentry__path_create_dir_all(retry_path); + write_retry_file(retry_path, old_ts, 1, &event_id); ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); - sentry_envelope_free(envelope); sentry__retry_free(retry); sentry__path_free(retry_path); sentry__path_remove_all(db_path); @@ -265,16 +251,16 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - // Create a retry file at the max retry count (4, with max_retries=5) + uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry_path, (uint64_t)time(NULL), 4, &event_id); + write_retry_file(retry_path, old_ts, 4, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // Network error on a file at count=4 with max_retries=5 → moves to cache retry_test_ctx_t ctx = { retry, -1, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -303,24 +289,24 @@ SENTRY_TEST(retry_backoff) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t now = (uint64_t)time(NULL); uint64_t base = SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t ref = (uint64_t)time(NULL) - 10 * base; - // retry 0 with old timestamp: eligible (base backoff expired) + // retry 0: 10*base old, eligible (backoff=base) sentry_uuid_t id1 = sentry_uuid_new_v4(); - write_retry_file(retry_path, now - base, 0, &id1); + write_retry_file(retry_path, ref, 0, &id1); - // retry 1 with recent timestamp: not yet eligible (needs 2*base) + // retry 1: 1*base old, not yet eligible (backoff=2*base) sentry_uuid_t id2 = sentry_uuid_new_v4(); - write_retry_file(retry_path, now, 1, &id2); + write_retry_file(retry_path, ref + 9 * base, 1, &id2); - // retry 1 with old timestamp: eligible (2*base backoff expired) + // retry 1: 10*base old, eligible (backoff=2*base) sentry_uuid_t id3 = sentry_uuid_new_v4(); - write_retry_file(retry_path, now - 2 * base, 1, &id3); + write_retry_file(retry_path, ref, 1, &id3); - // retry 2 with old-ish timestamp: needs 4*base but only 2*base old + // retry 2: 2*base old, not eligible (backoff=4*base) sentry_uuid_t id4 = sentry_uuid_new_v4(); - write_retry_file(retry_path, now - 2 * base, 2, &id4); + write_retry_file(retry_path, ref + 8 * base, 2, &id4); // Startup scan (no backoff check): all 4 files returned size_t count = 0; @@ -359,21 +345,23 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_uuid_t event_id; - sentry_envelope_t *envelope = make_test_envelope(&event_id); - sentry__retry_write_envelope(retry, envelope); + sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + + uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(retry_path, old_ts, 0, &event_id); // First scan returns the file retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); // Second scan returns nothing ctx.count = 0; - sentry__retry_foreach(retry, true, handle_result_cb, &ctx); + sentry__retry_foreach(retry, false, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 0); - sentry_envelope_free(envelope); + sentry__path_free(retry_path); sentry__retry_free(retry); sentry__path_remove_all(db_path); sentry__path_free(db_path); From 94a7ca7f8c7e09d1c5de92a568fe885ae50f2a4d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 14:49:31 +0100 Subject: [PATCH 010/121] fix(retry): return total file count so polling continues during backoff When files exist but aren't eligible yet (backoff not elapsed), foreach was returning 0 causing the retry polling task to stop. Return total valid retry files found instead of just the eligible count so the caller keeps rescheduling. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d7280ac7c..d3400fc2d 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -143,7 +143,8 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, return 0; } - size_t path_count = 0; + size_t total = 0; + size_t eligible = 0; uint64_t now = startup ? 0 : (uint64_t)time(NULL); const sentry_path_t *p; @@ -159,39 +160,41 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, if (retry->startup_time > 0 && ts >= retry->startup_time) { continue; } - } else if ((now - ts) < sentry__retry_backoff(count)) { + } + total++; + if (!startup && (now - ts) < sentry__retry_backoff(count)) { continue; } - if (path_count == path_cap) { + if (eligible == path_cap) { path_cap *= 2; sentry_path_t **tmp = sentry_malloc(path_cap * sizeof(sentry_path_t *)); if (!tmp) { break; } - memcpy(tmp, paths, path_count * sizeof(sentry_path_t *)); + memcpy(tmp, paths, eligible * sizeof(sentry_path_t *)); sentry_free(paths); paths = tmp; } - paths[path_count++] = sentry__path_clone(p); + paths[eligible++] = sentry__path_clone(p); } sentry__pathiter_free(piter); - if (path_count > 1) { - qsort(paths, path_count, sizeof(sentry_path_t *), compare_retry_paths); + if (eligible > 1) { + qsort(paths, eligible, sizeof(sentry_path_t *), compare_retry_paths); } - for (size_t i = 0; i < path_count; i++) { + for (size_t i = 0; i < eligible; i++) { if (!callback(paths[i], data)) { break; } } - for (size_t i = 0; i < path_count; i++) { + for (size_t i = 0; i < eligible; i++) { sentry__path_free(paths[i]); } sentry_free(paths); - return path_count; + return total; } void From 011fbb5adfc853285cd9d480d1aace2d333ebf73 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 15:08:40 +0100 Subject: [PATCH 011/121] fix(retry): use callback return value to track remaining retry files Make handle_result return bool (true = file rescheduled for retry, false = file consumed) and use it in foreach to decrement the total count. This avoids one extra no-op poll cycle after the last retry. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 9 ++++++--- src/sentry_retry.h | 4 ++-- src/transports/sentry_http_transport.c | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d3400fc2d..da99ffdae 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -186,7 +186,7 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, for (size_t i = 0; i < eligible; i++) { if (!callback(paths[i], data)) { - break; + total--; } } @@ -197,7 +197,7 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, return total; } -void +bool sentry__retry_handle_result( sentry_retry_t *retry, const sentry_path_t *path, int status_code) { @@ -207,7 +207,7 @@ sentry__retry_handle_result( const char *uuid_start; if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { sentry__path_remove(path); - return; + return false; } if (status_code < 0) { @@ -224,6 +224,7 @@ sentry__retry_handle_result( } else { sentry__path_remove(path); } + return false; } else { uint64_t now = (uint64_t)time(NULL); char new_filename[128]; @@ -235,6 +236,7 @@ sentry__retry_handle_result( sentry__path_rename(path, new_path); sentry__path_free(new_path); } + return true; } } else if (status_code >= 200 && status_code < 300) { if (retry->cache_dir) { @@ -251,4 +253,5 @@ sentry__retry_handle_result( } else { sentry__path_remove(path); } + return false; } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 56bf99966..0e46ff62a 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -4,7 +4,7 @@ #include "sentry_boot.h" #include "sentry_path.h" -#define SENTRY_RETRY_BACKOFF_BASE_S 900 +#define SENTRY_RETRY_BACKOFF_BASE_S 15 // 900 #define SENTRY_RETRY_STARTUP_DELAY_MS 100 typedef struct sentry_retry_s sentry_retry_t; @@ -18,7 +18,7 @@ void sentry__retry_write_envelope( size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, bool (*callback)(const sentry_path_t *path, void *data), void *data); -void sentry__retry_handle_result( +bool sentry__retry_handle_result( sentry_retry_t *retry, const sentry_path_t *path, int status_code); uint64_t sentry__retry_backoff(int count); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index caeff28aa..e41cc71fd 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -239,8 +239,7 @@ retry_send_cb(const sentry_path_t *path, void *_state) } sentry_envelope_free(envelope); - sentry__retry_handle_result(state->retry, path, status_code); - return true; + return sentry__retry_handle_result(state->retry, path, status_code); } static void From f6731932c2d682c952b3d9c604688d0eabd5de4e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 15:18:00 +0100 Subject: [PATCH 012/121] ref(retry): rename constants to SENTRY_RETRY_INTERVAL and SENTRY_RETRY_THROTTLE Replace SENTRY_RETRY_BACKOFF_BASE_S and SENTRY_RETRY_STARTUP_DELAY_MS with ms-based constants so the transport uses them directly without leaking unit conversion details. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- src/sentry_retry.h | 4 ++-- src/transports/sentry_http_transport.c | 6 +++--- tests/unit/test_retry.c | 14 +++++++++----- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index da99ffdae..96f8cf409 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -92,7 +92,7 @@ uint64_t sentry__retry_backoff(int count) { int shift = count < 3 ? count : 3; - return (uint64_t)SENTRY_RETRY_BACKOFF_BASE_S << shift; + return (uint64_t)(SENTRY_RETRY_INTERVAL / 1000) << shift; } static int diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 0e46ff62a..ac84afac6 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -4,8 +4,8 @@ #include "sentry_boot.h" #include "sentry_path.h" -#define SENTRY_RETRY_BACKOFF_BASE_S 15 // 900 -#define SENTRY_RETRY_STARTUP_DELAY_MS 100 +#define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) +#define SENTRY_RETRY_THROTTLE 100 typedef struct sentry_retry_s sentry_retry_t; diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index e41cc71fd..79406591e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -254,7 +254,7 @@ retry_process_task(void *_startup, void *_state) if (sentry__retry_foreach(state->retry, startup, retry_send_cb, state)) { sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_INTERVAL); } } @@ -290,7 +290,7 @@ http_send_task(void *_envelope, void *_state) if (status_code < 0 && state->retry) { sentry__retry_write_envelope(state->retry, envelope); sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_BACKOFF_BASE_S * 1000); + NULL, (void *)(intptr_t)0, SENTRY_RETRY_INTERVAL); } } @@ -320,7 +320,7 @@ http_transport_start(const sentry_options_t *options, void *transport_state) state->retry = sentry__retry_new(options); if (state->retry) { sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, - (void *)(intptr_t)1, SENTRY_RETRY_STARTUP_DELAY_MS); + (void *)(intptr_t)1, SENTRY_RETRY_THROTTLE); } return 0; diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 9f829daa0..375a55f51 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -105,7 +105,8 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t old_ts + = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); @@ -141,7 +142,8 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t old_ts + = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir @@ -251,7 +253,8 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t old_ts + = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, old_ts, 4, &event_id); @@ -289,7 +292,7 @@ SENTRY_TEST(retry_backoff) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t base = SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t base = (SENTRY_RETRY_INTERVAL / 1000); uint64_t ref = (uint64_t)time(NULL) - 10 * base; // retry 0: 10*base old, eligible (backoff=base) @@ -347,7 +350,8 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * SENTRY_RETRY_BACKOFF_BASE_S; + uint64_t old_ts + = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, old_ts, 0, &event_id); From caef6c18892666d24ac59e77d90c4cc1d72239ec Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:38:31 +0100 Subject: [PATCH 013/121] ref(retry): encapsulate retry scheduling into the retry module Give the retry module a bgworker ref and send callback so it owns all scheduling. Transport just calls _start and _enqueue. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 55 ++++++++++++++++++++++++++ src/sentry_retry.h | 12 ++++-- src/transports/sentry_http_transport.c | 30 ++------------ tests/unit/test_retry.c | 14 +++---- 4 files changed, 72 insertions(+), 39 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 96f8cf409..20767d49a 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -8,11 +8,17 @@ #include #include +#define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) +#define SENTRY_RETRY_THROTTLE 100 + struct sentry_retry_s { sentry_path_t *retry_dir; sentry_path_t *cache_dir; int max_retries; uint64_t startup_time; + sentry_bgworker_t *bgworker; + sentry_retry_send_func_t send_cb; + void *send_data; }; sentry_retry_t * @@ -59,6 +65,55 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } +static void retry_poll_task(void *_retry, void *_state); + +static void +retry_startup_task(void *_retry, void *_state) +{ + (void)_state; + sentry_retry_t *retry = _retry; + if (sentry__retry_foreach(retry, true, retry->send_cb, retry->send_data)) { + sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, + retry, SENTRY_RETRY_INTERVAL); + } +} + +static void +retry_poll_task(void *_retry, void *_state) +{ + (void)_state; + sentry_retry_t *retry = _retry; + if (sentry__retry_foreach(retry, false, retry->send_cb, retry->send_data)) { + sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, + retry, SENTRY_RETRY_INTERVAL); + } +} + +void +sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, + sentry_retry_send_func_t send_cb, void *send_data) +{ + if (!retry) { + return; + } + retry->bgworker = bgworker; + retry->send_cb = send_cb; + retry->send_data = send_data; + sentry__bgworker_submit_delayed( + bgworker, retry_startup_task, NULL, retry, SENTRY_RETRY_THROTTLE); +} + +void +sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) +{ + if (!retry) { + return; + } + sentry__retry_write_envelope(retry, envelope); + sentry__bgworker_submit_delayed( + retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); +} + bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out) diff --git a/src/sentry_retry.h b/src/sentry_retry.h index ac84afac6..9151888b1 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -3,15 +3,21 @@ #include "sentry_boot.h" #include "sentry_path.h" - -#define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) -#define SENTRY_RETRY_THROTTLE 100 +#include "sentry_sync.h" typedef struct sentry_retry_s sentry_retry_t; +typedef bool (*sentry_retry_send_func_t)(const sentry_path_t *path, void *data); + sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); +void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, + sentry_retry_send_func_t send_cb, void *send_data); + +void sentry__retry_enqueue( + sentry_retry_t *retry, const sentry_envelope_t *envelope); + void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 79406591e..90e7ff9d5 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -34,7 +34,6 @@ typedef struct { int (*start_client)(void *, const sentry_options_t *); sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); - sentry_bgworker_t *bgworker; sentry_retry_t *retry; } http_transport_state_t; @@ -242,22 +241,6 @@ retry_send_cb(const sentry_path_t *path, void *_state) return sentry__retry_handle_result(state->retry, path, status_code); } -static void -retry_process_task(void *_startup, void *_state) -{ - int startup = (int)(intptr_t)_startup; - http_transport_state_t *state = _state; - - if (!state->retry) { - return; - } - - if (sentry__retry_foreach(state->retry, startup, retry_send_cb, state)) { - sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_INTERVAL); - } -} - static void http_transport_state_free(void *_state) { @@ -287,10 +270,8 @@ http_send_task(void *_envelope, void *_state) int status_code = http_send_request(state, req); sentry__prepared_http_request_free(req); - if (status_code < 0 && state->retry) { - sentry__retry_write_envelope(state->retry, envelope); - sentry__bgworker_submit_delayed(state->bgworker, retry_process_task, - NULL, (void *)(intptr_t)0, SENTRY_RETRY_INTERVAL); + if (status_code < 0) { + sentry__retry_enqueue(state->retry, envelope); } } @@ -318,10 +299,7 @@ http_transport_start(const sentry_options_t *options, void *transport_state) } state->retry = sentry__retry_new(options); - if (state->retry) { - sentry__bgworker_submit_delayed(bgworker, retry_process_task, NULL, - (void *)(intptr_t)1, SENTRY_RETRY_THROTTLE); - } + sentry__retry_start(state->retry, bgworker, retry_send_cb, state); return 0; } @@ -395,8 +373,6 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) http_transport_state_free(state); return NULL; } - state->bgworker = bgworker; - sentry_transport_t *transport = sentry_transport_new(http_transport_send_envelope); if (!transport) { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 375a55f51..515c8bd9a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -105,8 +105,7 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts - = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); @@ -142,8 +141,7 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts - = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir @@ -253,8 +251,7 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts - = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, old_ts, 4, &event_id); @@ -292,7 +289,7 @@ SENTRY_TEST(retry_backoff) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t base = (SENTRY_RETRY_INTERVAL / 1000); + uint64_t base = sentry__retry_backoff(0); uint64_t ref = (uint64_t)time(NULL) - 10 * base; // retry 0: 10*base old, eligible (backoff=base) @@ -350,8 +347,7 @@ SENTRY_TEST(retry_no_duplicate_rescan) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts - = (uint64_t)time(NULL) - 10 * (SENTRY_RETRY_INTERVAL / 1000); + uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry_path, old_ts, 0, &event_id); From 9c7c99dd507e5fd5adb99823b5c24e029f2193b5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:41:57 +0100 Subject: [PATCH 014/121] ref(transport): remove unnecessary includes, restore blank line Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 90e7ff9d5..9c00deb20 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -8,15 +8,12 @@ #include "sentry_retry.h" #include "sentry_string.h" #include "sentry_transport.h" -#include "sentry_utils.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" #endif -#include #include -#include #define ENVELOPE_MIME "application/x-sentry-envelope" #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -373,6 +370,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) http_transport_state_free(state); return NULL; } + sentry_transport_t *transport = sentry_transport_new(http_transport_send_envelope); if (!transport) { From 3813feb1f00e856277db747cda836a9f4803dadb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:48:12 +0100 Subject: [PATCH 015/121] ref(retry): move precondition checks to callers sentry__retry_new only returns NULL on failure, not based on options. sentry__retry_start and _enqueue require non-NULL retry. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 9 --------- src/transports/sentry_http_transport.c | 10 +++++++--- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 20767d49a..6766d88ee 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -24,9 +24,6 @@ struct sentry_retry_s { sentry_retry_t * sentry__retry_new(const sentry_options_t *options) { - if (options->http_retries <= 0 || !options->database_path) { - return NULL; - } sentry_path_t *retry_dir = sentry__path_join_str(options->database_path, "retry"); if (!retry_dir) { @@ -93,9 +90,6 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data) { - if (!retry) { - return; - } retry->bgworker = bgworker; retry->send_cb = send_cb; retry->send_data = send_data; @@ -106,9 +100,6 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { - if (!retry) { - return; - } sentry__retry_write_envelope(retry, envelope); sentry__bgworker_submit_delayed( retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 9c00deb20..fe0102e37 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -267,7 +267,7 @@ http_send_task(void *_envelope, void *_state) int status_code = http_send_request(state, req); sentry__prepared_http_request_free(req); - if (status_code < 0) { + if (status_code < 0 && state->retry) { sentry__retry_enqueue(state->retry, envelope); } } @@ -295,8 +295,12 @@ http_transport_start(const sentry_options_t *options, void *transport_state) return rv; } - state->retry = sentry__retry_new(options); - sentry__retry_start(state->retry, bgworker, retry_send_cb, state); + if (options->http_retries > 0) { + state->retry = sentry__retry_new(options); + if (state->retry) { + sentry__retry_start(state->retry, bgworker, retry_send_cb, state); + } + } return 0; } From ea5233fa93577171e7178711e8bc0d10836dd108 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:55:04 +0100 Subject: [PATCH 016/121] ref(transport): extract http_send_envelope helper Deduplicate prepare/send/free sequence shared by retry_send_cb and http_send_task. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 34 ++++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index fe0102e37..4b688177e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -213,6 +213,19 @@ http_send_request( return resp.status_code; } +static int +http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope) +{ + sentry_prepared_http_request_t *req = sentry__prepare_http_request( + envelope, state->dsn, state->ratelimiter, state->user_agent); + if (!req) { + return 0; + } + int status_code = http_send_request(state, req); + sentry__prepared_http_request_free(req); + return status_code; +} + static bool retry_send_cb(const sentry_path_t *path, void *_state) { @@ -224,17 +237,8 @@ retry_send_cb(const sentry_path_t *path, void *_state) return true; } - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - envelope, state->dsn, state->ratelimiter, state->user_agent); - int status_code; - if (!req) { - status_code = 0; - } else { - status_code = http_send_request(state, req); - sentry__prepared_http_request_free(req); - } + int status_code = http_send_envelope(state, envelope); sentry_envelope_free(envelope); - return sentry__retry_handle_result(state->retry, path, status_code); } @@ -258,15 +262,7 @@ http_send_task(void *_envelope, void *_state) sentry_envelope_t *envelope = _envelope; http_transport_state_t *state = _state; - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - envelope, state->dsn, state->ratelimiter, state->user_agent); - if (!req) { - return; - } - - int status_code = http_send_request(state, req); - sentry__prepared_http_request_free(req); - + int status_code = http_send_envelope(state, envelope); if (status_code < 0 && state->retry) { sentry__retry_enqueue(state->retry, envelope); } From f17f8f93aac697510ef352ee06f2b205e1ee1ec8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 16:59:33 +0100 Subject: [PATCH 017/121] test(retry): remove redundant retry_no_duplicate_rescan test Already covered by retry_throttle and retry_result. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 36 ------------------------------------ tests/unit/tests.inc | 1 - 2 files changed, 37 deletions(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 515c8bd9a..a6b84a562 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -330,39 +330,3 @@ SENTRY_TEST(retry_backoff) sentry__path_remove_all(db_path); sentry__path_free(db_path); } - -SENTRY_TEST(retry_no_duplicate_rescan) -{ - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); - sentry__path_remove_all(db_path); - - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-no-dup-rescan"); - sentry_options_set_http_retries(options, 3); - sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); - TEST_ASSERT(!!retry); - - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - - uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); - sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry_path, old_ts, 0, &event_id); - - // First scan returns the file - retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); - TEST_CHECK_INT_EQUAL(ctx.count, 1); - - // Second scan returns nothing - ctx.count = 0; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); - TEST_CHECK_INT_EQUAL(ctx.count, 0); - - sentry__path_free(retry_path); - sentry__retry_free(retry); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); -} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 0b8ceefc8..537510365 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -193,7 +193,6 @@ XX(read_write_envelope_to_invalid_path) XX(recursive_paths) XX(retry_backoff) XX(retry_cache) -XX(retry_no_duplicate_rescan) XX(retry_result) XX(retry_session) XX(retry_throttle) From e7886a4e48335da4750e1c9ae8f484b70d33b72c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:02:22 +0100 Subject: [PATCH 018/121] ref(curl): use CURLOPT_TIMEOUT_MS for consistency with winhttp and crashpad Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_curl.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 417c24cfe..969f86925 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -189,7 +189,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); + curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 15000L); char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; From ce4ac80c107addef7e2065101cd23cbf16a1c2f9 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:20:24 +0100 Subject: [PATCH 019/121] ref(retry): unify startup and poll into a single task Pass startup_time directly to _foreach as a `before` filter instead of a bool. Clear it after the first run so subsequent polls use backoff. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 32 ++++++++++---------------------- src/sentry_retry.h | 2 +- tests/unit/test_retry.c | 18 +++++++++--------- 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6766d88ee..f388c9da6 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -62,28 +62,18 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } -static void retry_poll_task(void *_retry, void *_state); - -static void -retry_startup_task(void *_retry, void *_state) -{ - (void)_state; - sentry_retry_t *retry = _retry; - if (sentry__retry_foreach(retry, true, retry->send_cb, retry->send_data)) { - sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, - retry, SENTRY_RETRY_INTERVAL); - } -} - static void retry_poll_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (sentry__retry_foreach(retry, false, retry->send_cb, retry->send_data)) { + if (sentry__retry_foreach( + retry, retry->startup_time, retry->send_cb, retry->send_data)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } + // subsequent polls use backoff instead of the startup time filter + retry->startup_time = 0; } void @@ -94,7 +84,7 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, retry->send_cb = send_cb; retry->send_data = send_data; sentry__bgworker_submit_delayed( - bgworker, retry_startup_task, NULL, retry, SENTRY_RETRY_THROTTLE); + bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); } void @@ -174,7 +164,7 @@ sentry__retry_write_envelope( } size_t -sentry__retry_foreach(sentry_retry_t *retry, bool startup, +sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, bool (*callback)(const sentry_path_t *path, void *data), void *data) { sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); @@ -191,7 +181,7 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, size_t total = 0; size_t eligible = 0; - uint64_t now = startup ? 0 : (uint64_t)time(NULL); + uint64_t now = before ? 0 : (uint64_t)time(NULL); const sentry_path_t *p; while ((p = sentry__pathiter_next(piter)) != NULL) { @@ -202,13 +192,11 @@ sentry__retry_foreach(sentry_retry_t *retry, bool startup, if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { continue; } - if (startup) { - if (retry->startup_time > 0 && ts >= retry->startup_time) { - continue; - } + if (before && ts >= before) { + continue; } total++; - if (!startup && (now - ts) < sentry__retry_backoff(count)) { + if (!before && (now - ts) < sentry__retry_backoff(count)) { continue; } if (eligible == path_cap) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 9151888b1..898ce3a13 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -21,7 +21,7 @@ void sentry__retry_enqueue( void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); -size_t sentry__retry_foreach(sentry_retry_t *retry, bool startup, +size_t sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, bool (*callback)(const sentry_path_t *path, void *data), void *data); bool sentry__retry_handle_result( diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index a6b84a562..fc90e3a7f 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -115,7 +115,7 @@ SENTRY_TEST(retry_throttle) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 4); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -150,21 +150,21 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 2. Rate limited (429) → removes write_retry_file(retry_path, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 429, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 3. Discard (0) → removes write_retry_file(retry_path, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 0, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -173,7 +173,7 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); @@ -183,7 +183,7 @@ SENTRY_TEST(retry_result) sentry__path_create_dir_all(retry_path); write_retry_file(retry_path, old_ts, 1, &event_id); ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -260,7 +260,7 @@ SENTRY_TEST(retry_cache) // Network error on a file at count=4 with max_retries=5 → moves to cache retry_test_ctx_t ctx = { retry, -1, 0 }; - sentry__retry_foreach(retry, false, handle_result_cb, &ctx); + sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -310,12 +310,12 @@ SENTRY_TEST(retry_backoff) // Startup scan (no backoff check): all 4 files returned size_t count = 0; - sentry__retry_foreach(retry, true, count_cb, &count); + sentry__retry_foreach(retry, (uint64_t)time(NULL), count_cb, &count); TEST_CHECK_INT_EQUAL(count, 4); // With backoff check: only eligible ones (id1 and id3) count = 0; - sentry__retry_foreach(retry, false, count_cb, &count); + sentry__retry_foreach(retry, 0, count_cb, &count); TEST_CHECK_INT_EQUAL(count, 2); // Verify backoff calculation From 34d5131f31c4cae69bc848dc16ddee289d8675ba Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 17:47:36 +0100 Subject: [PATCH 020/121] ref(retry): extract sentry__retry_make_path helper Deduplicate filename construction across write_envelope, handle_result, and tests. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 46 +++++++++++++++++++++-------------------- src/sentry_retry.h | 9 ++++++++ tests/unit/test_retry.c | 36 +++++++++++++++----------------- 3 files changed, 50 insertions(+), 41 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index f388c9da6..99fe1f0c9 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -111,16 +111,16 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, return false; } - const char *uuid_start = end + 1; - size_t tail_len = strlen(uuid_start); + const char *uuid = end + 1; + size_t tail_len = strlen(uuid); // 36 chars UUID (with dashes) + ".envelope" - if (tail_len != 36 + 9 || strcmp(uuid_start + 36, ".envelope") != 0) { + if (tail_len != 36 + 9 || strcmp(uuid + 36, ".envelope") != 0) { return false; } *ts_out = ts; *count_out = (int)count; - *uuid_out = uuid_start; + *uuid_out = uuid; return true; } @@ -139,6 +139,16 @@ compare_retry_paths(const void *a, const void *b) return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); } +sentry_path_t * +sentry__retry_make_path( + sentry_retry_t *retry, uint64_t ts, int count, const char *uuid) +{ + char filename[128]; + snprintf(filename, sizeof(filename), "%llu-%02d-%.36s.envelope", + (unsigned long long)ts, count, uuid); + return sentry__path_join_str(retry->retry_dir, filename); +} + void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope) @@ -148,15 +158,11 @@ sentry__retry_write_envelope( return; } - uint64_t now = (uint64_t)time(NULL); - char uuid_str[37]; - sentry_uuid_as_string(&event_id, uuid_str); - - char filename[128]; - snprintf(filename, sizeof(filename), "%llu-00-%s.envelope", - (unsigned long long)now, uuid_str); + char uuid[37]; + sentry_uuid_as_string(&event_id, uuid); - sentry_path_t *path = sentry__path_join_str(retry->retry_dir, filename); + sentry_path_t *path + = sentry__retry_make_path(retry, (uint64_t)time(NULL), 0, uuid); if (path) { (void)sentry_envelope_write_to_path(envelope, path); sentry__path_free(path); @@ -188,8 +194,8 @@ sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, const char *fname = sentry__path_filename(p); uint64_t ts; int count; - const char *uuid_start; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + const char *uuid; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { continue; } if (before && ts >= before) { @@ -238,8 +244,8 @@ sentry__retry_handle_result( const char *fname = sentry__path_filename(path); uint64_t ts; int count; - const char *uuid_start; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid_start)) { + const char *uuid; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { sentry__path_remove(path); return false; } @@ -260,12 +266,8 @@ sentry__retry_handle_result( } return false; } else { - uint64_t now = (uint64_t)time(NULL); - char new_filename[128]; - snprintf(new_filename, sizeof(new_filename), "%llu-%02d-%s", - (unsigned long long)now, count + 1, uuid_start); - sentry_path_t *new_path - = sentry__path_join_str(retry->retry_dir, new_filename); + sentry_path_t *new_path = sentry__retry_make_path( + retry, (uint64_t)time(NULL), count + 1, uuid); if (new_path) { sentry__path_rename(path, new_path); sentry__path_free(new_path); diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 898ce3a13..665fa7564 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -29,6 +29,15 @@ bool sentry__retry_handle_result( uint64_t sentry__retry_backoff(int count); +/** + * /retry/--.envelope + */ +sentry_path_t *sentry__retry_make_path( + sentry_retry_t *retry, uint64_t ts, int count, const char *uuid); + +/** + * --.envelope + */ bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out); diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index fc90e3a7f..8fb8c9042 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -47,20 +47,18 @@ find_envelope_attempt(const sentry_path_t *dir) } static void -write_retry_file(const sentry_path_t *retry_path, uint64_t timestamp, - int retry_count, const sentry_uuid_t *event_id) +write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, + const sentry_uuid_t *event_id) { sentry_envelope_t *envelope = sentry__envelope_new(); sentry_value_t event = sentry__value_new_event_with_id(event_id); sentry__envelope_add_event(envelope, event); - char uuid_str[37]; - sentry_uuid_as_string(event_id, uuid_str); - char filename[80]; - snprintf(filename, sizeof(filename), "%llu-%02d-%s.envelope", - (unsigned long long)timestamp, retry_count, uuid_str); + char uuid[37]; + sentry_uuid_as_string(event_id, uuid); - sentry_path_t *path = sentry__path_join_str(retry_path, filename); + sentry_path_t *path + = sentry__retry_make_path(retry, timestamp, retry_count, uuid); (void)sentry_envelope_write_to_path(envelope, path); sentry__path_free(path); sentry_envelope_free(envelope); @@ -109,7 +107,7 @@ SENTRY_TEST(retry_throttle) sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); - write_retry_file(retry_path, old_ts, 0, &ids[i]); + write_retry_file(retry, old_ts, 0, &ids[i]); } TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); @@ -145,7 +143,7 @@ SENTRY_TEST(retry_result) sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir - write_retry_file(retry_path, old_ts, 0, &event_id); + write_retry_file(retry, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); @@ -155,21 +153,21 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 2. Rate limited (429) → removes - write_retry_file(retry_path, old_ts, 0, &event_id); + write_retry_file(retry, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 429, 0 }; sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 3. Discard (0) → removes - write_retry_file(retry_path, old_ts, 0, &event_id); + write_retry_file(retry, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { retry, 0, 0 }; sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 4. Network error → bumps count - write_retry_file(retry_path, old_ts, 0, &event_id); + write_retry_file(retry, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); ctx = (retry_test_ctx_t) { retry, -1, 0 }; @@ -181,7 +179,7 @@ SENTRY_TEST(retry_result) // 5. Network error at max count → exceeds max_retries=2, removed sentry__path_remove_all(retry_path); sentry__path_create_dir_all(retry_path); - write_retry_file(retry_path, old_ts, 1, &event_id); + write_retry_file(retry, old_ts, 1, &event_id); ctx = (retry_test_ctx_t) { retry, -1, 0 }; sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); @@ -253,7 +251,7 @@ SENTRY_TEST(retry_cache) uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry_path, old_ts, 4, &event_id); + write_retry_file(retry, old_ts, 4, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); @@ -294,19 +292,19 @@ SENTRY_TEST(retry_backoff) // retry 0: 10*base old, eligible (backoff=base) sentry_uuid_t id1 = sentry_uuid_new_v4(); - write_retry_file(retry_path, ref, 0, &id1); + write_retry_file(retry, ref, 0, &id1); // retry 1: 1*base old, not yet eligible (backoff=2*base) sentry_uuid_t id2 = sentry_uuid_new_v4(); - write_retry_file(retry_path, ref + 9 * base, 1, &id2); + write_retry_file(retry, ref + 9 * base, 1, &id2); // retry 1: 10*base old, eligible (backoff=2*base) sentry_uuid_t id3 = sentry_uuid_new_v4(); - write_retry_file(retry_path, ref, 1, &id3); + write_retry_file(retry, ref, 1, &id3); // retry 2: 2*base old, not eligible (backoff=4*base) sentry_uuid_t id4 = sentry_uuid_new_v4(); - write_retry_file(retry_path, ref + 8 * base, 2, &id4); + write_retry_file(retry, ref + 8 * base, 2, &id4); // Startup scan (no backoff check): all 4 files returned size_t count = 0; From 8015d2cc2c1a834f3f5eca80d495ffdf389d3089 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 20:03:19 +0100 Subject: [PATCH 021/121] fix(retry): prevent envelope duplication between retry and cache When the transport supports retry and http_retries > 0, sentry__process_old_runs now skips caching .envelope files from old runs. The retry system handles persistence, so duplicating into cache/ is unnecessary. Also simplifies sentry__retry_handle_result: only cache on max retries exhausted, not on successful send. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 5 ++- src/sentry_retry.c | 49 +++++++++----------------- src/sentry_transport.c | 13 +++++++ src/sentry_transport.h | 4 +++ src/transports/sentry_http_transport.c | 1 + tests/test_integration_http.py | 3 +- 6 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 45f8b8eb1..7976ffc6b 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -4,6 +4,7 @@ #include "sentry_json.h" #include "sentry_options.h" #include "sentry_session.h" +#include "sentry_transport.h" #include "sentry_uuid.h" #include #include @@ -292,7 +293,9 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - if (cache_dir) { + bool can_retry = sentry__transport_can_retry(options->transport) + && options->http_retries > 0; + if (cache_dir && !can_retry) { sentry_path_t *cached_file = sentry__path_join_str( cache_dir, sentry__path_filename(file)); if (!cached_file diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 99fe1f0c9..0f145cbc2 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -250,39 +250,24 @@ sentry__retry_handle_result( return false; } - if (status_code < 0) { - if (count + 1 >= retry->max_retries) { - if (retry->cache_dir) { - sentry_path_t *dst - = sentry__path_join_str(retry->cache_dir, fname); - if (dst) { - sentry__path_rename(path, dst); - sentry__path_free(dst); - } else { - sentry__path_remove(path); - } - } else { - sentry__path_remove(path); - } - return false; - } else { - sentry_path_t *new_path = sentry__retry_make_path( - retry, (uint64_t)time(NULL), count + 1, uuid); - if (new_path) { - sentry__path_rename(path, new_path); - sentry__path_free(new_path); - } - return true; + if (status_code < 0 && count + 1 < retry->max_retries) { + sentry_path_t *new_path = sentry__retry_make_path( + retry, (uint64_t)time(NULL), count + 1, uuid); + if (new_path) { + sentry__path_rename(path, new_path); + sentry__path_free(new_path); } - } else if (status_code >= 200 && status_code < 300) { - if (retry->cache_dir) { - sentry_path_t *dst = sentry__path_join_str(retry->cache_dir, fname); - if (dst) { - sentry__path_rename(path, dst); - sentry__path_free(dst); - } else { - sentry__path_remove(path); - } + return true; + } + + if (count + 1 >= retry->max_retries && retry->cache_dir) { + char cache_name[46]; + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid); + sentry_path_t *dst + = sentry__path_join_str(retry->cache_dir, cache_name); + if (dst) { + sentry__path_rename(path, dst); + sentry__path_free(dst); } else { sentry__path_remove(path); } diff --git a/src/sentry_transport.c b/src/sentry_transport.c index 6b63c5783..744570928 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -12,6 +12,7 @@ struct sentry_transport_s { size_t (*dump_func)(sentry_run_t *run, void *state); void *state; bool running; + bool can_retry; }; sentry_transport_t * @@ -147,3 +148,15 @@ sentry__transport_get_state(sentry_transport_t *transport) { return transport ? transport->state : NULL; } + +void +sentry__transport_set_can_retry(sentry_transport_t *transport, bool can_retry) +{ + transport->can_retry = can_retry; +} + +bool +sentry__transport_can_retry(sentry_transport_t *transport) +{ + return transport && transport->can_retry; +} diff --git a/src/sentry_transport.h b/src/sentry_transport.h index 036233284..ebb901ac6 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -57,4 +57,8 @@ size_t sentry__transport_dump_queue( void *sentry__transport_get_state(sentry_transport_t *transport); +void sentry__transport_set_can_retry( + sentry_transport_t *transport, bool can_retry); +bool sentry__transport_can_retry(sentry_transport_t *transport); + #endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 4b688177e..5ee48b7a7 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -385,6 +385,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) sentry_transport_set_flush_func(transport, http_transport_flush); sentry_transport_set_shutdown_func(transport, http_transport_shutdown); sentry__transport_set_dump_func(transport, http_dump_queue); + sentry__transport_set_can_retry(transport, true); return transport; } diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index f14c1645a..dc451ae92 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -953,7 +953,8 @@ def test_http_retry_with_cache_keep(cmake, httpserver): assert waiting.result assert len(list(retry_dir.glob("*.envelope"))) == 0 - assert len(list(cache_dir.glob("*.envelope"))) == 1 + cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") From 8fcdb4f7c6cc246d2448425c2e9fd9e915a9d532 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 21:19:40 +0100 Subject: [PATCH 022/121] ref(database): derive can_cache flag to skip cache dir creation early Move the retry-aware check before cache_dir creation so we avoid mkdir when the retry system handles persistence. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 7976ffc6b..0315c1484 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -240,8 +240,12 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) continue; } + bool can_cache = options->cache_keep + && (options->http_retries == 0 + || !sentry__transport_can_retry(options->transport)); + sentry_path_t *cache_dir = NULL; - if (options->cache_keep) { + if (can_cache) { cache_dir = sentry__path_join_str(options->database_path, "cache"); if (cache_dir) { sentry__path_create_dir_all(cache_dir); @@ -293,9 +297,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - bool can_retry = sentry__transport_can_retry(options->transport) - && options->http_retries > 0; - if (cache_dir && !can_retry) { + if (cache_dir) { sentry_path_t *cached_file = sentry__path_join_str( cache_dir, sentry__path_filename(file)); if (!cached_file From 551ad7464fd889263517aeb3ec748217048a2908 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 21:36:59 +0100 Subject: [PATCH 023/121] ref(retry): change send callback to envelope-based API The retry callback now receives a sentry_envelope_t and returns a status code. The retry system handles deserialization and file lifecycle internally, keeping path concerns out of the transport. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 148 +++++++++++++------------ src/sentry_retry.h | 10 +- src/transports/sentry_http_transport.c | 16 +-- tests/unit/test_retry.c | 83 ++++++++------ 4 files changed, 133 insertions(+), 124 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 0f145cbc2..6881880cb 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -62,39 +62,6 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } -static void -retry_poll_task(void *_retry, void *_state) -{ - (void)_state; - sentry_retry_t *retry = _retry; - if (sentry__retry_foreach( - retry, retry->startup_time, retry->send_cb, retry->send_data)) { - sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, - retry, SENTRY_RETRY_INTERVAL); - } - // subsequent polls use backoff instead of the startup time filter - retry->startup_time = 0; -} - -void -sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, - sentry_retry_send_func_t send_cb, void *send_data) -{ - retry->bgworker = bgworker; - retry->send_cb = send_cb; - retry->send_data = send_data; - sentry__bgworker_submit_delayed( - bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); -} - -void -sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) -{ - sentry__retry_write_envelope(retry, envelope); - sentry__bgworker_submit_delayed( - retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); -} - bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out) @@ -169,9 +136,48 @@ sentry__retry_write_envelope( } } +static bool +handle_result(sentry_retry_t *retry, const sentry_path_t *path, int status_code) +{ + const char *fname = sentry__path_filename(path); + uint64_t ts; + int count; + const char *uuid; + if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { + sentry__path_remove(path); + return false; + } + + if (status_code < 0 && count + 1 < retry->max_retries) { + sentry_path_t *new_path = sentry__retry_make_path( + retry, (uint64_t)time(NULL), count + 1, uuid); + if (new_path) { + sentry__path_rename(path, new_path); + sentry__path_free(new_path); + } + return true; + } + + if (count + 1 >= retry->max_retries && retry->cache_dir) { + char cache_name[46]; + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid); + sentry_path_t *dst + = sentry__path_join_str(retry->cache_dir, cache_name); + if (dst) { + sentry__path_rename(path, dst); + sentry__path_free(dst); + } else { + sentry__path_remove(path); + } + } else { + sentry__path_remove(path); + } + return false; +} + size_t -sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, - bool (*callback)(const sentry_path_t *path, void *data), void *data) +sentry__retry_send(sentry_retry_t *retry, uint64_t before, + sentry_retry_send_func_t send_cb, void *data) { sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); if (!piter) { @@ -225,8 +231,15 @@ sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, } for (size_t i = 0; i < eligible; i++) { - if (!callback(paths[i], data)) { - total--; + sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); + if (!envelope) { + sentry__path_remove(paths[i]); + } else { + int status_code = send_cb(envelope, data); + sentry_envelope_free(envelope); + if (!handle_result(retry, paths[i], status_code)) { + total--; + } } } @@ -237,42 +250,35 @@ sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, return total; } -bool -sentry__retry_handle_result( - sentry_retry_t *retry, const sentry_path_t *path, int status_code) +static void +retry_poll_task(void *_retry, void *_state) { - const char *fname = sentry__path_filename(path); - uint64_t ts; - int count; - const char *uuid; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { - sentry__path_remove(path); - return false; + (void)_state; + sentry_retry_t *retry = _retry; + if (sentry__retry_send( + retry, retry->startup_time, retry->send_cb, retry->send_data)) { + sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, + retry, SENTRY_RETRY_INTERVAL); } + // subsequent polls use backoff instead of the startup time filter + retry->startup_time = 0; +} - if (status_code < 0 && count + 1 < retry->max_retries) { - sentry_path_t *new_path = sentry__retry_make_path( - retry, (uint64_t)time(NULL), count + 1, uuid); - if (new_path) { - sentry__path_rename(path, new_path); - sentry__path_free(new_path); - } - return true; - } +void +sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, + sentry_retry_send_func_t send_cb, void *send_data) +{ + retry->bgworker = bgworker; + retry->send_cb = send_cb; + retry->send_data = send_data; + sentry__bgworker_submit_delayed( + bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); +} - if (count + 1 >= retry->max_retries && retry->cache_dir) { - char cache_name[46]; - snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid); - sentry_path_t *dst - = sentry__path_join_str(retry->cache_dir, cache_name); - if (dst) { - sentry__path_rename(path, dst); - sentry__path_free(dst); - } else { - sentry__path_remove(path); - } - } else { - sentry__path_remove(path); - } - return false; +void +sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) +{ + sentry__retry_write_envelope(retry, envelope); + sentry__bgworker_submit_delayed( + retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 665fa7564..a75045e67 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -7,7 +7,8 @@ typedef struct sentry_retry_s sentry_retry_t; -typedef bool (*sentry_retry_send_func_t)(const sentry_path_t *path, void *data); +typedef int (*sentry_retry_send_func_t)( + sentry_envelope_t *envelope, void *data); sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); @@ -21,11 +22,8 @@ void sentry__retry_enqueue( void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); -size_t sentry__retry_foreach(sentry_retry_t *retry, uint64_t before, - bool (*callback)(const sentry_path_t *path, void *data), void *data); - -bool sentry__retry_handle_result( - sentry_retry_t *retry, const sentry_path_t *path, int status_code); +size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, + sentry_retry_send_func_t send_cb, void *data); uint64_t sentry__retry_backoff(int count); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 5ee48b7a7..1cb3ac42d 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -3,7 +3,6 @@ #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" -#include "sentry_path.h" #include "sentry_ratelimiter.h" #include "sentry_retry.h" #include "sentry_string.h" @@ -226,20 +225,11 @@ http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope) return status_code; } -static bool -retry_send_cb(const sentry_path_t *path, void *_state) +static int +retry_send_cb(sentry_envelope_t *envelope, void *_state) { http_transport_state_t *state = _state; - - sentry_envelope_t *envelope = sentry__envelope_from_path(path); - if (!envelope) { - sentry__path_remove(path); - return true; - } - - int status_code = http_send_envelope(state, envelope); - sentry_envelope_free(envelope); - return sentry__retry_handle_result(state->retry, path, status_code); + return http_send_envelope(state, envelope); } static void diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 8fb8c9042..eec3f67f7 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -46,6 +46,33 @@ find_envelope_attempt(const sentry_path_t *dir) return -1; } +static int +count_eligible_files(const sentry_path_t *dir, uint64_t before) +{ + int eligible = 0; + uint64_t now = before ? 0 : (uint64_t)time(NULL); + sentry_pathiter_t *iter = sentry__path_iter_directory(dir); + const sentry_path_t *file; + while (iter && (file = sentry__pathiter_next(iter)) != NULL) { + const char *name = sentry__path_filename(file); + uint64_t ts; + int count; + const char *uuid; + if (!sentry__retry_parse_filename(name, &ts, &count, &uuid)) { + continue; + } + if (before && ts >= before) { + continue; + } + if (!before && (now - ts) < sentry__retry_backoff(count)) { + continue; + } + eligible++; + } + sentry__pathiter_free(iter); + return eligible; +} + static void write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, const sentry_uuid_t *event_id) @@ -65,26 +92,17 @@ write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, } typedef struct { - sentry_retry_t *retry; int status_code; size_t count; } retry_test_ctx_t; -static bool -handle_result_cb(const sentry_path_t *path, void *_ctx) +static int +test_send_cb(sentry_envelope_t *envelope, void *_ctx) { + (void)envelope; retry_test_ctx_t *ctx = _ctx; ctx->count++; - sentry__retry_handle_result(ctx->retry, path, ctx->status_code); - return true; -} - -static bool -count_cb(const sentry_path_t *path, void *_count) -{ - (void)path; - (*(size_t *)_count)++; - return true; + return ctx->status_code; } SENTRY_TEST(retry_throttle) @@ -112,8 +130,8 @@ SENTRY_TEST(retry_throttle) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); - retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 4); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -147,22 +165,22 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); - retry_test_ctx_t ctx = { retry, 200, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 2. Rate limited (429) → removes write_retry_file(retry, old_ts, 0, &event_id); - ctx = (retry_test_ctx_t) { retry, 429, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + ctx = (retry_test_ctx_t) { 429, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // 3. Discard (0) → removes write_retry_file(retry, old_ts, 0, &event_id); - ctx = (retry_test_ctx_t) { retry, 0, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + ctx = (retry_test_ctx_t) { 0, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -170,8 +188,8 @@ SENTRY_TEST(retry_result) write_retry_file(retry, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); - ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); @@ -180,8 +198,8 @@ SENTRY_TEST(retry_result) sentry__path_remove_all(retry_path); sentry__path_create_dir_all(retry_path); write_retry_file(retry, old_ts, 1, &event_id); - ctx = (retry_test_ctx_t) { retry, -1, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -257,8 +275,8 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // Network error on a file at count=4 with max_retries=5 → moves to cache - retry_test_ctx_t ctx = { retry, -1, 0 }; - sentry__retry_foreach(retry, 0, handle_result_cb, &ctx); + retry_test_ctx_t ctx = { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); @@ -306,15 +324,12 @@ SENTRY_TEST(retry_backoff) sentry_uuid_t id4 = sentry_uuid_new_v4(); write_retry_file(retry, ref + 8 * base, 2, &id4); - // Startup scan (no backoff check): all 4 files returned - size_t count = 0; - sentry__retry_foreach(retry, (uint64_t)time(NULL), count_cb, &count); - TEST_CHECK_INT_EQUAL(count, 4); + // Startup scan (no backoff check): all 4 files + TEST_CHECK_INT_EQUAL( + count_eligible_files(retry_path, (uint64_t)time(NULL)), 4); // With backoff check: only eligible ones (id1 and id3) - count = 0; - sentry__retry_foreach(retry, 0, count_cb, &count); - TEST_CHECK_INT_EQUAL(count, 2); + TEST_CHECK_INT_EQUAL(count_eligible_files(retry_path, 0), 2); // Verify backoff calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); From d2f1a071779bec58baa2e7b53cdf15a198d9cb49 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:08:46 +0100 Subject: [PATCH 024/121] test(retry): verify cache_keep preserves envelopes on successful send Add test case for successful send at max retry count with cache_keep enabled, confirming envelopes are cached regardless of send outcome. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index eec3f67f7..603e93652 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -282,6 +282,20 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + // Success on a file at count=4 → also moves to cache (cache_keep + // preserves all envelopes regardless of send outcome) + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); + write_retry_file(retry, old_ts, 4, &event_id); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + + ctx = (retry_test_ctx_t) { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); + + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + sentry__retry_free(retry); sentry__path_free(retry_path); sentry__path_free(cache_path); From 3750856bf57eb1e2f4c482e3d28af7585c9c1dd7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:10:39 +0100 Subject: [PATCH 025/121] fix(retry): use PRIu64 format specifier for uint64_t Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6881880cb..eb2e25071 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -111,8 +111,8 @@ sentry__retry_make_path( sentry_retry_t *retry, uint64_t ts, int count, const char *uuid) { char filename[128]; - snprintf(filename, sizeof(filename), "%llu-%02d-%.36s.envelope", - (unsigned long long)ts, count, uuid); + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, + count, uuid); return sentry__path_join_str(retry->retry_dir, filename); } From 0cbf407aadf01ab31d6d4269e1357169bae1ea75 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 12 Feb 2026 22:11:30 +0100 Subject: [PATCH 026/121] fix(retry): guard against unsigned underflow in backoff check Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index eb2e25071..77959f2a1 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -208,7 +208,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, continue; } total++; - if (!before && (now - ts) < sentry__retry_backoff(count)) { + if (!before && now >= ts && (now - ts) < sentry__retry_backoff(count)) { continue; } if (eligible == path_cap) { From 3b2a5d6b89dcdbf337da83c4e0c80285882fef3d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 13 Feb 2026 17:14:04 +0100 Subject: [PATCH 027/121] fix(retry): prevent startup poll from re-processing same-session envelopes The startup poll used `ts >= startup_time` to skip envelopes written after startup. With second-precision timestamps, this also skipped cross-session envelopes written in the same second as a fast restart. Reset `startup_time` in `sentry__retry_enqueue` so the startup poll falls through to the backoff path for same-session envelopes. The bgworker processes the send task (immediate) before the startup poll (delayed), so by the time the poll fires, `startup_time` is already 0. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 6 ++++-- tests/unit/test_retry.c | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 77959f2a1..393705913 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -193,7 +193,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, size_t total = 0; size_t eligible = 0; - uint64_t now = before ? 0 : (uint64_t)time(NULL); + uint64_t now = before > 0 ? 0 : (uint64_t)time(NULL); const sentry_path_t *p; while ((p = sentry__pathiter_next(piter)) != NULL) { @@ -204,7 +204,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { continue; } - if (before && ts >= before) { + if (before > 0 && ts >= before) { continue; } total++; @@ -279,6 +279,8 @@ void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { sentry__retry_write_envelope(retry, envelope); + // prevent the startup poll from re-processing this session's envelope + retry->startup_time = 0; sentry__bgworker_submit_delayed( retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 603e93652..7d29e6ba1 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -50,7 +50,7 @@ static int count_eligible_files(const sentry_path_t *dir, uint64_t before) { int eligible = 0; - uint64_t now = before ? 0 : (uint64_t)time(NULL); + uint64_t now = before > 0 ? 0 : (uint64_t)time(NULL); sentry_pathiter_t *iter = sentry__path_iter_directory(dir); const sentry_path_t *file; while (iter && (file = sentry__pathiter_next(iter)) != NULL) { @@ -61,7 +61,7 @@ count_eligible_files(const sentry_path_t *dir, uint64_t before) if (!sentry__retry_parse_filename(name, &ts, &count, &uuid)) { continue; } - if (before && ts >= before) { + if (before > 0 && ts >= before) { continue; } if (!before && (now - ts) < sentry__retry_backoff(count)) { From 6608084a4e1f5ff48db8781bc36ec93a3ce0ebfb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 10:40:22 +0100 Subject: [PATCH 028/121] fix(retry): flush pending retries on shutdown Submit a one-shot retry send task before bgworker shutdown to ensure pre-existing retry files are sent even if the startup poll hasn't fired yet. The flush checks startup_time on the worker thread to avoid re-sending files already handled by enqueue. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 19 +++++++++++++++++++ src/sentry_retry.h | 2 ++ src/transports/sentry_http_transport.c | 2 ++ 3 files changed, 23 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 393705913..f5b4bbf14 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -275,6 +275,25 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); } +static void +retry_flush_task(void *_retry, void *_state) +{ + (void)_state; + sentry_retry_t *retry = _retry; + if (retry->startup_time > 0) { + sentry__retry_send(retry, UINT64_MAX, retry->send_cb, retry->send_data); + retry->startup_time = 0; + } +} + +void +sentry__retry_flush(sentry_retry_t *retry) +{ + if (retry) { + sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); + } +} + void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index a75045e67..9518388bb 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -16,6 +16,8 @@ void sentry__retry_free(sentry_retry_t *retry); void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data); +void sentry__retry_flush(sentry_retry_t *retry); + void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 1cb3ac42d..b43885bf9 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -304,6 +304,8 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); + sentry__retry_flush(state->retry); + int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0 && state->shutdown_client) { state->shutdown_client(state->client); From 961ec95973791040444b23a3f42515210ff2e426 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 15:37:21 +0100 Subject: [PATCH 029/121] ref(retry): use millisecond timestamps for retry filenames Replace `time(NULL)` (1-second granularity) with `sentry__usec_time() / 1000` (millisecond granularity) to avoid timestamp collisions that caused flaky `>=` vs `>` comparison behavior in CI. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 11 +++++------ tests/unit/test_retry.c | 13 ++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index f5b4bbf14..d7c0bc477 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -6,7 +6,6 @@ #include #include -#include #define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) #define SENTRY_RETRY_THROTTLE 100 @@ -43,7 +42,7 @@ sentry__retry_new(const sentry_options_t *options) retry->retry_dir = retry_dir; retry->cache_dir = cache_dir; retry->max_retries = options->http_retries; - retry->startup_time = (uint64_t)time(NULL); + retry->startup_time = sentry__usec_time() / 1000; sentry__path_create_dir_all(retry->retry_dir); if (retry->cache_dir) { sentry__path_create_dir_all(retry->cache_dir); @@ -95,7 +94,7 @@ uint64_t sentry__retry_backoff(int count) { int shift = count < 3 ? count : 3; - return (uint64_t)(SENTRY_RETRY_INTERVAL / 1000) << shift; + return (uint64_t)SENTRY_RETRY_INTERVAL << shift; } static int @@ -129,7 +128,7 @@ sentry__retry_write_envelope( sentry_uuid_as_string(&event_id, uuid); sentry_path_t *path - = sentry__retry_make_path(retry, (uint64_t)time(NULL), 0, uuid); + = sentry__retry_make_path(retry, sentry__usec_time() / 1000, 0, uuid); if (path) { (void)sentry_envelope_write_to_path(envelope, path); sentry__path_free(path); @@ -150,7 +149,7 @@ handle_result(sentry_retry_t *retry, const sentry_path_t *path, int status_code) if (status_code < 0 && count + 1 < retry->max_retries) { sentry_path_t *new_path = sentry__retry_make_path( - retry, (uint64_t)time(NULL), count + 1, uuid); + retry, sentry__usec_time() / 1000, count + 1, uuid); if (new_path) { sentry__path_rename(path, new_path); sentry__path_free(new_path); @@ -193,7 +192,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, size_t total = 0; size_t eligible = 0; - uint64_t now = before > 0 ? 0 : (uint64_t)time(NULL); + uint64_t now = before > 0 ? 0 : sentry__usec_time() / 1000; const sentry_path_t *p; while ((p = sentry__pathiter_next(piter)) != NULL) { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 7d29e6ba1..ee3f89547 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -7,7 +7,6 @@ #include "sentry_uuid.h" #include -#include static int count_envelope_files(const sentry_path_t *dir) @@ -50,7 +49,7 @@ static int count_eligible_files(const sentry_path_t *dir, uint64_t before) { int eligible = 0; - uint64_t now = before > 0 ? 0 : (uint64_t)time(NULL); + uint64_t now = before > 0 ? 0 : sentry__usec_time() / 1000; sentry_pathiter_t *iter = sentry__path_iter_directory(dir); const sentry_path_t *file; while (iter && (file = sentry__pathiter_next(iter)) != NULL) { @@ -121,7 +120,7 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); + uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); @@ -157,7 +156,7 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); + uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir @@ -267,7 +266,7 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts = (uint64_t)time(NULL) - 10 * sentry__retry_backoff(0); + uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry, old_ts, 4, &event_id); @@ -320,7 +319,7 @@ SENTRY_TEST(retry_backoff) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); uint64_t base = sentry__retry_backoff(0); - uint64_t ref = (uint64_t)time(NULL) - 10 * base; + uint64_t ref = sentry__usec_time() / 1000 - 10 * base; // retry 0: 10*base old, eligible (backoff=base) sentry_uuid_t id1 = sentry_uuid_new_v4(); @@ -340,7 +339,7 @@ SENTRY_TEST(retry_backoff) // Startup scan (no backoff check): all 4 files TEST_CHECK_INT_EQUAL( - count_eligible_files(retry_path, (uint64_t)time(NULL)), 4); + count_eligible_files(retry_path, sentry__usec_time() / 1000), 4); // With backoff check: only eligible ones (id1 and id3) TEST_CHECK_INT_EQUAL(count_eligible_files(retry_path, 0), 2); From 558c32e85700fa8cbdd3a336b62cee61d9b0d69e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:16:03 +0100 Subject: [PATCH 030/121] fix(retry): flush pending retries synchronously before shutdown Make sentry__retry_flush block until the flush task completes by adding a bgworker_flush call, and subtract the elapsed time from the shutdown timeout. This ensures retries are actually sent before the worker stops. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 ++- src/sentry_retry.h | 2 +- src/transports/sentry_http_transport.c | 8 ++++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d7c0bc477..222294bc1 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -286,10 +286,11 @@ retry_flush_task(void *_retry, void *_state) } void -sentry__retry_flush(sentry_retry_t *retry) +sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) { if (retry) { sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); + sentry__bgworker_flush(retry->bgworker, timeout); } } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 9518388bb..c84e1760a 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -16,7 +16,7 @@ void sentry__retry_free(sentry_retry_t *retry); void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data); -void sentry__retry_flush(sentry_retry_t *retry); +void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index b43885bf9..d9cfae9ca 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -7,6 +7,7 @@ #include "sentry_retry.h" #include "sentry_string.h" #include "sentry_transport.h" +#include "sentry_utils.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" @@ -304,9 +305,12 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - sentry__retry_flush(state->retry); + uint64_t started = sentry__monotonic_time(); + sentry__retry_flush(state->retry, timeout); + uint64_t elapsed = sentry__monotonic_time() - started; + uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; - int rv = sentry__bgworker_shutdown(bgworker, timeout); + int rv = sentry__bgworker_shutdown(bgworker, remaining); if (rv != 0 && state->shutdown_client) { state->shutdown_client(state->client); } From be1d7cf2363dc666c578fba388c3cbc1eff7effd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:16:08 +0100 Subject: [PATCH 031/121] fix(retry): stop retrying on network failure Break out of the send loop on the first network error to avoid wasting time on a dead connection. Remaining envelopes stay untouched for the next retry poll. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 222294bc1..9b7730a7f 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -239,6 +239,11 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, if (!handle_result(retry, paths[i], status_code)) { total--; } + // stop on network failure to avoid wasting time on a dead + // connection; remaining envelopes stay untouched for later + if (status_code < 0) { + break; + } } } From fccc565934c3bb6cbe2cd96f39f52e4236ff49f7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:28:13 +0100 Subject: [PATCH 032/121] fix(retry): dump unsent envelopes to retry dir on shutdown timeout When bgworker shutdown times out, persist any remaining queued envelopes to the retry directory so they are not lost. The retry module provides sentry__retry_dump_queue to keep retry internals out of the transport. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 18 ++++++++++++++++++ src/sentry_retry.h | 3 +++ src/transports/sentry_http_transport.c | 7 +++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 9b7730a7f..dbc6ded05 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -299,6 +299,24 @@ sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) } } +static bool +retry_dump_cb(void *_envelope, void *_retry) +{ + sentry__retry_write_envelope( + (sentry_retry_t *)_retry, (sentry_envelope_t *)_envelope); + return true; +} + +void +sentry__retry_dump_queue( + sentry_retry_t *retry, sentry_task_exec_func_t task_func) +{ + if (retry) { + sentry__bgworker_foreach_matching( + retry->bgworker, task_func, retry_dump_cb, retry); + } +} + void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index c84e1760a..bd3b00ce4 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -18,6 +18,9 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); +void sentry__retry_dump_queue( + sentry_retry_t *retry, sentry_task_exec_func_t task_func); + void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index d9cfae9ca..bc1e7a782 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -311,8 +311,11 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; int rv = sentry__bgworker_shutdown(bgworker, remaining); - if (rv != 0 && state->shutdown_client) { - state->shutdown_client(state->client); + if (rv != 0) { + sentry__retry_dump_queue(state->retry, http_send_task); + if (state->shutdown_client) { + state->shutdown_client(state->client); + } } return rv; } From d8319877d054e43522b223221badef8fb71b3909 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:43:59 +0100 Subject: [PATCH 033/121] test(retry): update expectations for stop-on-failure behavior Co-Authored-By: Claude Opus 4.6 --- tests/test_integration_http.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index dc451ae92..d5af9b69b 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1090,11 +1090,11 @@ def test_http_retry_multiple_network_error(cmake): env=env, ) - # all envelopes retried, all bumped to retry 1 + # first envelope retried and bumped, rest untouched (stop on failure) retry_files = list(retry_dir.glob("*.envelope")) assert len(retry_files) == 10 - retry_1 = [f for f in retry_files if "-01-" in f.name] - assert len(retry_1) == 10 + assert len([f for f in retry_files if "-00-" in f.name]) == 9 + assert len([f for f in retry_files if "-01-" in f.name]) == 1 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") From 69f4f687b2c797c5aa7f73eac3e3bc3f20bc2d3a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:52:03 +0100 Subject: [PATCH 034/121] style(retry): fix line length in unit tests Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index ee3f89547..ebaea5161 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -120,7 +120,8 @@ SENTRY_TEST(retry_throttle) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + uint64_t old_ts + = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); @@ -156,7 +157,8 @@ SENTRY_TEST(retry_result) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + uint64_t old_ts + = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes from retry dir @@ -266,7 +268,8 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + uint64_t old_ts + = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry, old_ts, 4, &event_id); From 5562feb24839266202fc65c7d09db1138a687afe Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 16:58:35 +0100 Subject: [PATCH 035/121] fix(retry): prevent duplicate envelope writes from detached worker After shutdown timeout, the bgworker thread is detached but may still be executing an http_send_task. Since dump_queue already saves that task's envelope to the retry dir, the worker's subsequent call to retry_enqueue would create a duplicate file. Seal the retry module after dumping so that any late enqueue calls are silently skipped. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index dbc6ded05..a74968d6e 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -15,6 +15,7 @@ struct sentry_retry_s { sentry_path_t *cache_dir; int max_retries; uint64_t startup_time; + volatile long sealed; sentry_bgworker_t *bgworker; sentry_retry_send_func_t send_cb; void *send_data; @@ -43,6 +44,7 @@ sentry__retry_new(const sentry_options_t *options) retry->cache_dir = cache_dir; retry->max_retries = options->http_retries; retry->startup_time = sentry__usec_time() / 1000; + retry->sealed = 0; sentry__path_create_dir_all(retry->retry_dir); if (retry->cache_dir) { sentry__path_create_dir_all(retry->cache_dir); @@ -314,12 +316,17 @@ sentry__retry_dump_queue( if (retry) { sentry__bgworker_foreach_matching( retry->bgworker, task_func, retry_dump_cb, retry); + // prevent duplicate writes from a still-running detached worker + sentry__atomic_store(&retry->sealed, 1); } } void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { + if (sentry__atomic_fetch(&retry->sealed)) { + return; + } sentry__retry_write_envelope(retry, envelope); // prevent the startup poll from re-processing this session's envelope retry->startup_time = 0; From 0664fb3cada8def9f7a289f5cbcd473dc478452f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 17:45:36 +0100 Subject: [PATCH 036/121] docs: add changelog entry for HTTP retry feature Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 6 +++++- include/sentry.h | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad01ff29..98a94eb86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog -## Unreleased: +## Unreleased + +**Features**: + +- Add HTTP retry with exponential backoff: `sentry_options_set_http_retries()`. ([#1520](https://github.com/getsentry/sentry-native/pull/1520)) **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index 23f202d7c..aede8a588 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -2259,7 +2259,7 @@ SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( const sentry_options_t *opts); /** - * Sets the maximum number of HTTP retry attempts for transient network errors. + * Sets the maximum number of HTTP retry attempts for network failures. * Set to 0 to disable retries (default). */ SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retries( From c241adf549b7d5e5e454272c7cf9fa1d4a39d365 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 19:04:46 +0100 Subject: [PATCH 037/121] test(retry): use sentry__retry_send instead of duplicated eligibility logic Remove count_eligible_files helper that duplicated filtering logic from sentry__retry_send. The retry_backoff test now exercises the actual send path for both backoff and startup modes. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 42 ++++++++++------------------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index ebaea5161..a3cff011d 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -45,33 +45,6 @@ find_envelope_attempt(const sentry_path_t *dir) return -1; } -static int -count_eligible_files(const sentry_path_t *dir, uint64_t before) -{ - int eligible = 0; - uint64_t now = before > 0 ? 0 : sentry__usec_time() / 1000; - sentry_pathiter_t *iter = sentry__path_iter_directory(dir); - const sentry_path_t *file; - while (iter && (file = sentry__pathiter_next(iter)) != NULL) { - const char *name = sentry__path_filename(file); - uint64_t ts; - int count; - const char *uuid; - if (!sentry__retry_parse_filename(name, &ts, &count, &uuid)) { - continue; - } - if (before > 0 && ts >= before) { - continue; - } - if (!before && (now - ts) < sentry__retry_backoff(count)) { - continue; - } - eligible++; - } - sentry__pathiter_free(iter); - return eligible; -} - static void write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, const sentry_uuid_t *event_id) @@ -340,12 +313,17 @@ SENTRY_TEST(retry_backoff) sentry_uuid_t id4 = sentry_uuid_new_v4(); write_retry_file(retry, ref + 8 * base, 2, &id4); - // Startup scan (no backoff check): all 4 files - TEST_CHECK_INT_EQUAL( - count_eligible_files(retry_path, sentry__usec_time() / 1000), 4); + // With backoff: only eligible ones (id1 and id3) are sent + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 2); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 2); - // With backoff check: only eligible ones (id1 and id3) - TEST_CHECK_INT_EQUAL(count_eligible_files(retry_path, 0), 2); + // Startup scan (no backoff check): remaining 2 files are sent + ctx = (retry_test_ctx_t) { 200, 0 }; + sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 2); + TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); // Verify backoff calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); From a053ccb46803acb8215ccb604a1c124bd5201b16 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Feb 2026 19:28:49 +0100 Subject: [PATCH 038/121] fix(retry): raise backoff cap from 2h to 8h to match crashpad Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 6 ++++-- tests/unit/test_retry.c | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index a74968d6e..8b92e9244 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -92,11 +92,13 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, return true; } +/** + * Exponential backoff: 15m, 30m, 1h, 2h, 4h, 8h, 8h, ... (capped at 8 hours) + */ uint64_t sentry__retry_backoff(int count) { - int shift = count < 3 ? count : 3; - return (uint64_t)SENTRY_RETRY_INTERVAL << shift; + return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(count, 5); } static int diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index a3cff011d..fe6721c8a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -241,8 +241,7 @@ SENTRY_TEST(retry_cache) sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); - uint64_t old_ts - = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry, old_ts, 4, &event_id); @@ -330,7 +329,9 @@ SENTRY_TEST(retry_backoff) TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(1), base * 2); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(2), base * 4); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(3), base * 8); - TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 8); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 16); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(5), base * 32); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(6), base * 32); sentry__retry_free(retry); sentry__path_free(retry_path); From 71ee17a1611641d51b3a7e0dff70679138890796 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 09:55:45 +0100 Subject: [PATCH 039/121] refactor(retry): introduce retry_item_t to avoid re-parsing filenames Store parsed fields (ts, count, uuid) alongside the path during the filter phase so handle_result and future debug logging can use them without re-parsing. Also improves sort performance by comparing numeric fields before falling back to string comparison. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 86 +++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 8b92e9244..df6d78612 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -101,12 +101,25 @@ sentry__retry_backoff(int count) return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(count, 5); } +typedef struct { + sentry_path_t *path; + uint64_t ts; + int count; + char uuid[37]; +} retry_item_t; + static int -compare_retry_paths(const void *a, const void *b) +compare_retry_items(const void *a, const void *b) { - const sentry_path_t *const *pa = a; - const sentry_path_t *const *pb = b; - return strcmp(sentry__path_filename(*pa), sentry__path_filename(*pb)); + const retry_item_t *ia = a; + const retry_item_t *ib = b; + if (ia->ts != ib->ts) { + return ia->ts < ib->ts ? -1 : 1; + } + if (ia->count != ib->count) { + return ia->count - ib->count; + } + return strcmp(ia->uuid, ib->uuid); } sentry_path_t * @@ -140,40 +153,31 @@ sentry__retry_write_envelope( } static bool -handle_result(sentry_retry_t *retry, const sentry_path_t *path, int status_code) +handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { - const char *fname = sentry__path_filename(path); - uint64_t ts; - int count; - const char *uuid; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { - sentry__path_remove(path); - return false; - } - - if (status_code < 0 && count + 1 < retry->max_retries) { + if (status_code < 0 && item->count + 1 < retry->max_retries) { sentry_path_t *new_path = sentry__retry_make_path( - retry, sentry__usec_time() / 1000, count + 1, uuid); + retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { - sentry__path_rename(path, new_path); + sentry__path_rename(item->path, new_path); sentry__path_free(new_path); } return true; } - if (count + 1 >= retry->max_retries && retry->cache_dir) { + if (item->count + 1 >= retry->max_retries && retry->cache_dir) { char cache_name[46]; - snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid); + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); sentry_path_t *dst = sentry__path_join_str(retry->cache_dir, cache_name); if (dst) { - sentry__path_rename(path, dst); + sentry__path_rename(item->path, dst); sentry__path_free(dst); } else { - sentry__path_remove(path); + sentry__path_remove(item->path); } } else { - sentry__path_remove(path); + sentry__path_remove(item->path); } return false; } @@ -187,9 +191,9 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, return 0; } - size_t path_cap = 16; - sentry_path_t **paths = sentry_malloc(path_cap * sizeof(sentry_path_t *)); - if (!paths) { + size_t item_cap = 16; + retry_item_t *items = sentry_malloc(item_cap * sizeof(retry_item_t)); + if (!items) { sentry__pathiter_free(piter); return 0; } @@ -214,33 +218,37 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, if (!before && now >= ts && (now - ts) < sentry__retry_backoff(count)) { continue; } - if (eligible == path_cap) { - path_cap *= 2; - sentry_path_t **tmp - = sentry_malloc(path_cap * sizeof(sentry_path_t *)); + if (eligible == item_cap) { + item_cap *= 2; + retry_item_t *tmp = sentry_malloc(item_cap * sizeof(retry_item_t)); if (!tmp) { break; } - memcpy(tmp, paths, eligible * sizeof(sentry_path_t *)); - sentry_free(paths); - paths = tmp; + memcpy(tmp, items, eligible * sizeof(retry_item_t)); + sentry_free(items); + items = tmp; } - paths[eligible++] = sentry__path_clone(p); + retry_item_t *item = &items[eligible++]; + item->path = sentry__path_clone(p); + item->ts = ts; + item->count = count; + memcpy(item->uuid, uuid, 36); + item->uuid[36] = '\0'; } sentry__pathiter_free(piter); if (eligible > 1) { - qsort(paths, eligible, sizeof(sentry_path_t *), compare_retry_paths); + qsort(items, eligible, sizeof(retry_item_t), compare_retry_items); } for (size_t i = 0; i < eligible; i++) { - sentry_envelope_t *envelope = sentry__envelope_from_path(paths[i]); + sentry_envelope_t *envelope = sentry__envelope_from_path(items[i].path); if (!envelope) { - sentry__path_remove(paths[i]); + sentry__path_remove(items[i].path); } else { int status_code = send_cb(envelope, data); sentry_envelope_free(envelope); - if (!handle_result(retry, paths[i], status_code)) { + if (!handle_result(retry, &items[i], status_code)) { total--; } // stop on network failure to avoid wasting time on a dead @@ -252,9 +260,9 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, } for (size_t i = 0; i < eligible; i++) { - sentry__path_free(paths[i]); + sentry__path_free(items[i].path); } - sentry_free(paths); + sentry_free(items); return total; } From dfe7c1f3986086114cc79d7a9c624e262f251bb0 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 10:04:33 +0100 Subject: [PATCH 040/121] feat(retry): add debug and warning output for HTTP retries Log retry attempts at DEBUG level and max-retries-reached at WARN level to make retry behavior observable. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index df6d78612..82ccf0380 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -1,6 +1,7 @@ #include "sentry_retry.h" #include "sentry_alloc.h" #include "sentry_envelope.h" +#include "sentry_logger.h" #include "sentry_options.h" #include "sentry_utils.h" @@ -147,7 +148,10 @@ sentry__retry_write_envelope( sentry_path_t *path = sentry__retry_make_path(retry, sentry__usec_time() / 1000, 0, uuid); if (path) { - (void)sentry_envelope_write_to_path(envelope, path); + if (sentry_envelope_write_to_path(envelope, path) != 0) { + SENTRY_WARNF( + "failed to write retry envelope to \"%s\"", path->path); + } sentry__path_free(path); } } @@ -155,7 +159,9 @@ sentry__retry_write_envelope( static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { - if (status_code < 0 && item->count + 1 < retry->max_retries) { + bool exhausted = item->count + 1 >= retry->max_retries; + + if (status_code < 0 && !exhausted) { sentry_path_t *new_path = sentry__retry_make_path( retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { @@ -165,7 +171,9 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) return true; } - if (item->count + 1 >= retry->max_retries && retry->cache_dir) { + if (exhausted && retry->cache_dir) { + SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", + retry->max_retries); char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); sentry_path_t *dst @@ -177,6 +185,10 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) sentry__path_remove(item->path); } } else { + if (exhausted) { + SENTRY_WARNF("max retries (%d) reached, discarding envelope", + retry->max_retries); + } sentry__path_remove(item->path); } return false; @@ -246,6 +258,8 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, if (!envelope) { sentry__path_remove(items[i].path); } else { + SENTRY_DEBUGF("retrying envelope (%d/%d)", items[i].count + 1, + retry->max_retries); int status_code = send_cb(envelope, data); sentry_envelope_free(envelope); if (!handle_result(retry, &items[i], status_code)) { From 5d80da6bc89c55195259b2392b0e64640455186f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 11:08:31 +0100 Subject: [PATCH 041/121] refactor(cache): add cache_path to sentry_run_t and centralize cache writes Three places independently constructed /cache and wrote envelopes there. Add cache_path to sentry_run_t and introduce sentry__run_write_cache() and sentry__run_move_cache() to centralize the cache directory creation and file operations. Co-Authored-By: Claude Opus 4.6 --- src/backends/sentry_backend_crashpad.cpp | 8 +-- src/sentry_database.c | 87 ++++++++++++++++-------- src/sentry_database.h | 16 +++++ src/sentry_retry.c | 25 ++----- tests/unit/test_retry.c | 9 ++- 5 files changed, 92 insertions(+), 53 deletions(-) diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 9815bdb42..ba7420734 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -565,11 +565,9 @@ process_completed_reports( SENTRY_DEBUGF("caching %zu completed reports", reports.size()); - sentry_path_t *cache_dir - = sentry__path_join_str(options->database_path, "cache"); - if (!cache_dir || sentry__path_create_dir_all(cache_dir) != 0) { + sentry_path_t *cache_dir = options->run->cache_path; + if (sentry__path_create_dir_all(cache_dir) != 0) { SENTRY_WARN("failed to create cache dir"); - sentry__path_free(cache_dir); return; } @@ -593,8 +591,6 @@ process_completed_reports( sentry__path_free(out_path); sentry_envelope_free(envelope); } - - sentry__path_free(cache_dir); } static int diff --git a/src/sentry_database.c b/src/sentry_database.c index 0315c1484..1d55278bd 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -51,12 +51,23 @@ sentry__run_new(const sentry_path_t *database_path) return NULL; } + // `/cache` + sentry_path_t *cache_path = sentry__path_join_str(database_path, "cache"); + if (!cache_path) { + sentry__path_free(run_path); + sentry__path_free(lock_path); + sentry__path_free(session_path); + sentry__path_free(external_path); + return NULL; + } + sentry_run_t *run = SENTRY_MAKE(sentry_run_t); if (!run) { sentry__path_free(run_path); sentry__path_free(session_path); sentry__path_free(lock_path); sentry__path_free(external_path); + sentry__path_free(cache_path); return NULL; } @@ -64,6 +75,7 @@ sentry__run_new(const sentry_path_t *database_path) run->run_path = run_path; run->session_path = session_path; run->external_path = external_path; + run->cache_path = cache_path; run->lock = sentry__filelock_new(lock_path); if (!run->lock) { goto error; @@ -97,12 +109,13 @@ sentry__run_free(sentry_run_t *run) sentry__path_free(run->run_path); sentry__path_free(run->session_path); sentry__path_free(run->external_path); + sentry__path_free(run->cache_path); sentry__filelock_free(run->lock); sentry_free(run); } static bool -write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) +write_envelope(const sentry_path_t *dir, const sentry_envelope_t *envelope) { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); @@ -112,24 +125,23 @@ write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) event_id = sentry_uuid_new_v4(); } - char *envelope_filename = sentry__uuid_as_filename(&event_id, ".envelope"); - if (!envelope_filename) { + char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); + if (!filename) { return false; } - sentry_path_t *output_path = sentry__path_join_str(path, envelope_filename); - sentry_free(envelope_filename); - if (!output_path) { + sentry_path_t *path = sentry__path_join_str(dir, filename); + sentry_free(filename); + if (!path) { return false; } - int rv = sentry_envelope_write_to_path(envelope, output_path); - sentry__path_free(output_path); + int rv = sentry_envelope_write_to_path(envelope, path); + sentry__path_free(path); if (rv) { SENTRY_WARN("writing envelope to file failed"); return false; } - return true; } @@ -148,10 +160,45 @@ sentry__run_write_external( SENTRY_ERRORF("mkdir failed: \"%s\"", run->external_path->path); return false; } - return write_envelope(run->external_path, envelope); } +bool +sentry__run_write_cache( + const sentry_run_t *run, const sentry_envelope_t *envelope) +{ + if (sentry__path_create_dir_all(run->cache_path) != 0) { + SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); + return false; + } + return write_envelope(run->cache_path, envelope); +} + +bool +sentry__run_move_cache( + const sentry_run_t *run, const sentry_path_t *src, const char *dst) +{ + if (sentry__path_create_dir_all(run->cache_path) != 0) { + SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); + return false; + } + + const char *filename = dst ? dst : sentry__path_filename(src); + sentry_path_t *dst_path = sentry__path_join_str(run->cache_path, filename); + if (!dst_path) { + return false; + } + + int rv = sentry__path_rename(src, dst_path); + sentry__path_free(dst_path); + if (rv != 0) { + SENTRY_WARNF( + "failed to cache envelope \"%s\"", sentry__path_filename(src)); + return false; + } + return true; +} + bool sentry__run_write_session( const sentry_run_t *run, const sentry_session_t *session) @@ -244,14 +291,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) && (options->http_retries == 0 || !sentry__transport_can_retry(options->transport)); - sentry_path_t *cache_dir = NULL; - if (can_cache) { - cache_dir = sentry__path_join_str(options->database_path, "cache"); - if (cache_dir) { - sentry__path_create_dir_all(cache_dir); - } - } - sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) { @@ -297,15 +336,8 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - if (cache_dir) { - sentry_path_t *cached_file = sentry__path_join_str( - cache_dir, sentry__path_filename(file)); - if (!cached_file - || sentry__path_rename(file, cached_file) != 0) { - SENTRY_WARNF("failed to cache envelope \"%s\"", - sentry__path_filename(file)); - } - sentry__path_free(cached_file); + if (can_cache + && sentry__run_move_cache(options->run, file, NULL)) { continue; } } @@ -314,7 +346,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } sentry__pathiter_free(run_iter); - sentry__path_free(cache_dir); sentry__path_remove_all(run_dir); sentry__filelock_free(lock); } diff --git a/src/sentry_database.h b/src/sentry_database.h index c3fee8bc0..f92fff8f8 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -11,6 +11,7 @@ typedef struct sentry_run_s { sentry_path_t *run_path; sentry_path_t *session_path; sentry_path_t *external_path; + sentry_path_t *cache_path; sentry_filelock_t *lock; } sentry_run_t; @@ -63,6 +64,21 @@ bool sentry__run_write_session( */ bool sentry__run_clear_session(const sentry_run_t *run); +/** + * This will serialize and write the given envelope to disk into a file named + * like so: + * `/cache/.envelope` + */ +bool sentry__run_write_cache( + const sentry_run_t *run, const sentry_envelope_t *envelope); + +/** + * Moves `src` to `/cache/`. If `dst` is NULL, the filename of + * `src` is used. + */ +bool sentry__run_move_cache( + const sentry_run_t *run, const sentry_path_t *src, const char *dst); + /** * This function is essential to send crash reports from previous runs of the * program. diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 82ccf0380..b04a128ec 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -1,5 +1,6 @@ #include "sentry_retry.h" #include "sentry_alloc.h" +#include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_logger.h" #include "sentry_options.h" @@ -13,8 +14,9 @@ struct sentry_retry_s { sentry_path_t *retry_dir; - sentry_path_t *cache_dir; + const sentry_run_t *run; int max_retries; + bool cache_keep; uint64_t startup_time; volatile long sealed; sentry_bgworker_t *bgworker; @@ -30,26 +32,19 @@ sentry__retry_new(const sentry_options_t *options) if (!retry_dir) { return NULL; } - sentry_path_t *cache_dir = NULL; - if (options->cache_keep) { - cache_dir = sentry__path_join_str(options->database_path, "cache"); - } sentry_retry_t *retry = SENTRY_MAKE(sentry_retry_t); if (!retry) { - sentry__path_free(cache_dir); sentry__path_free(retry_dir); return NULL; } retry->retry_dir = retry_dir; - retry->cache_dir = cache_dir; + retry->run = options->run; retry->max_retries = options->http_retries; + retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; retry->sealed = 0; sentry__path_create_dir_all(retry->retry_dir); - if (retry->cache_dir) { - sentry__path_create_dir_all(retry->cache_dir); - } return retry; } @@ -60,7 +55,6 @@ sentry__retry_free(sentry_retry_t *retry) return; } sentry__path_free(retry->retry_dir); - sentry__path_free(retry->cache_dir); sentry_free(retry); } @@ -171,17 +165,12 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) return true; } - if (exhausted && retry->cache_dir) { + if (exhausted && retry->cache_keep && retry->run) { SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", retry->max_retries); char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); - sentry_path_t *dst - = sentry__path_join_str(retry->cache_dir, cache_name); - if (dst) { - sentry__path_rename(item->path, dst); - sentry__path_free(dst); - } else { + if (!sentry__run_move_cache(retry->run, item->path, cache_name)) { sentry__path_remove(item->path); } } else { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index fe6721c8a..6abcfa729 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -1,4 +1,6 @@ +#include "sentry_database.h" #include "sentry_envelope.h" +#include "sentry_options.h" #include "sentry_path.h" #include "sentry_retry.h" #include "sentry_session.h" @@ -228,14 +230,18 @@ SENTRY_TEST(retry_cache) sentry_path_t *db_path = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache"); sentry__path_remove_all(db_path); + sentry__path_create_dir_all(db_path); + + sentry_run_t *run = sentry__run_new(db_path); + TEST_ASSERT(!!run); SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_database_path( options, SENTRY_TEST_PATH_PREFIX ".retry-cache"); sentry_options_set_http_retries(options, 5); sentry_options_set_cache_keep(options, 1); + options->run = run; sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); @@ -273,6 +279,7 @@ SENTRY_TEST(retry_cache) sentry__retry_free(retry); sentry__path_free(retry_path); sentry__path_free(cache_path); + sentry_options_free(options); sentry__path_remove_all(db_path); sentry__path_free(db_path); } From 06cca6691e03f0ee4047649dc0d1e5384b5721bd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:00:36 +0100 Subject: [PATCH 042/121] fix(transport): use connect-only timeouts for curl and winhttp CURLOPT_TIMEOUT_MS is a total transfer timeout that could cut off large envelopes. Use CURLOPT_CONNECTTIMEOUT_MS instead so only connection establishment is bounded. For winhttp, limit resolve and connect to 15s but leave send/receive at their defaults. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_curl.c | 2 +- src/transports/sentry_http_transport_winhttp.c | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 969f86925..eec48f8e4 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -189,7 +189,7 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); - curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, 15000L); + curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 15000L); char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index e3f003a18..fa18ac8c3 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -134,7 +134,8 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 1; } - WinHttpSetTimeouts(client->session, 15000, 15000, 15000, 15000); + // 15s resolve, 15s connect, default send/receive + WinHttpSetTimeouts(client->session, 15000, 15000, 0, 0); return 0; } From fd6c5779d2c30f9e3f91fd01fa15f2a39211cbd6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:03:26 +0100 Subject: [PATCH 043/121] fix(retry): decrement total count when removing corrupt envelope files Without this, sentry__retry_send overcounts remaining files, causing an unnecessary extra poll cycle. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index b04a128ec..f8615e27f 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -246,6 +246,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_envelope_t *envelope = sentry__envelope_from_path(items[i].path); if (!envelope) { sentry__path_remove(items[i].path); + total--; } else { SENTRY_DEBUGF("retrying envelope (%d/%d)", items[i].count + 1, retry->max_retries); From 173de803f2d97b23e66c886dffb5dc5efee79450 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:25:39 +0100 Subject: [PATCH 044/121] fix(retry): only warn about exhausted retries on network failure Restructure handle_result so "max retries reached" warnings only fire on actual network failures, not on successful delivery at the last attempt. Separate the warning logic from the cache/discard actions and put the re-enqueue branch first for clarity. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index f8615e27f..07b54e8dd 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -153,9 +153,8 @@ sentry__retry_write_envelope( static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { - bool exhausted = item->count + 1 >= retry->max_retries; - - if (status_code < 0 && !exhausted) { + // network failure with retries remaining: bump count & re-enqueue + if (item->count + 1 < retry->max_retries && status_code < 0) { sentry_path_t *new_path = sentry__retry_make_path( retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { @@ -165,21 +164,30 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) return true; } - if (exhausted && retry->cache_keep && retry->run) { - SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", - retry->max_retries); + bool exhausted = item->count + 1 >= retry->max_retries; + + // network failure with retries exhausted + if (exhausted && status_code < 0) { + if (retry->cache_keep) { + SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", + retry->max_retries); + } else { + SENTRY_WARNF("max retries (%d) reached, discarding envelope", + retry->max_retries); + } + } + + // cache on last attempt + if (exhausted && retry->cache_keep) { char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); if (!sentry__run_move_cache(retry->run, item->path, cache_name)) { sentry__path_remove(item->path); } - } else { - if (exhausted) { - SENTRY_WARNF("max retries (%d) reached, discarding envelope", - retry->max_retries); - } - sentry__path_remove(item->path); + return false; } + + sentry__path_remove(item->path); return false; } From 0482bf40f5074ecff82ee266da6c21803ae922dd Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 12:53:01 +0100 Subject: [PATCH 045/121] docs(retry): add doc comments to sentry_retry.h declarations Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 --- src/sentry_retry.h | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 07b54e8dd..4d8e696ca 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -87,9 +87,6 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, return true; } -/** - * Exponential backoff: 15m, 30m, 1h, 2h, 4h, 8h, 8h, ... (capped at 8 hours) - */ uint64_t sentry__retry_backoff(int count) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index bd3b00ce4..12191e09e 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -13,23 +13,46 @@ typedef int (*sentry_retry_send_func_t)( sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); +/** + * Schedules retry polling on `bgworker` using `send_cb`. + */ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data); +/** + * Flushes unprocessed previous-session retries. No-op if already polled. + */ void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); +/** + * Dumps queued envelopes to the retry dir and seals against further writes. + */ void sentry__retry_dump_queue( sentry_retry_t *retry, sentry_task_exec_func_t task_func); +/** + * Writes a failed envelope to the retry dir and schedules a delayed poll. + */ void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); +/** + * Writes an event envelope to the retry dir. Non-event envelopes are skipped. + */ void sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); +/** + * Sends eligible retry files via `send_cb`. `before > 0`: send files with + * ts < before (startup). `before == 0`: use backoff. Returns remaining file + * count for controlling polling. + */ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_retry_send_func_t send_cb, void *data); +/** + * Exponential backoff: 15m, 30m, 1h, 2h, 4h, 8h, 8h, ... (capped at 8h). + */ uint64_t sentry__retry_backoff(int count); /** From 39f36ec50ce7518d5d65cdc5a2636bf124fdf1b4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 13:54:06 +0100 Subject: [PATCH 046/121] feat(transport): add sentry_transport_retry() Replace the `can_retry` bool on the transport with a `retry_func` callback, and expose `sentry_transport_retry()` as an experimental public API for explicitly retrying all pending envelopes, e.g. when coming back online. Co-Authored-By: Claude Opus 4.6 --- include/sentry.h | 8 ++++++ src/sentry_retry.c | 17 ++++++++++++ src/sentry_retry.h | 5 ++++ src/sentry_transport.c | 17 +++++++++--- src/sentry_transport.h | 4 +-- src/transports/sentry_http_transport.c | 12 +++++++- tests/unit/test_retry.c | 38 ++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 8 files changed, 95 insertions(+), 7 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index aede8a588..5e7e8427b 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -946,6 +946,14 @@ SENTRY_API void sentry_transport_set_shutdown_func( sentry_transport_t *transport, int (*shutdown_func)(uint64_t timeout, void *state)); +/** + * Retries sending all pending envelopes in the transport's retry queue, + * e.g. when coming back online. Only applicable for HTTP transports with + * retries enabled via `sentry_options_set_http_retries`. + */ +SENTRY_EXPERIMENTAL_API void sentry_transport_retry( + sentry_transport_t *transport); + /** * Generic way to free transport. */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 4d8e696ca..e66c47d40 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -340,6 +340,23 @@ sentry__retry_dump_queue( } } +static void +retry_trigger_task(void *_retry, void *_state) +{ + (void)_state; + sentry_retry_t *retry = _retry; + if (sentry__retry_send( + retry, UINT64_MAX, retry->send_cb, retry->send_data)) { + sentry__retry_trigger(retry); + } +} + +void +sentry__retry_trigger(sentry_retry_t *retry) +{ + sentry__bgworker_submit(retry->bgworker, retry_trigger_task, NULL, retry); +} + void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 12191e09e..609e6dd0e 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -67,4 +67,9 @@ sentry_path_t *sentry__retry_make_path( bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out); +/** + * Submits a delayed retry poll task on the background worker. + */ +void sentry__retry_trigger(sentry_retry_t *retry); + #endif diff --git a/src/sentry_transport.c b/src/sentry_transport.c index 744570928..1b81cb652 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -10,9 +10,9 @@ struct sentry_transport_s { int (*flush_func)(uint64_t timeout, void *state); void (*free_func)(void *state); size_t (*dump_func)(sentry_run_t *run, void *state); + void (*retry_func)(void *state); void *state; bool running; - bool can_retry; }; sentry_transport_t * @@ -150,13 +150,22 @@ sentry__transport_get_state(sentry_transport_t *transport) } void -sentry__transport_set_can_retry(sentry_transport_t *transport, bool can_retry) +sentry_transport_retry(sentry_transport_t *transport) { - transport->can_retry = can_retry; + if (transport && transport->retry_func) { + transport->retry_func(transport->state); + } +} + +void +sentry__transport_set_retry_func( + sentry_transport_t *transport, void (*retry_func)(void *state)) +{ + transport->retry_func = retry_func; } bool sentry__transport_can_retry(sentry_transport_t *transport) { - return transport && transport->can_retry; + return transport && transport->retry_func; } diff --git a/src/sentry_transport.h b/src/sentry_transport.h index ebb901ac6..5ed1e7b81 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -57,8 +57,8 @@ size_t sentry__transport_dump_queue( void *sentry__transport_get_state(sentry_transport_t *transport); -void sentry__transport_set_can_retry( - sentry_transport_t *transport, bool can_retry); +void sentry__transport_set_retry_func( + sentry_transport_t *transport, void (*retry_func)(void *state)); bool sentry__transport_can_retry(sentry_transport_t *transport); #endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index bc1e7a782..04249a109 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -351,6 +351,16 @@ http_transport_get_state(sentry_transport_t *transport) return sentry__bgworker_get_state(bgworker); } +static void +http_transport_retry(void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + http_transport_state_t *state = sentry__bgworker_get_state(bgworker); + if (state->retry) { + sentry__retry_trigger(state->retry); + } +} + sentry_transport_t * sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) { @@ -384,7 +394,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) sentry_transport_set_flush_func(transport, http_transport_flush); sentry_transport_set_shutdown_func(transport, http_transport_shutdown); sentry__transport_set_dump_func(transport, http_dump_queue); - sentry__transport_set_can_retry(transport, true); + sentry__transport_set_retry_func(transport, http_transport_retry); return transport; } diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 6abcfa729..738fa994d 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -5,6 +5,7 @@ #include "sentry_retry.h" #include "sentry_session.h" #include "sentry_testsupport.h" +#include "sentry_transport.h" #include "sentry_utils.h" #include "sentry_uuid.h" @@ -284,6 +285,43 @@ SENTRY_TEST(retry_cache) sentry__path_free(db_path); } +static int retry_func_calls = 0; + +static void +mock_retry_func(void *state) +{ + (void)state; + retry_func_calls++; +} + +static void +noop_send(sentry_envelope_t *envelope, void *state) +{ + (void)state; + sentry_envelope_free(envelope); +} + +SENTRY_TEST(transport_retry) +{ + // no retry_func → no-op + sentry_transport_t *transport = sentry_transport_new(noop_send); + TEST_CHECK(!sentry__transport_can_retry(transport)); + sentry_transport_retry(transport); + + // with retry_func → calls it + retry_func_calls = 0; + sentry__transport_set_retry_func(transport, mock_retry_func); + TEST_CHECK(sentry__transport_can_retry(transport)); + sentry_transport_retry(transport); + TEST_CHECK_INT_EQUAL(retry_func_calls, 1); + + // NULL transport → no-op + sentry_transport_retry(NULL); + TEST_CHECK_INT_EQUAL(retry_func_calls, 1); + + sentry_transport_free(transport); +} + SENTRY_TEST(retry_backoff) { sentry_path_t *db_path diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 537510365..9619585da 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -245,6 +245,7 @@ XX(traceparent_header_disabled_by_default) XX(traceparent_header_generation) XX(transaction_name_backfill_on_finish) XX(transactions_skip_before_send) +XX(transport_retry) XX(transport_sampling_transactions) XX(transport_sampling_transactions_set_trace) XX(txn_data) From 7b306885ff04fea9f5e2a0e41da9e39f1afe0289 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 15 Feb 2026 17:37:37 +0100 Subject: [PATCH 047/121] refactor(retry): store retry envelopes in cache/ directory Move retry envelopes from a separate retry/ directory into cache/ so that sentry__cleanup_cache() enforces disk limits for both file formats out of the box. The two formats are distinguishable by length: retry files use --.envelope (49+ chars) while cache files use .envelope (45 chars). Default http_retries to 0 (opt-in). Co-Authored-By: Claude Opus 4.6 --- src/sentry_core.c | 3 +- src/sentry_options.h | 2 +- src/sentry_retry.c | 23 ++--- src/sentry_retry.h | 2 +- tests/test_integration_cache.py | 54 +++++++++++ tests/test_integration_http.py | 101 +++++++++----------- tests/unit/test_cache.c | 64 +++++++++++++ tests/unit/test_retry.c | 160 +++++++++++++------------------- tests/unit/tests.inc | 1 + 9 files changed, 245 insertions(+), 165 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index 3b4b38bfb..3522f935a 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -292,7 +292,8 @@ sentry_init(sentry_options_t *options) backend->prune_database_func(backend); } - if (options->cache_keep) { + if (options->cache_keep + || sentry__transport_can_retry(options->transport)) { sentry__cleanup_cache(options); } diff --git a/src/sentry_options.h b/src/sentry_options.h index 1c9a96f45..77209314b 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -72,13 +72,13 @@ struct sentry_options_s { void *traces_sampler_data; size_t max_spans; bool enable_logs; - int http_retries; // takes the first varg as a `sentry_value_t` object containing attributes // if no custom attributes are to be passed, use `sentry_value_new_object()` bool logs_with_attributes; bool enable_metrics; sentry_before_send_metric_function_t before_send_metric_func; void *before_send_metric_data; + int http_retries; /* everything from here on down are options which are stored here but not exposed through the options API */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index e66c47d40..6eda36524 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -13,7 +13,6 @@ #define SENTRY_RETRY_THROTTLE 100 struct sentry_retry_s { - sentry_path_t *retry_dir; const sentry_run_t *run; int max_retries; bool cache_keep; @@ -27,24 +26,16 @@ struct sentry_retry_s { sentry_retry_t * sentry__retry_new(const sentry_options_t *options) { - sentry_path_t *retry_dir - = sentry__path_join_str(options->database_path, "retry"); - if (!retry_dir) { - return NULL; - } - sentry_retry_t *retry = SENTRY_MAKE(sentry_retry_t); if (!retry) { - sentry__path_free(retry_dir); return NULL; } - retry->retry_dir = retry_dir; retry->run = options->run; retry->max_retries = options->http_retries; retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; retry->sealed = 0; - sentry__path_create_dir_all(retry->retry_dir); + sentry__path_create_dir_all(options->run->cache_path); return retry; } @@ -54,7 +45,6 @@ sentry__retry_free(sentry_retry_t *retry) if (!retry) { return; } - sentry__path_free(retry->retry_dir); sentry_free(retry); } @@ -62,6 +52,12 @@ bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, int *count_out, const char **uuid_out) { + // Minimum retry filename: --.envelope (49+ chars). + // Cache filenames are exactly 45 chars (.envelope). + if (strlen(filename) <= 45) { + return false; + } + char *end; uint64_t ts = strtoull(filename, &end, 10); if (*end != '-') { @@ -121,7 +117,7 @@ sentry__retry_make_path( char filename[128]; snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, count, uuid); - return sentry__path_join_str(retry->retry_dir, filename); + return sentry__path_join_str(retry->run->cache_path, filename); } void @@ -192,7 +188,8 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_retry_send_func_t send_cb, void *data) { - sentry_pathiter_t *piter = sentry__path_iter_directory(retry->retry_dir); + sentry_pathiter_t *piter + = sentry__path_iter_directory(retry->run->cache_path); if (!piter) { return 0; } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 609e6dd0e..12008259e 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -56,7 +56,7 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, uint64_t sentry__retry_backoff(int count); /** - * /retry/--.envelope + * /cache/--.envelope */ sentry_path_t *sentry__retry_make_path( sentry_retry_t *retry, uint64_t ts, int count, const char *uuid); diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index aff10fa9f..354d12c11 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -179,3 +179,57 @@ def test_cache_max_items(cmake, backend): assert cache_dir.exists() cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 5 + + +@pytest.mark.parametrize( + "backend", + [ + "inproc", + pytest.param( + "breakpad", + marks=pytest.mark.skipif( + not has_breakpad, reason="breakpad backend not available" + ), + ), + ], +) +def test_cache_max_items_with_retry(cmake, backend): + tmp_path = cmake( + ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} + ) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + # Create cache files via crash+restart cycles + for i in range(4): + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "crash"], + expect_failure=True, + ) + + # Move envelopes into cache + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "no-setup"], + ) + + # Pre-populate cache/ with retry-format envelope files + cache_dir.mkdir(parents=True, exist_ok=True) + for i in range(4): + ts = int(time.time() * 1000) + f = cache_dir / f"{ts}-00-00000000-0000-0000-0000-{i:012x}.envelope" + f.write_text("dummy envelope content") + + # Trigger sentry_init which runs cleanup + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "http-retry", "no-setup"], + ) + + # max 5 items total in cache/ + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) <= 5 diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index d5af9b69b..509964593 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -843,7 +843,7 @@ def test_native_crash_http(cmake, httpserver): @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_on_network_error(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") # unreachable port triggers CURLE_COULDNT_CONNECT unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" @@ -856,10 +856,10 @@ def test_http_retry_on_network_error(cmake, httpserver): env=env_unreachable, ) - assert retry_dir.exists() - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 1 - assert "-00-" in str(retry_files[0].name) + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-00-" in str(cache_files[0].name) # retry on next run with working server env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) @@ -878,53 +878,48 @@ def test_http_retry_on_network_error(cmake, httpserver): envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) assert_meta(envelope, integration="inproc") - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_multiple_attempts(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) run(tmp_path, "sentry_example", ["log", "http-retry", "capture-event"], env=env) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 1 - assert "-00-" in str(retry_files[0].name) + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-00-" in str(cache_files[0].name) run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 1 - assert "-01-" in str(retry_files[0].name) + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-01-" in str(cache_files[0].name) run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 1 - assert "-02-" in str(retry_files[0].name) + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-02-" in str(cache_files[0].name) # exhaust remaining retries (max 5) for i in range(3): run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) # discarded after max retries (cache_keep not enabled) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 0 - - cache_dir = tmp_path.joinpath(".sentry-native/cache") - cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_with_cache_keep(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" @@ -937,8 +932,8 @@ def test_http_retry_with_cache_keep(cmake, httpserver): env=env_unreachable, ) - assert retry_dir.exists() - assert len(list(retry_dir.glob("*.envelope"))) == 1 + assert cache_dir.exists() + assert len(list(cache_dir.glob("*.envelope"))) == 1 env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") @@ -952,15 +947,12 @@ def test_http_retry_with_cache_keep(cmake, httpserver): ) assert waiting.result - assert len(list(retry_dir.glob("*.envelope"))) == 0 - cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] - assert len(cache_files) == 0 + assert len(list(cache_dir.glob("*.envelope"))) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_cache_keep_max_attempts(cmake): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" @@ -973,8 +965,8 @@ def test_http_retry_cache_keep_max_attempts(cmake): env=env, ) - assert retry_dir.exists() - assert len(list(retry_dir.glob("*.envelope"))) == 1 + assert cache_dir.exists() + assert len(list(cache_dir.glob("*.envelope"))) == 1 for _ in range(5): run( @@ -984,7 +976,6 @@ def test_http_retry_cache_keep_max_attempts(cmake): env=env, ) - assert len(list(retry_dir.glob("*.envelope"))) == 0 assert cache_dir.exists() assert len(list(cache_dir.glob("*.envelope"))) == 1 @@ -992,7 +983,7 @@ def test_http_retry_cache_keep_max_attempts(cmake): @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_http_error_discards_envelope(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( @@ -1004,14 +995,14 @@ def test_http_retry_http_error_discards_envelope(cmake, httpserver): assert waiting.result # HTTP errors discard, not retry - retry_files = list(retry_dir.glob("*.envelope")) if retry_dir.exists() else [] - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_rate_limit_discards_envelope(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data( @@ -1023,14 +1014,14 @@ def test_http_retry_rate_limit_discards_envelope(cmake, httpserver): assert waiting.result # 429 discards, not retry - retry_files = list(retry_dir.glob("*.envelope")) if retry_dir.exists() else [] - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) if cache_dir.exists() else [] + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_multiple_success(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) @@ -1042,8 +1033,8 @@ def test_http_retry_multiple_success(cmake, httpserver): env=env_unreachable, ) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 10 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 10 env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) for _ in range(10): @@ -1061,14 +1052,14 @@ def test_http_retry_multiple_success(cmake, httpserver): assert waiting.result assert len(httpserver.log) == 10 - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 0 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_multiple_network_error(cmake): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) @@ -1080,8 +1071,8 @@ def test_http_retry_multiple_network_error(cmake): env=env, ) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 10 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 10 run( tmp_path, @@ -1091,16 +1082,16 @@ def test_http_retry_multiple_network_error(cmake): ) # first envelope retried and bumped, rest untouched (stop on failure) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 10 - assert len([f for f in retry_files if "-00-" in f.name]) == 9 - assert len([f for f in retry_files if "-01-" in f.name]) == 1 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 10 + assert len([f for f in cache_files if "-00-" in f.name]) == 9 + assert len([f for f in cache_files if "-01-" in f.name]) == 1 @pytest.mark.skipif(not has_files, reason="test needs a local filesystem") def test_http_retry_multiple_rate_limit(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) - retry_dir = tmp_path.joinpath(".sentry-native/retry") + cache_dir = tmp_path.joinpath(".sentry-native/cache") unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) @@ -1112,8 +1103,8 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): env=env_unreachable, ) - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 10 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 10 # rate limit response followed by discards for the rest (rate limiter # kicks in after the first 429) @@ -1130,5 +1121,5 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): ) # first envelope gets 429, rest are discarded by rate limiter - retry_files = list(retry_dir.glob("*.envelope")) - assert len(retry_files) == 0 + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 0 diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index e161340bc..abfd73d78 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -46,6 +46,7 @@ SENTRY_TEST(cache_keep) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); + sentry_options_set_http_retries(options, 0); sentry_init(options); sentry_path_t *cache_path @@ -243,6 +244,69 @@ SENTRY_TEST(cache_max_items) sentry_close(); } +SENTRY_TEST(cache_max_items_with_retry) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#endif + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_cache_keep(options, true); + sentry_options_set_cache_max_items(options, 7); + sentry_init(options); + + sentry_path_t *cache_path + = sentry__path_join_str(options->database_path, "cache"); + TEST_ASSERT(!!cache_path); + TEST_ASSERT(sentry__path_remove_all(cache_path) == 0); + TEST_ASSERT(sentry__path_create_dir_all(cache_path) == 0); + + time_t now = time(NULL); + + // 5 cache-format files: 1,3,5,7,9 min old + for (int i = 0; i < 5; i++) { + sentry_uuid_t event_id = sentry_uuid_new_v4(); + char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); + TEST_ASSERT(!!filename); + sentry_path_t *filepath = sentry__path_join_str(cache_path, filename); + sentry_free(filename); + + TEST_ASSERT(sentry__path_touch(filepath) == 0); + TEST_ASSERT(set_file_mtime(filepath, now - ((i * 2 + 1) * 60)) == 0); + sentry__path_free(filepath); + } + + // 5 retry-format files: 0,2,4,6,8 min old + for (int i = 0; i < 5; i++) { + sentry_uuid_t event_id = sentry_uuid_new_v4(); + char uuid[37]; + sentry_uuid_as_string(&event_id, uuid); + char filename[128]; + snprintf(filename, sizeof(filename), "%" PRIu64 "-00-%.36s.envelope", + (uint64_t)now, uuid); + sentry_path_t *filepath = sentry__path_join_str(cache_path, filename); + + TEST_ASSERT(sentry__path_touch(filepath) == 0); + TEST_ASSERT(set_file_mtime(filepath, now - (i * 2 * 60)) == 0); + sentry__path_free(filepath); + } + + sentry__cleanup_cache(options); + + int total_count = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(cache_path); + const sentry_path_t *entry; + while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { + total_count++; + } + sentry__pathiter_free(iter); + + TEST_CHECK_INT_EQUAL(total_count, 7); + + sentry__path_free(cache_path); + sentry_close(); +} + SENTRY_TEST(cache_max_size_and_age) { #if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 738fa994d..05479ee6a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -82,19 +82,16 @@ test_send_cb(sentry_envelope_t *envelope, void *_ctx) SENTRY_TEST(retry_throttle) { - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-throttle"); - sentry__path_remove_all(db_path); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-throttle"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_http_retries(options, 5); + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry__path_remove_all(options->run->cache_path); + sentry__path_create_dir_all(options->run->cache_path); uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); @@ -104,108 +101,95 @@ SENTRY_TEST(retry_throttle) write_retry_file(retry, old_ts, 0, &ids[i]); } - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 4); + TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 4); retry_test_ctx_t ctx = { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 4); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 0); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); + sentry_close(); } SENTRY_TEST(retry_result) { - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-result"); - sentry__path_remove_all(db_path); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-result"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_http_retries(options, 2); + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); - // 1. Success (200) → removes from retry dir + // 1. Success (200) → removes write_retry_file(retry, old_ts, 0, &event_id); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 0); retry_test_ctx_t ctx = { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 2. Rate limited (429) → removes write_retry_file(retry, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { 429, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 3. Discard (0) → removes write_retry_file(retry, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { 0, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 4. Network error → bumps count write_retry_file(retry, old_ts, 0, &event_id); - TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 0); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 0); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - TEST_CHECK_INT_EQUAL(find_envelope_attempt(retry_path), 1); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 1); // 5. Network error at max count → exceeds max_retries=2, removed - sentry__path_remove_all(retry_path); - sentry__path_create_dir_all(retry_path); + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); write_retry_file(retry, old_ts, 1, &event_id); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); + sentry_close(); } SENTRY_TEST(retry_session) { - SENTRY_TEST_OPTIONS_NEW(init_options); - sentry_options_set_dsn(init_options, "https://foo@sentry.invalid/42"); - sentry_options_set_release(init_options, "test@1.0.0"); - sentry_init(init_options); - - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-session"); - sentry__path_remove_all(db_path); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-session"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_release(options, "test@1.0.0"); sentry_options_set_http_retries(options, 2); + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + sentry__path_remove_all(options->run->cache_path); + sentry__path_create_dir_all(options->run->cache_path); sentry_session_t *session = sentry__session_new(); TEST_ASSERT(!!session); @@ -215,74 +199,66 @@ SENTRY_TEST(retry_session) // Session-only envelopes have no event_id → should not be written sentry__retry_write_envelope(retry, envelope); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 0); sentry_envelope_free(envelope); sentry__session_free(session); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); sentry_close(); } SENTRY_TEST(retry_cache) { - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-cache"); - sentry__path_remove_all(db_path); - sentry__path_create_dir_all(db_path); - - sentry_run_t *run = sentry__run_new(db_path); - TEST_ASSERT(!!run); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-cache"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_http_retries(options, 5); sentry_options_set_cache_keep(options, 1); - options->run = run; + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); - sentry_path_t *cache_path = sentry__path_join_str(db_path, "cache"); + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); sentry_uuid_t event_id = sentry_uuid_new_v4(); write_retry_file(retry, old_ts, 4, &event_id); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); - TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); + char uuid_str[37]; + sentry_uuid_as_string(&event_id, uuid_str); + char cache_name[46]; + snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", uuid_str); + sentry_path_t *cached = sentry__path_join_str(cache_path, cache_name); + + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK(!sentry__path_is_file(cached)); - // Network error on a file at count=4 with max_retries=5 → moves to cache + // Network error on a file at count=4 with max_retries=5 → renames to + // cache format (.envelope) retry_test_ctx_t ctx = { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK(sentry__path_is_file(cached)); - // Success on a file at count=4 → also moves to cache (cache_keep - // preserves all envelopes regardless of send outcome) + // Success on a file at count=4 → also renames to cache format + // (cache_keep preserves all envelopes regardless of send outcome) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); write_retry_file(retry, old_ts, 4, &event_id); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 1); + TEST_CHECK(!sentry__path_is_file(cached)); ctx = (retry_test_ctx_t) { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); - - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); + TEST_CHECK(sentry__path_is_file(cached)); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_free(cache_path); - sentry_options_free(options); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); + sentry__path_free(cached); + sentry_close(); } static int retry_func_calls = 0; @@ -324,19 +300,17 @@ SENTRY_TEST(transport_retry) SENTRY_TEST(retry_backoff) { - sentry_path_t *db_path - = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".retry-backoff"); - sentry__path_remove_all(db_path); - SENTRY_TEST_OPTIONS_NEW(options); - sentry_options_set_database_path( - options, SENTRY_TEST_PATH_PREFIX ".retry-backoff"); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_http_retries(options, 5); + sentry_init(options); + sentry_retry_t *retry = sentry__retry_new(options); - sentry_options_free(options); TEST_ASSERT(!!retry); - sentry_path_t *retry_path = sentry__path_join_str(db_path, "retry"); + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); uint64_t base = sentry__retry_backoff(0); uint64_t ref = sentry__usec_time() / 1000 - 10 * base; @@ -361,13 +335,13 @@ SENTRY_TEST(retry_backoff) retry_test_ctx_t ctx = { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 2); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 2); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 2); // Startup scan (no backoff check): remaining 2 files are sent ctx = (retry_test_ctx_t) { 200, 0 }; sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 2); - TEST_CHECK_INT_EQUAL(count_envelope_files(retry_path), 0); + TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // Verify backoff calculation TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(0), base); @@ -379,7 +353,5 @@ SENTRY_TEST(retry_backoff) TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(6), base * 32); sentry__retry_free(retry); - sentry__path_free(retry_path); - sentry__path_remove_all(db_path); - sentry__path_free(db_path); + sentry_close(); } diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 9619585da..710e02c87 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -43,6 +43,7 @@ XX(build_id_parser) XX(cache_keep) XX(cache_max_age) XX(cache_max_items) +XX(cache_max_items_with_retry) XX(cache_max_size) XX(cache_max_size_and_age) XX(capture_minidump_basic) From e6c8db4e10438646384ab49128b1af6f878b06b1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 09:51:52 +0100 Subject: [PATCH 048/121] fix(retry): own cache_path to prevent use-after-free on detached thread When bgworker is detached during shutdown timeout, retry_poll_task can access retry->run->cache_path after sentry_options_free frees the run. Clone the path so it outlives options and is freed with the bgworker. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6eda36524..60a01094c 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -13,7 +13,7 @@ #define SENTRY_RETRY_THROTTLE 100 struct sentry_retry_s { - const sentry_run_t *run; + sentry_path_t *cache_path; int max_retries; bool cache_keep; uint64_t startup_time; @@ -30,7 +30,7 @@ sentry__retry_new(const sentry_options_t *options) if (!retry) { return NULL; } - retry->run = options->run; + retry->cache_path = sentry__path_clone(options->run->cache_path); retry->max_retries = options->http_retries; retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; @@ -45,6 +45,7 @@ sentry__retry_free(sentry_retry_t *retry) if (!retry) { return; } + sentry__path_free(retry->cache_path); sentry_free(retry); } @@ -117,7 +118,7 @@ sentry__retry_make_path( char filename[128]; snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, count, uuid); - return sentry__path_join_str(retry->run->cache_path, filename); + return sentry__path_join_str(retry->cache_path, filename); } void @@ -174,9 +175,12 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) if (exhausted && retry->cache_keep) { char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); - if (!sentry__run_move_cache(retry->run, item->path, cache_name)) { + sentry_path_t *dest + = sentry__path_join_str(retry->cache_path, cache_name); + if (!dest || sentry__path_rename(item->path, dest) != 0) { sentry__path_remove(item->path); } + sentry__path_free(dest); return false; } @@ -188,8 +192,7 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_retry_send_func_t send_cb, void *data) { - sentry_pathiter_t *piter - = sentry__path_iter_directory(retry->run->cache_path); + sentry_pathiter_t *piter = sentry__path_iter_directory(retry->cache_path); if (!piter) { return 0; } From c5205751186d1f93533baa3639479e4178f31f9d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 09:33:34 +0100 Subject: [PATCH 049/121] fix(retry): don't consume shutdown timeout with bgworker flush The bgworker_flush in sentry__retry_flush would delay its flush_task by min(delayed_task_time, timeout) when a 15-minute delayed retry_poll_task existed. This consumed the entire shutdown timeout, leaving 0ms for bgworker_shutdown, which then detached the worker thread. On Windows, winhttp_client_shutdown would close handles still in use by the detached thread, causing a crash. The flush is unnecessary because retry_flush_task is an immediate task and bgworker_shutdown already processes all immediate tasks before the shutdown_task runs. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 +-- src/sentry_retry.h | 2 +- src/transports/sentry_http_transport.c | 7 ++----- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 60a01094c..7123328c5 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -312,11 +312,10 @@ retry_flush_task(void *_retry, void *_state) } void -sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) +sentry__retry_flush(sentry_retry_t *retry) { if (retry) { sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); - sentry__bgworker_flush(retry->bgworker, timeout); } } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 12008259e..fc5ef7123 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -22,7 +22,7 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, /** * Flushes unprocessed previous-session retries. No-op if already polled. */ -void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); +void sentry__retry_flush(sentry_retry_t *retry); /** * Dumps queued envelopes to the retry dir and seals against further writes. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 04249a109..24710542e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -305,12 +305,9 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - uint64_t started = sentry__monotonic_time(); - sentry__retry_flush(state->retry, timeout); - uint64_t elapsed = sentry__monotonic_time() - started; - uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; + sentry__retry_flush(state->retry); - int rv = sentry__bgworker_shutdown(bgworker, remaining); + int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); if (state->shutdown_client) { From 79d17321cca53d92558d0aebc8b1a9545637284d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 10:24:17 +0100 Subject: [PATCH 050/121] fix(retry): flush in-flight retries before shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit removed bgworker_flush from retry_flush, which caused a race between WinHTTP connect timeout (~2s) and bgworker shutdown (2s). Restore the flush and pass the full timeout to both flush and shutdown — after flush drains in-flight work, shutdown completes near-instantly. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 ++- src/sentry_retry.h | 2 +- src/transports/sentry_http_transport.c | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 7123328c5..60a01094c 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -312,10 +312,11 @@ retry_flush_task(void *_retry, void *_state) } void -sentry__retry_flush(sentry_retry_t *retry) +sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) { if (retry) { sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); + sentry__bgworker_flush(retry->bgworker, timeout); } } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index fc5ef7123..12008259e 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -22,7 +22,7 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, /** * Flushes unprocessed previous-session retries. No-op if already polled. */ -void sentry__retry_flush(sentry_retry_t *retry); +void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); /** * Dumps queued envelopes to the retry dir and seals against further writes. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 24710542e..9827c2d3b 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -305,7 +305,8 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - sentry__retry_flush(state->retry); + // flush drains in-flight retries; shutdown is near-instant afterward + sentry__retry_flush(state->retry, timeout); int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0) { From 344dcc896d35281b8afead5b1d1c018cf11d87ba Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 13:33:53 +0100 Subject: [PATCH 051/121] refactor(retry): replace http_retries count with boolean http_retry Make retry count an internal constant (SENTRY_RETRY_ATTEMPTS = 5) and expose only a boolean toggle. Enabled by default. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 2 +- examples/example.c | 4 ++-- include/sentry.h | 13 +++++------ src/sentry_database.c | 2 +- src/sentry_options.c | 9 ++++---- src/sentry_options.h | 2 +- src/sentry_retry.c | 13 +++++------ src/transports/sentry_http_transport.c | 2 +- tests/test_integration_cache.py | 2 +- tests/test_integration_http.py | 32 +++++++++++++------------- tests/unit/test_cache.c | 2 +- tests/unit/test_retry.c | 16 +++++++------ 12 files changed, 50 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a94eb86..e23b571e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ **Features**: -- Add HTTP retry with exponential backoff: `sentry_options_set_http_retries()`. ([#1520](https://github.com/getsentry/sentry-native/pull/1520)) +- Add HTTP retry with exponential backoff. ([#1520](https://github.com/getsentry/sentry-native/pull/1520)) **Fixes**: diff --git a/examples/example.c b/examples/example.c index 5c2a0a85b..652bdc29f 100644 --- a/examples/example.c +++ b/examples/example.c @@ -659,8 +659,8 @@ main(int argc, char **argv) sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); // 5 days sentry_options_set_cache_max_items(options, 5); } - if (has_arg(argc, argv, "http-retry")) { - sentry_options_set_http_retries(options, 5); + if (has_arg(argc, argv, "no-http-retry")) { + sentry_options_set_http_retry(options, false); } if (has_arg(argc, argv, "enable-metrics")) { diff --git a/include/sentry.h b/include/sentry.h index 5e7e8427b..ea86bd3b4 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -948,8 +948,7 @@ SENTRY_API void sentry_transport_set_shutdown_func( /** * Retries sending all pending envelopes in the transport's retry queue, - * e.g. when coming back online. Only applicable for HTTP transports with - * retries enabled via `sentry_options_set_http_retries`. + * e.g. when coming back online. Only applicable for HTTP transports. */ SENTRY_EXPERIMENTAL_API void sentry_transport_retry( sentry_transport_t *transport); @@ -2267,12 +2266,12 @@ SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( const sentry_options_t *opts); /** - * Sets the maximum number of HTTP retry attempts for network failures. - * Set to 0 to disable retries (default). + * Enables or disables HTTP retry with exponential backoff for network failures. + * Enabled by default. */ -SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retries( - sentry_options_t *opts, int http_retries); -SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retries( +SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retry( + sentry_options_t *opts, int enabled); +SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retry( const sentry_options_t *opts); /** diff --git a/src/sentry_database.c b/src/sentry_database.c index 1d55278bd..34a7d9926 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -288,7 +288,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } bool can_cache = options->cache_keep - && (options->http_retries == 0 + && (!options->http_retry || !sentry__transport_can_retry(options->transport)); sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); diff --git a/src/sentry_options.c b/src/sentry_options.c index d8376f0d5..916b180d6 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -82,6 +82,7 @@ sentry_options_new(void) opts->crash_reporting_mode = SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP; // Default: best of // both worlds + opts->http_retry = true; return opts; } @@ -877,15 +878,15 @@ sentry_options_set_handler_strategy( #endif // SENTRY_PLATFORM_LINUX void -sentry_options_set_http_retries(sentry_options_t *opts, int http_retries) +sentry_options_set_http_retry(sentry_options_t *opts, int enabled) { - opts->http_retries = http_retries; + opts->http_retry = enabled; } int -sentry_options_get_http_retries(const sentry_options_t *opts) +sentry_options_get_http_retry(const sentry_options_t *opts) { - return opts->http_retries; + return opts->http_retry; } void diff --git a/src/sentry_options.h b/src/sentry_options.h index 77209314b..064c0d1fa 100644 --- a/src/sentry_options.h +++ b/src/sentry_options.h @@ -78,7 +78,7 @@ struct sentry_options_s { bool enable_metrics; sentry_before_send_metric_function_t before_send_metric_func; void *before_send_metric_data; - int http_retries; + bool http_retry; /* everything from here on down are options which are stored here but not exposed through the options API */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 60a01094c..e82d66c9f 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -9,12 +9,12 @@ #include #include +#define SENTRY_RETRY_ATTEMPTS 5 #define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) #define SENTRY_RETRY_THROTTLE 100 struct sentry_retry_s { sentry_path_t *cache_path; - int max_retries; bool cache_keep; uint64_t startup_time; volatile long sealed; @@ -31,7 +31,6 @@ sentry__retry_new(const sentry_options_t *options) return NULL; } retry->cache_path = sentry__path_clone(options->run->cache_path); - retry->max_retries = options->http_retries; retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; retry->sealed = 0; @@ -148,7 +147,7 @@ static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { // network failure with retries remaining: bump count & re-enqueue - if (item->count + 1 < retry->max_retries && status_code < 0) { + if (item->count + 1 < SENTRY_RETRY_ATTEMPTS && status_code < 0) { sentry_path_t *new_path = sentry__retry_make_path( retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { @@ -158,16 +157,16 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) return true; } - bool exhausted = item->count + 1 >= retry->max_retries; + bool exhausted = item->count + 1 >= SENTRY_RETRY_ATTEMPTS; // network failure with retries exhausted if (exhausted && status_code < 0) { if (retry->cache_keep) { SENTRY_WARNF("max retries (%d) reached, moving envelope to cache", - retry->max_retries); + SENTRY_RETRY_ATTEMPTS); } else { SENTRY_WARNF("max retries (%d) reached, discarding envelope", - retry->max_retries); + SENTRY_RETRY_ATTEMPTS); } } @@ -254,7 +253,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, total--; } else { SENTRY_DEBUGF("retrying envelope (%d/%d)", items[i].count + 1, - retry->max_retries); + SENTRY_RETRY_ATTEMPTS); int status_code = send_cb(envelope, data); sentry_envelope_free(envelope); if (!handle_result(retry, &items[i], status_code)) { diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 9827c2d3b..f527116ed 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -282,7 +282,7 @@ http_transport_start(const sentry_options_t *options, void *transport_state) return rv; } - if (options->http_retries > 0) { + if (options->http_retry) { state->retry = sentry__retry_new(options); if (state->retry) { sentry__retry_start(state->retry, bgworker, retry_send_cb, state); diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index 354d12c11..6ba9fd225 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -226,7 +226,7 @@ def test_cache_max_items_with_retry(cmake, backend): run( tmp_path, "sentry_example", - ["log", "cache-keep", "http-retry", "no-setup"], + ["log", "cache-keep", "no-setup"], ) # max 5 items total in cache/ diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 509964593..fb70524b5 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -852,7 +852,7 @@ def test_http_retry_on_network_error(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-event"], + ["log", "capture-event"], env=env_unreachable, ) @@ -869,7 +869,7 @@ def test_http_retry_on_network_error(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env_reachable, ) assert waiting.result @@ -890,19 +890,19 @@ def test_http_retry_multiple_attempts(cmake, httpserver): unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) - run(tmp_path, "sentry_example", ["log", "http-retry", "capture-event"], env=env) + run(tmp_path, "sentry_example", ["log", "capture-event"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-00-" in str(cache_files[0].name) - run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-01-" in str(cache_files[0].name) - run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 @@ -910,7 +910,7 @@ def test_http_retry_multiple_attempts(cmake, httpserver): # exhaust remaining retries (max 5) for i in range(3): - run(tmp_path, "sentry_example", ["log", "http-retry", "no-setup"], env=env) + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) # discarded after max retries (cache_keep not enabled) cache_files = list(cache_dir.glob("*.envelope")) @@ -928,7 +928,7 @@ def test_http_retry_with_cache_keep(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "cache-keep", "capture-event"], + ["log", "cache-keep", "capture-event"], env=env_unreachable, ) @@ -942,7 +942,7 @@ def test_http_retry_with_cache_keep(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "cache-keep", "no-setup"], + ["log", "cache-keep", "no-setup"], env=env_reachable, ) assert waiting.result @@ -961,7 +961,7 @@ def test_http_retry_cache_keep_max_attempts(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "cache-keep", "capture-event"], + ["log", "cache-keep", "capture-event"], env=env, ) @@ -972,7 +972,7 @@ def test_http_retry_cache_keep_max_attempts(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "cache-keep", "no-setup"], + ["log", "cache-keep", "no-setup"], env=env, ) @@ -1029,7 +1029,7 @@ def test_http_retry_multiple_success(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-multiple"], + ["log", "capture-multiple"], env=env_unreachable, ) @@ -1046,7 +1046,7 @@ def test_http_retry_multiple_success(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env_reachable, ) assert waiting.result @@ -1067,7 +1067,7 @@ def test_http_retry_multiple_network_error(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-multiple"], + ["log", "capture-multiple"], env=env, ) @@ -1077,7 +1077,7 @@ def test_http_retry_multiple_network_error(cmake): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env, ) @@ -1099,7 +1099,7 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "capture-multiple"], + ["log", "capture-multiple"], env=env_unreachable, ) @@ -1116,7 +1116,7 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): run( tmp_path, "sentry_example", - ["log", "http-retry", "no-setup"], + ["log", "no-setup"], env=env_reachable, ) diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index abfd73d78..f49637bc4 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -46,7 +46,7 @@ SENTRY_TEST(cache_keep) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); - sentry_options_set_http_retries(options, 0); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_path_t *cache_path diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 05479ee6a..764f33bcf 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -84,7 +84,7 @@ SENTRY_TEST(retry_throttle) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retries(options, 5); + sentry_options_set_http_retry(options, true); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -116,7 +116,7 @@ SENTRY_TEST(retry_result) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retries(options, 2); + sentry_options_set_http_retry(options, true); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -164,10 +164,12 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 1); - // 5. Network error at max count → exceeds max_retries=2, removed + // 5. Network error at last attempt → removed sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); - write_retry_file(retry, old_ts, 1, &event_id); + uint64_t very_old_ts + = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); + write_retry_file(retry, very_old_ts, 4, &event_id); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); @@ -182,7 +184,7 @@ SENTRY_TEST(retry_session) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_release(options, "test@1.0.0"); - sentry_options_set_http_retries(options, 2); + sentry_options_set_http_retry(options, true); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -211,7 +213,7 @@ SENTRY_TEST(retry_cache) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retries(options, 5); + sentry_options_set_http_retry(options, true); sentry_options_set_cache_keep(options, 1); sentry_init(options); @@ -302,7 +304,7 @@ SENTRY_TEST(retry_backoff) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retries(options, 5); + sentry_options_set_http_retry(options, true); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); From 5cdcc47615391c0fb01ef8b2b6d6024733d63fe3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 14:18:41 +0100 Subject: [PATCH 052/121] fix(transport): use explicit WinHTTP send/receive timeouts 0 means infinite, not default. Pass 30000ms to match WinHTTP defaults. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_winhttp.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index fa18ac8c3..e73de7405 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -134,8 +134,8 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 1; } - // 15s resolve, 15s connect, default send/receive - WinHttpSetTimeouts(client->session, 15000, 15000, 0, 0); + // 15s resolve/connect, 30s send/receive (WinHTTP defaults) + WinHttpSetTimeouts(client->session, 15000, 15000, 30000, 30000); return 0; } From 913650034a9ac68b13ae270fb189fa23f4bc681e Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 14:32:46 +0100 Subject: [PATCH 053/121] fix(retry): deduplicate poll tasks on concurrent envelope failures Use a 'scheduled' flag with atomic compare-and-swap to ensure at most one retry_poll_task is queued at a time. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index e82d66c9f..7040b51c7 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -18,6 +18,7 @@ struct sentry_retry_s { bool cache_keep; uint64_t startup_time; volatile long sealed; + volatile long scheduled; sentry_bgworker_t *bgworker; sentry_retry_send_func_t send_cb; void *send_data; @@ -283,6 +284,8 @@ retry_poll_task(void *_retry, void *_state) retry, retry->startup_time, retry->send_cb, retry->send_data)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); + } else { + sentry__atomic_store(&retry->scheduled, 0); } // subsequent polls use backoff instead of the startup time filter retry->startup_time = 0; @@ -295,6 +298,7 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, retry->bgworker = bgworker; retry->send_cb = send_cb; retry->send_data = send_data; + sentry__atomic_store(&retry->scheduled, 1); sentry__bgworker_submit_delayed( bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); } @@ -365,6 +369,8 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) sentry__retry_write_envelope(retry, envelope); // prevent the startup poll from re-processing this session's envelope retry->startup_time = 0; - sentry__bgworker_submit_delayed( - retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); + if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { + sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, + retry, SENTRY_RETRY_INTERVAL); + } } From 9b14dc2844b6cf80dde8a28107c7f477b2ae38f6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:13:38 +0100 Subject: [PATCH 054/121] fix(retry): set sealed flag before dumping queued envelopes Move `sealed = 1` before `foreach_matching` in `retry_dump_queue` to prevent the detached worker from writing duplicate envelopes via `retry_enqueue` while the main thread is dumping the queue. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 7040b51c7..e29f312ec 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -336,10 +336,10 @@ sentry__retry_dump_queue( sentry_retry_t *retry, sentry_task_exec_func_t task_func) { if (retry) { - sentry__bgworker_foreach_matching( - retry->bgworker, task_func, retry_dump_cb, retry); // prevent duplicate writes from a still-running detached worker sentry__atomic_store(&retry->sealed, 1); + sentry__bgworker_foreach_matching( + retry->bgworker, task_func, retry_dump_cb, retry); } } From e75f595d1ca50dcf8639918a975aff4bd4d56f49 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:27:38 +0100 Subject: [PATCH 055/121] fix(retry): prevent retry flush from consuming shutdown timeout Drop the delayed retry_poll_task before bgworker_flush to prevent it from delaying the flush_task by min(retry_interval, timeout). Subtract elapsed flush time from the shutdown timeout so the total is bounded. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 12 ++++++++++++ src/transports/sentry_http_transport.c | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index e29f312ec..3f3d6a1fa 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -314,10 +314,22 @@ retry_flush_task(void *_retry, void *_state) } } +static bool +drop_task_cb(void *_data, void *_ctx) +{ + (void)_data; + (void)_ctx; + return true; +} + void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) { if (retry) { + // drop the delayed poll that would stall bgworker_flush + sentry__bgworker_foreach_matching( + retry->bgworker, retry_poll_task, drop_task_cb, NULL); + sentry__atomic_store(&retry->scheduled, 0); sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); sentry__bgworker_flush(retry->bgworker, timeout); } diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index f527116ed..031a9be54 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -305,10 +305,12 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - // flush drains in-flight retries; shutdown is near-instant afterward + uint64_t started = sentry__monotonic_time(); sentry__retry_flush(state->retry, timeout); + uint64_t elapsed = sentry__monotonic_time() - started; + uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; - int rv = sentry__bgworker_shutdown(bgworker, timeout); + int rv = sentry__bgworker_shutdown(bgworker, remaining); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); if (state->shutdown_client) { From 136fabf52ea0052bd98334e2e91cc51cb532359b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 16:58:10 +0100 Subject: [PATCH 056/121] fix(retry): zero-initialize retry struct after malloc Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 3f3d6a1fa..6ffaf2c52 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -31,10 +31,10 @@ sentry__retry_new(const sentry_options_t *options) if (!retry) { return NULL; } + memset(retry, 0, sizeof(sentry_retry_t)); retry->cache_path = sentry__path_clone(options->run->cache_path); retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; - retry->sealed = 0; sentry__path_create_dir_all(options->run->cache_path); return retry; } From 1a0d99b7d50ee01e8fd4426a34ce3fa3b4a905e8 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 17:03:59 +0100 Subject: [PATCH 057/121] fix(retry): skip flush task after seal to prevent duplicate sends When the bgworker is detached after shutdown timeout, retry_dump_queue writes retry files and sets sealed=1. The detached thread could then run retry_flush_task and re-send those files, causing duplicates. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6ffaf2c52..46d04a706 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -308,7 +308,7 @@ retry_flush_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (retry->startup_time > 0) { + if (retry->startup_time > 0 && !sentry__atomic_fetch(&retry->sealed)) { sentry__retry_send(retry, UINT64_MAX, retry->send_cb, retry->send_data); retry->startup_time = 0; } From ed28b85ee03d9347968f442f6dad1f37809e2550 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Feb 2026 17:26:26 +0100 Subject: [PATCH 058/121] refactor(database): remove unused sentry__run_write_cache The retry system writes cache files directly via its own paths. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 11 ----------- src/sentry_database.h | 8 -------- 2 files changed, 19 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 34a7d9926..e11244c49 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -163,17 +163,6 @@ sentry__run_write_external( return write_envelope(run->external_path, envelope); } -bool -sentry__run_write_cache( - const sentry_run_t *run, const sentry_envelope_t *envelope) -{ - if (sentry__path_create_dir_all(run->cache_path) != 0) { - SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); - return false; - } - return write_envelope(run->cache_path, envelope); -} - bool sentry__run_move_cache( const sentry_run_t *run, const sentry_path_t *src, const char *dst) diff --git a/src/sentry_database.h b/src/sentry_database.h index f92fff8f8..fd44d598d 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -64,14 +64,6 @@ bool sentry__run_write_session( */ bool sentry__run_clear_session(const sentry_run_t *run); -/** - * This will serialize and write the given envelope to disk into a file named - * like so: - * `/cache/.envelope` - */ -bool sentry__run_write_cache( - const sentry_run_t *run, const sentry_envelope_t *envelope); - /** * Moves `src` to `/cache/`. If `dst` is NULL, the filename of * `src` is used. From 62999c39a6c0fff23abb78bf7b5a9e545a82e3ef Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 10:11:08 +0100 Subject: [PATCH 059/121] fix(retry): make trigger one-shot to prevent rapid retry exhaustion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit retry_trigger_task recursively re-triggered itself on network failure, bypassing exponential backoff (UINT64_MAX skips the backoff check) and burning through all 5 retry attempts in milliseconds. Since sentry__retry_send already processes all cached envelopes in a single call, the re-trigger is only ever reached on network failure — exactly the case where it's harmful. Make the trigger one-shot; failed items are left for the regular poll task which respects backoff. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 5 +---- tests/unit/test_retry.c | 41 +++++++++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 46d04a706..d6e4b8002 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -360,10 +360,7 @@ retry_trigger_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (sentry__retry_send( - retry, UINT64_MAX, retry->send_cb, retry->send_data)) { - sentry__retry_trigger(retry); - } + sentry__retry_send(retry, UINT64_MAX, retry->send_cb, retry->send_data); } void diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 764f33bcf..21792b13a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -357,3 +357,44 @@ SENTRY_TEST(retry_backoff) sentry__retry_free(retry); sentry_close(); } + +SENTRY_TEST(retry_trigger) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_http_retry(options, true); + sentry_init(options); + + sentry_retry_t *retry = sentry__retry_new(options); + TEST_ASSERT(!!retry); + + const sentry_path_t *cache_path = options->run->cache_path; + sentry__path_remove_all(cache_path); + sentry__path_create_dir_all(cache_path); + + uint64_t old_ts + = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(retry, old_ts, 0, &event_id); + + // UINT64_MAX (trigger mode) bypasses backoff: bumps count + retry_test_ctx_t ctx = { -1, 0 }; + sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 1); + + // second call: bumps again because UINT64_MAX skips backoff + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, UINT64_MAX, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 1); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 2); + + // before=0 (poll mode) respects backoff: item is skipped + ctx = (retry_test_ctx_t) { -1, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + TEST_CHECK_INT_EQUAL(ctx.count, 0); + TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 2); + + sentry__retry_free(retry); + sentry_close(); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 710e02c87..473391251 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -197,6 +197,7 @@ XX(retry_cache) XX(retry_result) XX(retry_session) XX(retry_throttle) +XX(retry_trigger) XX(ringbuffer_append) XX(ringbuffer_append_invalid_decref_value) XX(ringbuffer_append_null_decref_value) From 70b1040f87f0a28f269902abf975e6074b8509de Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 10:14:49 +0100 Subject: [PATCH 060/121] fix(core): check http_retry option instead of transport capability cleanup_cache was gated on sentry__transport_can_retry, which checks for retry_func. Since retry_func is unconditionally set for all HTTP transports, this ran cleanup_cache even with http_retry disabled. Check the option directly instead. Co-Authored-By: Claude Opus 4.6 --- src/sentry_core.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sentry_core.c b/src/sentry_core.c index 3522f935a..14dba4a2d 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -292,8 +292,7 @@ sentry_init(sentry_options_t *options) backend->prune_database_func(backend); } - if (options->cache_keep - || sentry__transport_can_retry(options->transport)) { + if (options->cache_keep || options->http_retry) { sentry__cleanup_cache(options); } From 794df4be97d9fde3ae5906542791a185411c4e27 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 11:04:45 +0100 Subject: [PATCH 061/121] fix(retry): prevent UB from negative count in backoff shift Reject negative counts in parse_filename (a corrupted filename like 123--01-.envelope parses count=-1 via strtol). Also clamp the count in sentry__retry_backoff to prevent left-shift by a negative amount. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 4 ++-- tests/unit/test_retry.c | 34 ++++++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d6e4b8002..d33a5ab73 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -67,7 +67,7 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, const char *count_str = end + 1; long count = strtol(count_str, &end, 10); - if (*end != '-') { + if (*end != '-' || count < 0) { return false; } @@ -87,7 +87,7 @@ sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, uint64_t sentry__retry_backoff(int count) { - return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(count, 5); + return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(MAX(count, 0), 5); } typedef struct { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 21792b13a..5b9b9dc22 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -80,6 +80,39 @@ test_send_cb(sentry_envelope_t *envelope, void *_ctx) return ctx->status_code; } +SENTRY_TEST(retry_filename) +{ + uint64_t ts; + int count; + const char *uuid; + + TEST_CHECK(sentry__retry_parse_filename( + "1234567890-00-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, + &count, &uuid)); + TEST_CHECK_UINT64_EQUAL(ts, 1234567890); + TEST_CHECK_INT_EQUAL(count, 0); + TEST_CHECK(strncmp(uuid, "abcdefab-1234-5678-9abc-def012345678", 36) == 0); + + TEST_CHECK(sentry__retry_parse_filename( + "999-04-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, + &uuid)); + TEST_CHECK_UINT64_EQUAL(ts, 999); + TEST_CHECK_INT_EQUAL(count, 4); + + // negative count + TEST_CHECK(!sentry__retry_parse_filename( + "123--01-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, + &uuid)); + + // cache filename (no timestamp/count) + TEST_CHECK(!sentry__retry_parse_filename( + "abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); + + // missing .envelope suffix + TEST_CHECK(!sentry__retry_parse_filename( + "123-00-abcdefab-1234-5678-9abc-def012345678.txt", &ts, &count, &uuid)); +} + SENTRY_TEST(retry_throttle) { SENTRY_TEST_OPTIONS_NEW(options); @@ -353,6 +386,7 @@ SENTRY_TEST(retry_backoff) TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(4), base * 16); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(5), base * 32); TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(6), base * 32); + TEST_CHECK_UINT64_EQUAL(sentry__retry_backoff(-1), base); sentry__retry_free(retry); sentry_close(); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 473391251..d0a7c257e 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -194,6 +194,7 @@ XX(read_write_envelope_to_invalid_path) XX(recursive_paths) XX(retry_backoff) XX(retry_cache) +XX(retry_filename) XX(retry_result) XX(retry_session) XX(retry_throttle) From 83475e446b7d105ad841d5a4f36c03409cfdc42c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Feb 2026 11:48:55 +0100 Subject: [PATCH 062/121] fix(options): normalize http_retry with !! to match other boolean setters Co-Authored-By: Claude Opus 4.6 --- src/sentry_options.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_options.c b/src/sentry_options.c index 916b180d6..43e4ed467 100644 --- a/src/sentry_options.c +++ b/src/sentry_options.c @@ -880,7 +880,7 @@ sentry_options_set_handler_strategy( void sentry_options_set_http_retry(sentry_options_t *opts, int enabled) { - opts->http_retry = enabled; + opts->http_retry = !!enabled; } int From 8aae7469a6675fc826898677755165cbbcebed6f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 23 Feb 2026 15:34:15 +0100 Subject: [PATCH 063/121] revert(database): restore original variable names and whitespace in write_envelope Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index e11244c49..4bc8784fa 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -115,7 +115,7 @@ sentry__run_free(sentry_run_t *run) } static bool -write_envelope(const sentry_path_t *dir, const sentry_envelope_t *envelope) +write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); @@ -125,23 +125,24 @@ write_envelope(const sentry_path_t *dir, const sentry_envelope_t *envelope) event_id = sentry_uuid_new_v4(); } - char *filename = sentry__uuid_as_filename(&event_id, ".envelope"); - if (!filename) { + char *envelope_filename = sentry__uuid_as_filename(&event_id, ".envelope"); + if (!envelope_filename) { return false; } - sentry_path_t *path = sentry__path_join_str(dir, filename); - sentry_free(filename); - if (!path) { + sentry_path_t *output_path = sentry__path_join_str(path, envelope_filename); + sentry_free(envelope_filename); + if (!output_path) { return false; } - int rv = sentry_envelope_write_to_path(envelope, path); - sentry__path_free(path); + int rv = sentry_envelope_write_to_path(envelope, output_path); + sentry__path_free(output_path); if (rv) { SENTRY_WARN("writing envelope to file failed"); return false; } + return true; } @@ -160,6 +161,7 @@ sentry__run_write_external( SENTRY_ERRORF("mkdir failed: \"%s\"", run->external_path->path); return false; } + return write_envelope(run->external_path, envelope); } From 791d598766f5c7472a98571a3db4158bb4ceb8d7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 14:36:39 +0100 Subject: [PATCH 064/121] docs: clarify sentry_transport_retry behavior and limitations - Document the 5-attempt retry limit - Note there is no rate limiting between attempts - Warn about potential event loss during extended network outages --- include/sentry.h | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/include/sentry.h b/include/sentry.h index ea86bd3b4..ae5df82bb 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -949,6 +949,17 @@ SENTRY_API void sentry_transport_set_shutdown_func( /** * Retries sending all pending envelopes in the transport's retry queue, * e.g. when coming back online. Only applicable for HTTP transports. + * + * Note: The SDK automatically retries failed envelopes on next application + * startup. This function allows manual triggering of pending retries at + * runtime. Each envelope is retried up to 5 times. If all attempts are + * exhausted during intermittent connectivity, events will be discarded + * (or moved to cache if enabled via sentry_options_set_cache_keep). + * + * Warning: This function has no rate limiting - it will immediately + * attempt to send all pending envelopes. Calling this repeatedly during + * extended network outages may exhaust retry attempts that might have + * succeeded with the SDK's built-in exponential backoff. */ SENTRY_EXPERIMENTAL_API void sentry_transport_retry( sentry_transport_t *transport); From a2efe25633543408023bf6a056b84cafbd92fe07 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 14:42:23 +0100 Subject: [PATCH 065/121] docs(retry): document retry behavior for network failures vs HTTP responses Only network failures (negative status codes) trigger retries. HTTP responses including 5xx (500, 502, 503, 504) are discarded. --- src/sentry_retry.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d33a5ab73..4b832f787 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -147,6 +147,10 @@ sentry__retry_write_envelope( static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { + // Only network failures (status_code < 0) trigger retries. HTTP responses + // including 5xx (500, 502, 503, 504) are discarded: + // https://develop.sentry.dev/sdk/expected-features/#dealing-with-network-failures + // network failure with retries remaining: bump count & re-enqueue if (item->count + 1 < SENTRY_RETRY_ATTEMPTS && status_code < 0) { sentry_path_t *new_path = sentry__retry_make_path( From 4b1e7d7e1cd4f6b2aaf8944bb268375fe5aaa702 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 15:21:00 +0100 Subject: [PATCH 066/121] fix(retry): only clear startup_time when envelope is written - Change sentry__retry_write_envelope to return bool indicating success - Return false for nil event IDs (session envelopes) and write failures - sentry__retry_enqueue now returns early if write fails, preserving startup_time for session envelopes so retry_flush_task can flush them --- src/sentry_retry.c | 23 ++++++++++++++--------- src/sentry_retry.h | 3 ++- tests/unit/test_retry.c | 2 +- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 4b832f787..578306a9c 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -121,13 +121,13 @@ sentry__retry_make_path( return sentry__path_join_str(retry->cache_path, filename); } -void +bool sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope) { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); if (sentry_uuid_is_nil(&event_id)) { - return; + return false; } char uuid[37]; @@ -135,13 +135,16 @@ sentry__retry_write_envelope( sentry_path_t *path = sentry__retry_make_path(retry, sentry__usec_time() / 1000, 0, uuid); - if (path) { - if (sentry_envelope_write_to_path(envelope, path) != 0) { - SENTRY_WARNF( - "failed to write retry envelope to \"%s\"", path->path); - } - sentry__path_free(path); + if (!path) { + return false; + } + + int rv = sentry_envelope_write_to_path(envelope, path); + if (rv != 0) { + SENTRY_WARNF("failed to write retry envelope to \"%s\"", path->path); } + sentry__path_free(path); + return rv == 0; } static bool @@ -379,7 +382,9 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) if (sentry__atomic_fetch(&retry->sealed)) { return; } - sentry__retry_write_envelope(retry, envelope); + if (!sentry__retry_write_envelope(retry, envelope)) { + return; + } // prevent the startup poll from re-processing this session's envelope retry->startup_time = 0; if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 12008259e..50f39e9ac 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -38,8 +38,9 @@ void sentry__retry_enqueue( /** * Writes an event envelope to the retry dir. Non-event envelopes are skipped. + * Returns true if an envelope was written, false otherwise. */ -void sentry__retry_write_envelope( +bool sentry__retry_write_envelope( sentry_retry_t *retry, const sentry_envelope_t *envelope); /** diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 5b9b9dc22..9cdde2f6e 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -233,7 +233,7 @@ SENTRY_TEST(retry_session) sentry__envelope_add_session(envelope, session); // Session-only envelopes have no event_id → should not be written - sentry__retry_write_envelope(retry, envelope); + TEST_CHECK(!sentry__retry_write_envelope(retry, envelope)); TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 0); sentry_envelope_free(envelope); From 6064f1d120f52542ef918471b5ea49dbc534f9b7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 15:36:58 +0100 Subject: [PATCH 067/121] fix(retry): check for NULL from sentry__path_clone Add null check after cloning cache_path to prevent dereferencing null later in sentry__retry_send when iterating directory or joining paths. --- src/sentry_retry.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 578306a9c..c961e01cc 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -33,9 +33,13 @@ sentry__retry_new(const sentry_options_t *options) } memset(retry, 0, sizeof(sentry_retry_t)); retry->cache_path = sentry__path_clone(options->run->cache_path); + if (!retry->cache_path) { + sentry_free(retry); + return NULL; + } retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; - sentry__path_create_dir_all(options->run->cache_path); + sentry__path_create_dir_all(retry->cache_path); return retry; } From 45ee8860190b697dfea3ba59cd79a6701eb3c814 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 17:08:37 +0100 Subject: [PATCH 068/121] fix(retry): apply backoff when system clock moves backward When system clock moves backward (now < ts), the condition now >= ts was false, causing the backoff check to be skipped entirely. This made items immediately eligible for retry regardless of their count. Now checks if now < ts (clock skew) OR if backoff hasn't elapsed. --- src/sentry_retry.c | 3 ++- tests/unit/test_retry.c | 28 ++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index c961e01cc..67919daf9 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -232,7 +232,8 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, continue; } total++; - if (!before && now >= ts && (now - ts) < sentry__retry_backoff(count)) { + if (!before + && (now < ts || (now - ts) < sentry__retry_backoff(count))) { continue; } if (eligible == item_cap) { diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 9cdde2f6e..d0798087d 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -145,6 +145,34 @@ SENTRY_TEST(retry_throttle) sentry_close(); } +SENTRY_TEST(retry_skew) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_http_retry(options, true); + sentry_init(options); + + sentry_retry_t *retry = sentry__retry_new(options); + TEST_ASSERT(!!retry); + + sentry__path_remove_all(options->run->cache_path); + sentry__path_create_dir_all(options->run->cache_path); + + // future timestamp simulates clock moving backward + uint64_t future_ts = sentry__usec_time() / 1000 + 1000000; + sentry_uuid_t event_id = sentry_uuid_new_v4(); + write_retry_file(retry, future_ts, 0, &event_id); + + retry_test_ctx_t ctx = { 200, 0 }; + sentry__retry_send(retry, 0, test_send_cb, &ctx); + + // item should NOT be processed due to backoff (clock backward) + TEST_CHECK_INT_EQUAL(ctx.count, 0); + + sentry__retry_free(retry); + sentry_close(); +} + SENTRY_TEST(retry_result) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index d0a7c257e..2318ff8be 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -197,6 +197,7 @@ XX(retry_cache) XX(retry_filename) XX(retry_result) XX(retry_session) +XX(retry_skew) XX(retry_throttle) XX(retry_trigger) XX(ringbuffer_append) From ec59d5e53343693822a492aad6ffe3928e3191bb Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 18:12:46 +0100 Subject: [PATCH 069/121] fix(retry): increase SENTRY_RETRY_ATTEMPTS to 6 to match Crashpad Crashpad's kRetryAttempts=5 with `upload_attempts > kRetryAttempts` (checked before post-increment) allows upload_attempts 0-5, giving 6 retries with backoffs: 15m, 30m, 1h, 2h, 4h, 8h. sentry-native's `count + 1 < SENTRY_RETRY_ATTEMPTS` with the old value of 5 only allowed counts 0-3 to be re-enqueued, so the max backoff reached was 4h. Bumping to 6 gives the same 6 retries and the full backoff sequence up to 8h. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- tests/test_integration_http.py | 4 ++-- tests/unit/test_retry.c | 14 +++++++------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 67919daf9..911afb045 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -9,7 +9,7 @@ #include #include -#define SENTRY_RETRY_ATTEMPTS 5 +#define SENTRY_RETRY_ATTEMPTS 6 #define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) #define SENTRY_RETRY_THROTTLE 100 diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index fb70524b5..e2b2ceac8 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -908,8 +908,8 @@ def test_http_retry_multiple_attempts(cmake, httpserver): assert len(cache_files) == 1 assert "-02-" in str(cache_files[0].name) - # exhaust remaining retries (max 5) - for i in range(3): + # exhaust remaining retries (max 6) + for i in range(4): run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) # discarded after max retries (cache_keep not enabled) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index d0798087d..553179932 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -229,8 +229,8 @@ SENTRY_TEST(retry_result) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); uint64_t very_old_ts - = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); - write_retry_file(retry, very_old_ts, 4, &event_id); + = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); + write_retry_file(retry, very_old_ts, 5, &event_id); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); @@ -285,9 +285,9 @@ SENTRY_TEST(retry_cache) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); - uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(4); + uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry, old_ts, 4, &event_id); + write_retry_file(retry, old_ts, 5, &event_id); char uuid_str[37]; sentry_uuid_as_string(&event_id, uuid_str); @@ -298,7 +298,7 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); TEST_CHECK(!sentry__path_is_file(cached)); - // Network error on a file at count=4 with max_retries=5 → renames to + // Network error on a file at count=5 with max_retries=6 → renames to // cache format (.envelope) retry_test_ctx_t ctx = { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); @@ -306,11 +306,11 @@ SENTRY_TEST(retry_cache) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); TEST_CHECK(sentry__path_is_file(cached)); - // Success on a file at count=4 → also renames to cache format + // Success on a file at count=5 → also renames to cache format // (cache_keep preserves all envelopes regardless of send outcome) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); - write_retry_file(retry, old_ts, 4, &event_id); + write_retry_file(retry, old_ts, 5, &event_id); TEST_CHECK(!sentry__path_is_file(cached)); ctx = (retry_test_ctx_t) { 200, 0 }; From 00bef0171e2af65f9b53253574d45495519e89c3 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 18:28:46 +0100 Subject: [PATCH 070/121] fix(retry): avoid retry flush consuming entire shutdown timeout Rename sentry__retry_flush to sentry__retry_shutdown and remove the bgworker_flush call so bgworker_shutdown gets the full timeout. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 3 +-- src/sentry_retry.h | 4 ++-- src/transports/sentry_http_transport.c | 7 ++----- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 911afb045..016f59de8 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -335,7 +335,7 @@ drop_task_cb(void *_data, void *_ctx) } void -sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) +sentry__retry_shutdown(sentry_retry_t *retry) { if (retry) { // drop the delayed poll that would stall bgworker_flush @@ -343,7 +343,6 @@ sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout) retry->bgworker, retry_poll_task, drop_task_cb, NULL); sentry__atomic_store(&retry->scheduled, 0); sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); - sentry__bgworker_flush(retry->bgworker, timeout); } } diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 50f39e9ac..5789eb26a 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -20,9 +20,9 @@ void sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, sentry_retry_send_func_t send_cb, void *send_data); /** - * Flushes unprocessed previous-session retries. No-op if already polled. + * Prepares retry for shutdown: drops pending polls and submits a flush task. */ -void sentry__retry_flush(sentry_retry_t *retry, uint64_t timeout); +void sentry__retry_shutdown(sentry_retry_t *retry); /** * Dumps queued envelopes to the retry dir and seals against further writes. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 031a9be54..ac1b38a8a 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -305,12 +305,9 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry_bgworker_t *bgworker = transport_state; http_transport_state_t *state = sentry__bgworker_get_state(bgworker); - uint64_t started = sentry__monotonic_time(); - sentry__retry_flush(state->retry, timeout); - uint64_t elapsed = sentry__monotonic_time() - started; - uint64_t remaining = elapsed < timeout ? timeout - elapsed : 0; + sentry__retry_shutdown(state->retry); - int rv = sentry__bgworker_shutdown(bgworker, remaining); + int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); if (state->shutdown_client) { From d96beb1532aa1b7071d97f9fbe5b959f68cb5e2f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:21:49 +0100 Subject: [PATCH 071/121] fix(retry): warn on failed retry envelope rename Check the sentry__path_rename return value and log a warning on failure instead of silently ignoring it. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 016f59de8..6fce3a011 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -163,7 +163,10 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) sentry_path_t *new_path = sentry__retry_make_path( retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { - sentry__path_rename(item->path, new_path); + if (sentry__path_rename(item->path, new_path) != 0) { + SENTRY_WARNF( + "failed to rename retry envelope \"%s\"", item->path->path); + } sentry__path_free(new_path); } return true; From 985b3daf06c929f6e02454b575802b58be41c64b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:24:15 +0100 Subject: [PATCH 072/121] fix(retry): check for NULL from sentry__path_clone in retry send Move eligible++ after all item fields are populated so a NULL path from allocation failure does not leave a half-initialized item in the array. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6fce3a011..6ebbee398 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -249,12 +249,16 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_free(items); items = tmp; } - retry_item_t *item = &items[eligible++]; + retry_item_t *item = &items[eligible]; item->path = sentry__path_clone(p); + if (!item->path) { + break; + } item->ts = ts; item->count = count; memcpy(item->uuid, uuid, 36); item->uuid[36] = '\0'; + eligible++; } sentry__pathiter_free(piter); From 87532a30101b24003e8bcce46198f2be11f8ea5c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 19:45:02 +0100 Subject: [PATCH 073/121] fix(retry): fix data race on startup_time between threads Replace mutable startup_time + sealed with a state enum so that the startup flag is managed via atomic operations instead of clearing a uint64_t that may tear on 32-bit systems. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6ebbee398..48cd06f12 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -13,11 +13,17 @@ #define SENTRY_RETRY_INTERVAL (15 * 60 * 1000) #define SENTRY_RETRY_THROTTLE 100 +typedef enum { + SENTRY_RETRY_STARTUP = 0, + SENTRY_RETRY_RUNNING = 1, + SENTRY_RETRY_SEALED = 2 +} sentry_retry_state_t; + struct sentry_retry_s { sentry_path_t *cache_path; bool cache_keep; uint64_t startup_time; - volatile long sealed; + volatile long state; volatile long scheduled; sentry_bgworker_t *bgworker; sentry_retry_send_func_t send_cb; @@ -299,15 +305,19 @@ retry_poll_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (sentry__retry_send( - retry, retry->startup_time, retry->send_cb, retry->send_data)) { + uint64_t before + = sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_STARTUP + ? retry->startup_time + : 0; + if (sentry__retry_send(retry, before, retry->send_cb, retry->send_data)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } else { sentry__atomic_store(&retry->scheduled, 0); } // subsequent polls use backoff instead of the startup time filter - retry->startup_time = 0; + sentry__atomic_compare_swap( + &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING); } void @@ -327,9 +337,9 @@ retry_flush_task(void *_retry, void *_state) { (void)_state; sentry_retry_t *retry = _retry; - if (retry->startup_time > 0 && !sentry__atomic_fetch(&retry->sealed)) { + if (sentry__atomic_compare_swap( + &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING)) { sentry__retry_send(retry, UINT64_MAX, retry->send_cb, retry->send_data); - retry->startup_time = 0; } } @@ -367,7 +377,7 @@ sentry__retry_dump_queue( { if (retry) { // prevent duplicate writes from a still-running detached worker - sentry__atomic_store(&retry->sealed, 1); + sentry__atomic_store(&retry->state, SENTRY_RETRY_SEALED); sentry__bgworker_foreach_matching( retry->bgworker, task_func, retry_dump_cb, retry); } @@ -390,14 +400,14 @@ sentry__retry_trigger(sentry_retry_t *retry) void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { - if (sentry__atomic_fetch(&retry->sealed)) { + if (sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_SEALED) { return; } if (!sentry__retry_write_envelope(retry, envelope)) { return; } - // prevent the startup poll from re-processing this session's envelope - retry->startup_time = 0; + sentry__atomic_compare_swap( + &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING); if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); From 754a65d95f9f3c41e8648cd914329c20a29d70e5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 24 Feb 2026 20:31:48 +0100 Subject: [PATCH 074/121] fix(retry): clear retry_func when retry fails to initialize If sentry__retry_new() fails, retry_func was still set on the transport, causing can_retry to return true and envelopes to be dropped instead of cached. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index ac1b38a8a..230df598b 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -286,6 +286,10 @@ http_transport_start(const sentry_options_t *options, void *transport_state) state->retry = sentry__retry_new(options); if (state->retry) { sentry__retry_start(state->retry, bgworker, retry_send_cb, state); + } else { + // cannot retry, clear retry_func so envelopes get cached instead of + // dropped + sentry__transport_set_retry_func(options->transport, NULL); } } From 0e641152b7c8efcd43bbfdacc77538a5ed17abc2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 09:00:13 +0100 Subject: [PATCH 075/121] fix(retry): persist non-event envelopes to the retry cache Envelopes without an event_id (e.g. sessions) were silently dropped by sentry__retry_write_envelope. This was a workaround (cd57ff4d) for the old retry_write_envelope approach that regenerated a random UUID on each attempt, orphaning files on disk. The rewrite to rename-based counter bumps in handle_result (0f371772) made this safe: the UUID is parsed from the filename and preserved across renames, so a random UUID assigned at initial write stays stable through all retry cycles. Generate a random UUIDv4 for nil event_id envelopes instead of skipping them. Extract unreachable_dsn to module level. Add UUID consistency assertions to existing retry tests. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 +- src/sentry_retry.h | 2 +- tests/test_integration_http.py | 72 ++++++++++++++++++++++++++++++---- tests/unit/test_retry.c | 5 +-- 4 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 48cd06f12..4d976d569 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -137,7 +137,7 @@ sentry__retry_write_envelope( { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); if (sentry_uuid_is_nil(&event_id)) { - return false; + event_id = sentry_uuid_new_v4(); } char uuid[37]; diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 5789eb26a..a171ccdf4 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -37,7 +37,7 @@ void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); /** - * Writes an event envelope to the retry dir. Non-event envelopes are skipped. + * Writes an envelope to the retry dir. * Returns true if an envelope was written, false otherwise. */ bool sentry__retry_write_envelope( diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index e2b2ceac8..0561f98dc 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -59,6 +59,8 @@ def get_asan_crash_env(env): pytestmark = pytest.mark.skipif(not has_http, reason="tests need http") +unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" + # fmt: off auth_header = ( f"Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/{SENTRY_VERSION}" @@ -846,7 +848,6 @@ def test_http_retry_on_network_error(cmake, httpserver): cache_dir = tmp_path.joinpath(".sentry-native/cache") # unreachable port triggers CURLE_COULDNT_CONNECT - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -860,6 +861,7 @@ def test_http_retry_on_network_error(cmake, httpserver): cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-00-" in str(cache_files[0].name) + envelope_uuid = cache_files[0].stem[-36:] # retry on next run with working server env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) @@ -876,6 +878,7 @@ def test_http_retry_on_network_error(cmake, httpserver): assert len(httpserver.log) == 1 envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + assert envelope.headers["event_id"] == envelope_uuid assert_meta(envelope, integration="inproc") cache_files = list(cache_dir.glob("*.envelope")) @@ -887,7 +890,6 @@ def test_http_retry_multiple_attempts(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) run(tmp_path, "sentry_example", ["log", "capture-event"], env=env) @@ -895,18 +897,23 @@ def test_http_retry_multiple_attempts(cmake, httpserver): cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-00-" in str(cache_files[0].name) + envelope_uuid = cache_files[0].stem[-36:] + envelope = Envelope.deserialize(cache_files[0].read_bytes()) + assert envelope.headers["event_id"] == envelope_uuid run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-01-" in str(cache_files[0].name) + assert cache_files[0].stem[-36:] == envelope_uuid run(tmp_path, "sentry_example", ["log", "no-setup"], env=env) cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 1 assert "-02-" in str(cache_files[0].name) + assert cache_files[0].stem[-36:] == envelope_uuid # exhaust remaining retries (max 6) for i in range(4): @@ -922,7 +929,6 @@ def test_http_retry_with_cache_keep(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "inproc"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -955,7 +961,6 @@ def test_http_retry_cache_keep_max_attempts(cmake): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -1023,7 +1028,6 @@ def test_http_retry_multiple_success(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -1061,7 +1065,6 @@ def test_http_retry_multiple_network_error(cmake): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -1093,7 +1096,6 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) cache_dir = tmp_path.joinpath(".sentry-native/cache") - unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) run( @@ -1123,3 +1125,59 @@ def test_http_retry_multiple_rate_limit(cmake, httpserver): # first envelope gets 429, rest are discarded by rate limiter cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 0 + + +@pytest.mark.skipif(not has_files, reason="test needs a local filesystem") +def test_http_retry_session_on_network_error(cmake, httpserver): + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "none"}) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + env_unreachable = dict(os.environ, SENTRY_DSN=unreachable_dsn) + + run( + tmp_path, + "sentry_example", + ["log", "start-session"], + env=env_unreachable, + ) + + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-00-" in str(cache_files[0].name) + envelope_uuid = cache_files[0].stem[-36:] + + # second and third attempts still fail — envelope gets renamed each time + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env_unreachable) + + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-01-" in str(cache_files[0].name) + assert cache_files[0].stem[-36:] == envelope_uuid + + run(tmp_path, "sentry_example", ["log", "no-setup"], env=env_unreachable) + + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + assert "-02-" in str(cache_files[0].name) + assert cache_files[0].stem[-36:] == envelope_uuid + + # succeed on fourth attempt + env_reachable = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_oneshot_request("/api/123456/envelope/").respond_with_data("OK") + + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "no-setup"], + env=env_reachable, + ) + assert waiting.result + + assert len(httpserver.log) == 1 + envelope = Envelope.deserialize(httpserver.log[0][0].get_data()) + assert_session(envelope, {"init": True, "status": "exited", "errors": 0}) + + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 0 diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 553179932..aa9f2474d 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -260,9 +260,8 @@ SENTRY_TEST(retry_session) TEST_ASSERT(!!envelope); sentry__envelope_add_session(envelope, session); - // Session-only envelopes have no event_id → should not be written - TEST_CHECK(!sentry__retry_write_envelope(retry, envelope)); - TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 0); + TEST_CHECK(sentry__retry_write_envelope(retry, envelope)); + TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 1); sentry_envelope_free(envelope); sentry__session_free(session); From 9ab3e11ca9d2d8e0d0d6f7c191f593b62fc769aa Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 09:42:01 +0100 Subject: [PATCH 076/121] fix(retry): close race between poll task and enqueue on scheduled flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clear the scheduled flag before scanning so that a concurrent retry_enqueue always sees 0 and successfully arms a new poll via CAS. Previously, the flag was cleared after the scan returned, creating a window where enqueue could see 1, skip scheduling, and then the poll would clear the flag — stranding the newly enqueued item. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 4d976d569..d28852d6d 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -309,11 +309,12 @@ retry_poll_task(void *_retry, void *_state) = sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_STARTUP ? retry->startup_time : 0; - if (sentry__retry_send(retry, before, retry->send_cb, retry->send_data)) { + // clear before scanning so a concurrent enqueue sees 0 and arms a poll + sentry__atomic_store(&retry->scheduled, 0); + if (sentry__retry_send(retry, before, retry->send_cb, retry->send_data) + && sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); - } else { - sentry__atomic_store(&retry->scheduled, 0); } // subsequent polls use backoff instead of the startup time filter sentry__atomic_compare_swap( From 5a100446709ba59451b231a87a16926998ec8438 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:51:20 +0100 Subject: [PATCH 077/121] refactor(database): add retry_count to write_envelope, add sentry__run_write_cache Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 35 +++++++++++++++++++++++++++-------- src/sentry_database.h | 8 ++++++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 4bc8784fa..16a551a85 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -5,6 +5,7 @@ #include "sentry_options.h" #include "sentry_session.h" #include "sentry_transport.h" +#include "sentry_utils.h" #include "sentry_uuid.h" #include #include @@ -115,7 +116,8 @@ sentry__run_free(sentry_run_t *run) } static bool -write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) +write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope, + int retry_count) { sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); @@ -125,13 +127,18 @@ write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope) event_id = sentry_uuid_new_v4(); } - char *envelope_filename = sentry__uuid_as_filename(&event_id, ".envelope"); - if (!envelope_filename) { - return false; + char uuid[37]; + sentry_uuid_as_string(&event_id, uuid); + + char filename[128]; + if (retry_count >= 0) { + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", + sentry__usec_time() / 1000, retry_count, uuid); + } else { + snprintf(filename, sizeof(filename), "%.36s.envelope", uuid); } - sentry_path_t *output_path = sentry__path_join_str(path, envelope_filename); - sentry_free(envelope_filename); + sentry_path_t *output_path = sentry__path_join_str(path, filename); if (!output_path) { return false; } @@ -150,7 +157,7 @@ bool sentry__run_write_envelope( const sentry_run_t *run, const sentry_envelope_t *envelope) { - return write_envelope(run->run_path, envelope); + return write_envelope(run->run_path, envelope, -1); } bool @@ -162,7 +169,19 @@ sentry__run_write_external( return false; } - return write_envelope(run->external_path, envelope); + return write_envelope(run->external_path, envelope, -1); +} + +bool +sentry__run_write_cache( + const sentry_run_t *run, const sentry_envelope_t *envelope, int retry_count) +{ + if (sentry__path_create_dir_all(run->cache_path) != 0) { + SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); + return false; + } + + return write_envelope(run->cache_path, envelope, retry_count); } bool diff --git a/src/sentry_database.h b/src/sentry_database.h index fd44d598d..e28c62fe6 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -64,6 +64,14 @@ bool sentry__run_write_session( */ bool sentry__run_clear_session(const sentry_run_t *run); +/** + * This will serialize and write the given envelope to disk into the cache + * directory. When retry_count >= 0 the filename uses retry format + * `--.envelope`, otherwise `.envelope`. + */ +bool sentry__run_write_cache(const sentry_run_t *run, + const sentry_envelope_t *envelope, int retry_count); + /** * Moves `src` to `/cache/`. If `dst` is NULL, the filename of * `src` is used. From f35817d6e72cb3cd31c42715cf0c7bf553a432b6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:53:36 +0100 Subject: [PATCH 078/121] refactor(database): make sentry_run_t refcounted Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 13 ++++++++++- src/sentry_database.h | 6 +++++ src/sentry_retry.c | 50 +++++++++-------------------------------- src/sentry_retry.h | 7 ------ tests/unit/test_retry.c | 3 ++- 5 files changed, 30 insertions(+), 49 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 16a551a85..59d2e9bc8 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -4,6 +4,7 @@ #include "sentry_json.h" #include "sentry_options.h" #include "sentry_session.h" +#include "sentry_sync.h" #include "sentry_transport.h" #include "sentry_utils.h" #include "sentry_uuid.h" @@ -72,6 +73,7 @@ sentry__run_new(const sentry_path_t *database_path) return NULL; } + run->refcount = 1; run->uuid = uuid; run->run_path = run_path; run->session_path = session_path; @@ -94,6 +96,15 @@ sentry__run_new(const sentry_path_t *database_path) return NULL; } +sentry_run_t * +sentry__run_incref(sentry_run_t *run) +{ + if (run) { + sentry__atomic_fetch_and_add(&run->refcount, 1); + } + return run; +} + void sentry__run_clean(sentry_run_t *run) { @@ -104,7 +115,7 @@ sentry__run_clean(sentry_run_t *run) void sentry__run_free(sentry_run_t *run) { - if (!run) { + if (!run || sentry__atomic_fetch_and_add(&run->refcount, -1) != 1) { return; } sentry__path_free(run->run_path); diff --git a/src/sentry_database.h b/src/sentry_database.h index e28c62fe6..e461c25f3 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -13,6 +13,7 @@ typedef struct sentry_run_s { sentry_path_t *external_path; sentry_path_t *cache_path; sentry_filelock_t *lock; + long refcount; } sentry_run_t; /** @@ -23,6 +24,11 @@ typedef struct sentry_run_s { */ sentry_run_t *sentry__run_new(const sentry_path_t *database_path); +/** + * Increment the refcount and return the run pointer. + */ +sentry_run_t *sentry__run_incref(sentry_run_t *run); + /** * This will clean up all the files belonging to this run. */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d28852d6d..9f0cc2c00 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -20,7 +20,7 @@ typedef enum { } sentry_retry_state_t; struct sentry_retry_s { - sentry_path_t *cache_path; + sentry_run_t *run; bool cache_keep; uint64_t startup_time; volatile long state; @@ -38,14 +38,9 @@ sentry__retry_new(const sentry_options_t *options) return NULL; } memset(retry, 0, sizeof(sentry_retry_t)); - retry->cache_path = sentry__path_clone(options->run->cache_path); - if (!retry->cache_path) { - sentry_free(retry); - return NULL; - } + retry->run = sentry__run_incref(options->run); retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; - sentry__path_create_dir_all(retry->cache_path); return retry; } @@ -55,7 +50,7 @@ sentry__retry_free(sentry_retry_t *retry) if (!retry) { return; } - sentry__path_free(retry->cache_path); + sentry__run_free(retry->run); sentry_free(retry); } @@ -128,33 +123,7 @@ sentry__retry_make_path( char filename[128]; snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, count, uuid); - return sentry__path_join_str(retry->cache_path, filename); -} - -bool -sentry__retry_write_envelope( - sentry_retry_t *retry, const sentry_envelope_t *envelope) -{ - sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); - if (sentry_uuid_is_nil(&event_id)) { - event_id = sentry_uuid_new_v4(); - } - - char uuid[37]; - sentry_uuid_as_string(&event_id, uuid); - - sentry_path_t *path - = sentry__retry_make_path(retry, sentry__usec_time() / 1000, 0, uuid); - if (!path) { - return false; - } - - int rv = sentry_envelope_write_to_path(envelope, path); - if (rv != 0) { - SENTRY_WARNF("failed to write retry envelope to \"%s\"", path->path); - } - sentry__path_free(path); - return rv == 0; + return sentry__path_join_str(retry->run->cache_path, filename); } static bool @@ -196,7 +165,7 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) char cache_name[46]; snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); sentry_path_t *dest - = sentry__path_join_str(retry->cache_path, cache_name); + = sentry__path_join_str(retry->run->cache_path, cache_name); if (!dest || sentry__path_rename(item->path, dest) != 0) { sentry__path_remove(item->path); } @@ -212,7 +181,8 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, sentry_retry_send_func_t send_cb, void *data) { - sentry_pathiter_t *piter = sentry__path_iter_directory(retry->cache_path); + sentry_pathiter_t *piter + = sentry__path_iter_directory(retry->run->cache_path); if (!piter) { return 0; } @@ -367,8 +337,8 @@ sentry__retry_shutdown(sentry_retry_t *retry) static bool retry_dump_cb(void *_envelope, void *_retry) { - sentry__retry_write_envelope( - (sentry_retry_t *)_retry, (sentry_envelope_t *)_envelope); + sentry_retry_t *retry = (sentry_retry_t *)_retry; + sentry__run_write_cache(retry->run, (sentry_envelope_t *)_envelope, 0); return true; } @@ -404,7 +374,7 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) if (sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_SEALED) { return; } - if (!sentry__retry_write_envelope(retry, envelope)) { + if (!sentry__run_write_cache(retry->run, envelope, 0)) { return; } sentry__atomic_compare_swap( diff --git a/src/sentry_retry.h b/src/sentry_retry.h index a171ccdf4..edde49d8f 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -36,13 +36,6 @@ void sentry__retry_dump_queue( void sentry__retry_enqueue( sentry_retry_t *retry, const sentry_envelope_t *envelope); -/** - * Writes an envelope to the retry dir. - * Returns true if an envelope was written, false otherwise. - */ -bool sentry__retry_write_envelope( - sentry_retry_t *retry, const sentry_envelope_t *envelope); - /** * Sends eligible retry files via `send_cb`. `before > 0`: send files with * ts < before (startup). `before == 0`: use backoff. Returns remaining file diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index aa9f2474d..b5edb383e 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -1,3 +1,4 @@ +#include "sentry_core.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" @@ -260,7 +261,7 @@ SENTRY_TEST(retry_session) TEST_ASSERT(!!envelope); sentry__envelope_add_session(envelope, session); - TEST_CHECK(sentry__retry_write_envelope(retry, envelope)); + TEST_CHECK(sentry__run_write_cache(options->run, envelope, 0)); TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 1); sentry_envelope_free(envelope); From 123a7be4267152252ed39e724c7ec8b98ad9dee5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 16:55:10 +0100 Subject: [PATCH 079/121] refactor(cache): strip retry prefix in move_cache and simplify handle_result Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 26 +++++++++++++++++++++----- src/sentry_database.h | 7 ++++--- src/sentry_retry.c | 7 +------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 59d2e9bc8..0d869a325 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -197,14 +197,30 @@ sentry__run_write_cache( bool sentry__run_move_cache( - const sentry_run_t *run, const sentry_path_t *src, const char *dst) + const sentry_run_t *run, const sentry_path_t *src, int retry_count) { if (sentry__path_create_dir_all(run->cache_path) != 0) { SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path); return false; } - const char *filename = dst ? dst : sentry__path_filename(src); + char filename[128]; + const char *src_name = sentry__path_filename(src); + if (retry_count >= 0) { + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", + sentry__usec_time() / 1000, retry_count, src_name); + } else { + // Strip the retry prefix if present. Envelope filenames are either + // ".envelope" (45 chars) or "--.envelope" + // (>45 chars). The last 45 chars are always ".envelope". + size_t len = strlen(src_name); + if (len > 45) { + snprintf(filename, sizeof(filename), "%s", src_name + len - 45); + } else { + snprintf(filename, sizeof(filename), "%s", src_name); + } + } + sentry_path_t *dst_path = sentry__path_join_str(run->cache_path, filename); if (!dst_path) { return false; @@ -213,10 +229,10 @@ sentry__run_move_cache( int rv = sentry__path_rename(src, dst_path); sentry__path_free(dst_path); if (rv != 0) { - SENTRY_WARNF( - "failed to cache envelope \"%s\"", sentry__path_filename(src)); + SENTRY_WARNF("failed to cache envelope \"%s\"", src_name); return false; } + return true; } @@ -358,7 +374,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) sentry__capture_envelope(options->transport, envelope); if (can_cache - && sentry__run_move_cache(options->run, file, NULL)) { + && sentry__run_move_cache(options->run, file, -1)) { continue; } } diff --git a/src/sentry_database.h b/src/sentry_database.h index e461c25f3..0e0152423 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -79,11 +79,12 @@ bool sentry__run_write_cache(const sentry_run_t *run, const sentry_envelope_t *envelope, int retry_count); /** - * Moves `src` to `/cache/`. If `dst` is NULL, the filename of - * `src` is used. + * Moves a file into the cache directory. When retry_count >= 0 the + * destination uses retry format `--.envelope`, + * otherwise the original filename is preserved. */ bool sentry__run_move_cache( - const sentry_run_t *run, const sentry_path_t *src, const char *dst); + const sentry_run_t *run, const sentry_path_t *src, int retry_count); /** * This function is essential to send crash reports from previous runs of the diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 9f0cc2c00..7cca087ae 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -162,14 +162,9 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) // cache on last attempt if (exhausted && retry->cache_keep) { - char cache_name[46]; - snprintf(cache_name, sizeof(cache_name), "%.36s.envelope", item->uuid); - sentry_path_t *dest - = sentry__path_join_str(retry->run->cache_path, cache_name); - if (!dest || sentry__path_rename(item->path, dest) != 0) { + if (!sentry__run_move_cache(retry->run, item->path, -1)) { sentry__path_remove(item->path); } - sentry__path_free(dest); return false; } From acfd148462bf81f174b4831747db8dc28bab1d2a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 25 Feb 2026 18:01:07 +0100 Subject: [PATCH 080/121] fix(cache): use cache_name instead of src_name for UUID in move_cache The retry_count >= 0 branch passed the full source filename to %.36s, which would grab the timestamp prefix instead of the UUID for retry- format filenames. Extract the cache name (last 45 chars) before either branch so both use the correct UUID. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 0d869a325..c46c1dcaa 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -206,19 +206,16 @@ sentry__run_move_cache( char filename[128]; const char *src_name = sentry__path_filename(src); + // Strip the retry prefix if present. Envelope filenames are either + // ".envelope" (45 chars) or "--.envelope" + // (>45 chars). The last 45 chars are always ".envelope". + size_t src_len = strlen(src_name); + const char *cache_name = src_len > 45 ? src_name + src_len - 45 : src_name; if (retry_count >= 0) { snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", - sentry__usec_time() / 1000, retry_count, src_name); + sentry__usec_time() / 1000, retry_count, cache_name); } else { - // Strip the retry prefix if present. Envelope filenames are either - // ".envelope" (45 chars) or "--.envelope" - // (>45 chars). The last 45 chars are always ".envelope". - size_t len = strlen(src_name); - if (len > 45) { - snprintf(filename, sizeof(filename), "%s", src_name + len - 45); - } else { - snprintf(filename, sizeof(filename), "%s", src_name); - } + snprintf(filename, sizeof(filename), "%s", cache_name); } sentry_path_t *dst_path = sentry__path_join_str(run->cache_path, filename); From 019e0e9b0ba1196ed887dce6b21ca72b3fd7caae Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 08:47:05 +0100 Subject: [PATCH 081/121] fix(retry): prevent duplicate cache writes during shutdown race Use a mutex (sealed_lock) to serialize the SEALED check in retry_enqueue with the SEALED set in retry_dump_queue. Store the envelope address as uintptr_t so retry_dump_cb can skip envelopes already written by retry_enqueue without risking accidental dereferencing. The address is safe to compare because the task holds a ref that keeps the envelope alive during foreach_matching. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 7cca087ae..f10a98431 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -28,6 +28,8 @@ struct sentry_retry_s { sentry_bgworker_t *bgworker; sentry_retry_send_func_t send_cb; void *send_data; + sentry_mutex_t sealed_lock; + uintptr_t sealed_envelope; }; sentry_retry_t * @@ -38,6 +40,7 @@ sentry__retry_new(const sentry_options_t *options) return NULL; } memset(retry, 0, sizeof(sentry_retry_t)); + sentry__mutex_init(&retry->sealed_lock); retry->run = sentry__run_incref(options->run); retry->cache_keep = options->cache_keep; retry->startup_time = sentry__usec_time() / 1000; @@ -50,6 +53,7 @@ sentry__retry_free(sentry_retry_t *retry) if (!retry) { return; } + sentry__mutex_free(&retry->sealed_lock); sentry__run_free(retry->run); sentry_free(retry); } @@ -333,7 +337,10 @@ static bool retry_dump_cb(void *_envelope, void *_retry) { sentry_retry_t *retry = (sentry_retry_t *)_retry; - sentry__run_write_cache(retry->run, (sentry_envelope_t *)_envelope, 0); + sentry_envelope_t *envelope = (sentry_envelope_t *)_envelope; + if ((uintptr_t)envelope != retry->sealed_envelope) { + sentry__run_write_cache(retry->run, envelope, 0); + } return true; } @@ -343,7 +350,10 @@ sentry__retry_dump_queue( { if (retry) { // prevent duplicate writes from a still-running detached worker + sentry__mutex_lock(&retry->sealed_lock); sentry__atomic_store(&retry->state, SENTRY_RETRY_SEALED); + sentry__mutex_unlock(&retry->sealed_lock); + sentry__bgworker_foreach_matching( retry->bgworker, task_func, retry_dump_cb, retry); } @@ -366,12 +376,18 @@ sentry__retry_trigger(sentry_retry_t *retry) void sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) { + sentry__mutex_lock(&retry->sealed_lock); if (sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_SEALED) { + sentry__mutex_unlock(&retry->sealed_lock); return; } if (!sentry__run_write_cache(retry->run, envelope, 0)) { + sentry__mutex_unlock(&retry->sealed_lock); return; } + retry->sealed_envelope = (uintptr_t)envelope; + sentry__mutex_unlock(&retry->sealed_lock); + sentry__atomic_compare_swap( &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING); if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { From 0e83c47dc149ab7b79213a68b30fc6dca06b00f6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:35:50 +0100 Subject: [PATCH 082/121] fix(cache): replace length heuristic with proper filename parsing in move_cache Move sentry__retry_parse_filename and sentry__retry_make_path into sentry_database as sentry__parse_cache_filename and sentry__run_make_cache_path. This consolidates cache filename format knowledge in one module and replaces the fragile `src_len > 45` heuristic in sentry__run_move_cache with proper parsing. Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 57 +++++++++++++++++++++++++++++++++++++---- src/sentry_database.h | 13 ++++++++++ src/sentry_retry.c | 51 +++--------------------------------- src/sentry_retry.h | 12 --------- tests/unit/test_retry.c | 44 +++++++++++++++---------------- 5 files changed, 90 insertions(+), 87 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index c46c1dcaa..a7114e3bc 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -195,6 +195,51 @@ sentry__run_write_cache( return write_envelope(run->cache_path, envelope, retry_count); } +bool +sentry__parse_cache_filename(const char *filename, uint64_t *ts_out, + int *count_out, const char **uuid_out) +{ + // Minimum retry filename: --.envelope (49+ chars). + // Cache filenames are exactly 45 chars (.envelope). + if (strlen(filename) <= 45) { + return false; + } + + char *end; + uint64_t ts = strtoull(filename, &end, 10); + if (*end != '-') { + return false; + } + + const char *count_str = end + 1; + long count = strtol(count_str, &end, 10); + if (*end != '-' || count < 0) { + return false; + } + + const char *uuid = end + 1; + size_t tail_len = strlen(uuid); + // 36 chars UUID (with dashes) + ".envelope" + if (tail_len != 36 + 9 || strcmp(uuid + 36, ".envelope") != 0) { + return false; + } + + *ts_out = ts; + *count_out = (int)count; + *uuid_out = uuid; + return true; +} + +sentry_path_t * +sentry__run_make_cache_path( + const sentry_run_t *run, uint64_t ts, int count, const char *uuid) +{ + char filename[128]; + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, + count, uuid); + return sentry__path_join_str(run->cache_path, filename); +} + bool sentry__run_move_cache( const sentry_run_t *run, const sentry_path_t *src, int retry_count) @@ -206,11 +251,13 @@ sentry__run_move_cache( char filename[128]; const char *src_name = sentry__path_filename(src); - // Strip the retry prefix if present. Envelope filenames are either - // ".envelope" (45 chars) or "--.envelope" - // (>45 chars). The last 45 chars are always ".envelope". - size_t src_len = strlen(src_name); - const char *cache_name = src_len > 45 ? src_name + src_len - 45 : src_name; + uint64_t parsed_ts; + int parsed_count; + const char *parsed_uuid; + const char *cache_name = sentry__parse_cache_filename(src_name, &parsed_ts, + &parsed_count, &parsed_uuid) + ? parsed_uuid + : src_name; if (retry_count >= 0) { snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", sentry__usec_time() / 1000, retry_count, cache_name); diff --git a/src/sentry_database.h b/src/sentry_database.h index 0e0152423..0e355f482 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -86,6 +86,12 @@ bool sentry__run_write_cache(const sentry_run_t *run, bool sentry__run_move_cache( const sentry_run_t *run, const sentry_path_t *src, int retry_count); +/** + * Builds a cache path: `/cache/--.envelope`. + */ +sentry_path_t *sentry__run_make_cache_path( + const sentry_run_t *run, uint64_t ts, int count, const char *uuid); + /** * This function is essential to send crash reports from previous runs of the * program. @@ -101,6 +107,13 @@ bool sentry__run_move_cache( void sentry__process_old_runs( const sentry_options_t *options, uint64_t last_crash); +/** + * Parses a retry cache filename: `--.envelope`. + * Returns false for plain cache filenames (`.envelope`). + */ +bool sentry__parse_cache_filename(const char *filename, uint64_t *ts_out, + int *count_out, const char **uuid_out); + /** * Cleans up the cache based on options.cache_max_items, * options.cache_max_size and options.cache_max_age. diff --git a/src/sentry_retry.c b/src/sentry_retry.c index f10a98431..bff474963 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -58,41 +58,6 @@ sentry__retry_free(sentry_retry_t *retry) sentry_free(retry); } -bool -sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, - int *count_out, const char **uuid_out) -{ - // Minimum retry filename: --.envelope (49+ chars). - // Cache filenames are exactly 45 chars (.envelope). - if (strlen(filename) <= 45) { - return false; - } - - char *end; - uint64_t ts = strtoull(filename, &end, 10); - if (*end != '-') { - return false; - } - - const char *count_str = end + 1; - long count = strtol(count_str, &end, 10); - if (*end != '-' || count < 0) { - return false; - } - - const char *uuid = end + 1; - size_t tail_len = strlen(uuid); - // 36 chars UUID (with dashes) + ".envelope" - if (tail_len != 36 + 9 || strcmp(uuid + 36, ".envelope") != 0) { - return false; - } - - *ts_out = ts; - *count_out = (int)count; - *uuid_out = uuid; - return true; -} - uint64_t sentry__retry_backoff(int count) { @@ -120,16 +85,6 @@ compare_retry_items(const void *a, const void *b) return strcmp(ia->uuid, ib->uuid); } -sentry_path_t * -sentry__retry_make_path( - sentry_retry_t *retry, uint64_t ts, int count, const char *uuid) -{ - char filename[128]; - snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, - count, uuid); - return sentry__path_join_str(retry->run->cache_path, filename); -} - static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { @@ -139,8 +94,8 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) // network failure with retries remaining: bump count & re-enqueue if (item->count + 1 < SENTRY_RETRY_ATTEMPTS && status_code < 0) { - sentry_path_t *new_path = sentry__retry_make_path( - retry, sentry__usec_time() / 1000, item->count + 1, item->uuid); + sentry_path_t *new_path = sentry__run_make_cache_path(retry->run, + sentry__usec_time() / 1000, item->count + 1, item->uuid); if (new_path) { if (sentry__path_rename(item->path, new_path) != 0) { SENTRY_WARNF( @@ -203,7 +158,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, uint64_t ts; int count; const char *uuid; - if (!sentry__retry_parse_filename(fname, &ts, &count, &uuid)) { + if (!sentry__parse_cache_filename(fname, &ts, &count, &uuid)) { continue; } if (before > 0 && ts >= before) { diff --git a/src/sentry_retry.h b/src/sentry_retry.h index edde49d8f..5e5222097 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -49,18 +49,6 @@ size_t sentry__retry_send(sentry_retry_t *retry, uint64_t before, */ uint64_t sentry__retry_backoff(int count); -/** - * /cache/--.envelope - */ -sentry_path_t *sentry__retry_make_path( - sentry_retry_t *retry, uint64_t ts, int count, const char *uuid); - -/** - * --.envelope - */ -bool sentry__retry_parse_filename(const char *filename, uint64_t *ts_out, - int *count_out, const char **uuid_out); - /** * Submits a delayed retry poll task on the background worker. */ diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index b5edb383e..94c4f638a 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -40,7 +40,7 @@ find_envelope_attempt(const sentry_path_t *dir) uint64_t ts; int attempt; const char *uuid; - if (sentry__retry_parse_filename(name, &ts, &attempt, &uuid)) { + if (sentry__parse_cache_filename(name, &ts, &attempt, &uuid)) { sentry__pathiter_free(iter); return attempt; } @@ -50,7 +50,7 @@ find_envelope_attempt(const sentry_path_t *dir) } static void -write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, +write_retry_file(const sentry_run_t *run, uint64_t timestamp, int retry_count, const sentry_uuid_t *event_id) { sentry_envelope_t *envelope = sentry__envelope_new(); @@ -61,7 +61,7 @@ write_retry_file(sentry_retry_t *retry, uint64_t timestamp, int retry_count, sentry_uuid_as_string(event_id, uuid); sentry_path_t *path - = sentry__retry_make_path(retry, timestamp, retry_count, uuid); + = sentry__run_make_cache_path(run, timestamp, retry_count, uuid); (void)sentry_envelope_write_to_path(envelope, path); sentry__path_free(path); sentry_envelope_free(envelope); @@ -87,30 +87,30 @@ SENTRY_TEST(retry_filename) int count; const char *uuid; - TEST_CHECK(sentry__retry_parse_filename( + TEST_CHECK(sentry__parse_cache_filename( "1234567890-00-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); TEST_CHECK_UINT64_EQUAL(ts, 1234567890); TEST_CHECK_INT_EQUAL(count, 0); TEST_CHECK(strncmp(uuid, "abcdefab-1234-5678-9abc-def012345678", 36) == 0); - TEST_CHECK(sentry__retry_parse_filename( + TEST_CHECK(sentry__parse_cache_filename( "999-04-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); TEST_CHECK_UINT64_EQUAL(ts, 999); TEST_CHECK_INT_EQUAL(count, 4); // negative count - TEST_CHECK(!sentry__retry_parse_filename( + TEST_CHECK(!sentry__parse_cache_filename( "123--01-abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); // cache filename (no timestamp/count) - TEST_CHECK(!sentry__retry_parse_filename( + TEST_CHECK(!sentry__parse_cache_filename( "abcdefab-1234-5678-9abc-def012345678.envelope", &ts, &count, &uuid)); // missing .envelope suffix - TEST_CHECK(!sentry__retry_parse_filename( + TEST_CHECK(!sentry__parse_cache_filename( "123-00-abcdefab-1234-5678-9abc-def012345678.txt", &ts, &count, &uuid)); } @@ -132,7 +132,7 @@ SENTRY_TEST(retry_throttle) sentry_uuid_t ids[4]; for (int i = 0; i < 4; i++) { ids[i] = sentry_uuid_new_v4(); - write_retry_file(retry, old_ts, 0, &ids[i]); + write_retry_file(options->run, old_ts, 0, &ids[i]); } TEST_CHECK_INT_EQUAL(count_envelope_files(options->run->cache_path), 4); @@ -162,7 +162,7 @@ SENTRY_TEST(retry_skew) // future timestamp simulates clock moving backward uint64_t future_ts = sentry__usec_time() / 1000 + 1000000; sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry, future_ts, 0, &event_id); + write_retry_file(options->run, future_ts, 0, &event_id); retry_test_ctx_t ctx = { 200, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); @@ -193,7 +193,7 @@ SENTRY_TEST(retry_result) sentry_uuid_t event_id = sentry_uuid_new_v4(); // 1. Success (200) → removes - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 1); TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 0); @@ -203,21 +203,21 @@ SENTRY_TEST(retry_result) TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 2. Rate limited (429) → removes - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { 429, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 3. Discard (0) → removes - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); ctx = (retry_test_ctx_t) { 0, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); TEST_CHECK_INT_EQUAL(count_envelope_files(cache_path), 0); // 4. Network error → bumps count - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); TEST_CHECK_INT_EQUAL(find_envelope_attempt(cache_path), 0); ctx = (retry_test_ctx_t) { -1, 0 }; @@ -231,7 +231,7 @@ SENTRY_TEST(retry_result) sentry__path_create_dir_all(cache_path); uint64_t very_old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); - write_retry_file(retry, very_old_ts, 5, &event_id); + write_retry_file(options->run, very_old_ts, 5, &event_id); ctx = (retry_test_ctx_t) { -1, 0 }; sentry__retry_send(retry, 0, test_send_cb, &ctx); TEST_CHECK_INT_EQUAL(ctx.count, 1); @@ -287,7 +287,7 @@ SENTRY_TEST(retry_cache) uint64_t old_ts = sentry__usec_time() / 1000 - 2 * sentry__retry_backoff(5); sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry, old_ts, 5, &event_id); + write_retry_file(options->run, old_ts, 5, &event_id); char uuid_str[37]; sentry_uuid_as_string(&event_id, uuid_str); @@ -310,7 +310,7 @@ SENTRY_TEST(retry_cache) // (cache_keep preserves all envelopes regardless of send outcome) sentry__path_remove_all(cache_path); sentry__path_create_dir_all(cache_path); - write_retry_file(retry, old_ts, 5, &event_id); + write_retry_file(options->run, old_ts, 5, &event_id); TEST_CHECK(!sentry__path_is_file(cached)); ctx = (retry_test_ctx_t) { 200, 0 }; @@ -380,19 +380,19 @@ SENTRY_TEST(retry_backoff) // retry 0: 10*base old, eligible (backoff=base) sentry_uuid_t id1 = sentry_uuid_new_v4(); - write_retry_file(retry, ref, 0, &id1); + write_retry_file(options->run, ref, 0, &id1); // retry 1: 1*base old, not yet eligible (backoff=2*base) sentry_uuid_t id2 = sentry_uuid_new_v4(); - write_retry_file(retry, ref + 9 * base, 1, &id2); + write_retry_file(options->run, ref + 9 * base, 1, &id2); // retry 1: 10*base old, eligible (backoff=2*base) sentry_uuid_t id3 = sentry_uuid_new_v4(); - write_retry_file(retry, ref, 1, &id3); + write_retry_file(options->run, ref, 1, &id3); // retry 2: 2*base old, not eligible (backoff=4*base) sentry_uuid_t id4 = sentry_uuid_new_v4(); - write_retry_file(retry, ref + 8 * base, 2, &id4); + write_retry_file(options->run, ref + 8 * base, 2, &id4); // With backoff: only eligible ones (id1 and id3) are sent retry_test_ctx_t ctx = { 200, 0 }; @@ -437,7 +437,7 @@ SENTRY_TEST(retry_trigger) uint64_t old_ts = sentry__usec_time() / 1000 - 10 * sentry__retry_backoff(0); sentry_uuid_t event_id = sentry_uuid_new_v4(); - write_retry_file(retry, old_ts, 0, &event_id); + write_retry_file(options->run, old_ts, 0, &event_id); // UINT64_MAX (trigger mode) bypasses backoff: bumps count retry_test_ctx_t ctx = { -1, 0 }; From 5f8e4526314142de1021e835571ca8df004f410b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:43:56 +0100 Subject: [PATCH 083/121] =?UTF-8?q?docs:=20fix=20retry=20count=205=20?= =?UTF-8?q?=E2=86=92=206=20in=20sentry=5Ftransport=5Fretry=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SENTRY_RETRY_ATTEMPTS constant was bumped from 5 to 6 in 81d0f68a but the public API documentation was not updated to match. Co-Authored-By: Claude Opus 4.6 --- include/sentry.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/sentry.h b/include/sentry.h index ae5df82bb..00bc3cab4 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -952,7 +952,7 @@ SENTRY_API void sentry_transport_set_shutdown_func( * * Note: The SDK automatically retries failed envelopes on next application * startup. This function allows manual triggering of pending retries at - * runtime. Each envelope is retried up to 5 times. If all attempts are + * runtime. Each envelope is retried up to 6 times. If all attempts are * exhausted during intermittent connectivity, events will be discarded * (or moved to cache if enabled via sentry_options_set_cache_keep). * From 69ffc09ef5fb03bea56b7e91743ba4ec09e8b63b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 09:47:16 +0100 Subject: [PATCH 084/121] fix(retry): prevent poll task from re-arming after shutdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use a SENTRY_POLL_SHUTDOWN sentinel so that a concurrent retry_poll_task cannot resubmit the delayed poll that shutdown just dropped. The CAS(SCHEDULED→IDLE) in retry_poll_task is a no-op when scheduled is SHUTDOWN, and the subsequent CAS(IDLE→SCHEDULED) also fails. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index bff474963..74502d2a4 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -19,6 +19,12 @@ typedef enum { SENTRY_RETRY_SEALED = 2 } sentry_retry_state_t; +typedef enum { + SENTRY_POLL_IDLE = 0, + SENTRY_POLL_SCHEDULED = 1, + SENTRY_POLL_SHUTDOWN = 2 +} sentry_poll_state_t; + struct sentry_retry_s { sentry_run_t *run; bool cache_keep; @@ -233,10 +239,12 @@ retry_poll_task(void *_retry, void *_state) = sentry__atomic_fetch(&retry->state) == SENTRY_RETRY_STARTUP ? retry->startup_time : 0; - // clear before scanning so a concurrent enqueue sees 0 and arms a poll - sentry__atomic_store(&retry->scheduled, 0); + // CAS instead of unconditional store to preserve SENTRY_POLL_SHUTDOWN + sentry__atomic_compare_swap( + &retry->scheduled, SENTRY_POLL_SCHEDULED, SENTRY_POLL_IDLE); if (sentry__retry_send(retry, before, retry->send_cb, retry->send_data) - && sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { + && sentry__atomic_compare_swap( + &retry->scheduled, SENTRY_POLL_IDLE, SENTRY_POLL_SCHEDULED)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } @@ -252,7 +260,7 @@ sentry__retry_start(sentry_retry_t *retry, sentry_bgworker_t *bgworker, retry->bgworker = bgworker; retry->send_cb = send_cb; retry->send_data = send_data; - sentry__atomic_store(&retry->scheduled, 1); + sentry__atomic_store(&retry->scheduled, SENTRY_POLL_SCHEDULED); sentry__bgworker_submit_delayed( bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_THROTTLE); } @@ -280,10 +288,10 @@ void sentry__retry_shutdown(sentry_retry_t *retry) { if (retry) { - // drop the delayed poll that would stall bgworker_flush + // drop the delayed poll and prevent retry_poll_task from re-arming sentry__bgworker_foreach_matching( retry->bgworker, retry_poll_task, drop_task_cb, NULL); - sentry__atomic_store(&retry->scheduled, 0); + sentry__atomic_store(&retry->scheduled, SENTRY_POLL_SHUTDOWN); sentry__bgworker_submit(retry->bgworker, retry_flush_task, NULL, retry); } } @@ -345,7 +353,8 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) sentry__atomic_compare_swap( &retry->state, SENTRY_RETRY_STARTUP, SENTRY_RETRY_RUNNING); - if (sentry__atomic_compare_swap(&retry->scheduled, 0, 1)) { + if (sentry__atomic_compare_swap( + &retry->scheduled, SENTRY_POLL_IDLE, SENTRY_POLL_SCHEDULED)) { sentry__bgworker_submit_delayed(retry->bgworker, retry_poll_task, NULL, retry, SENTRY_RETRY_INTERVAL); } From e50dcf571645986664f5e6ae403d03a8d22fdfea Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 14:40:00 +0100 Subject: [PATCH 085/121] fix(winhttp): cancel in-flight request before shutdown to unblock worker On Windows, WinHTTP TCP connect to an unreachable host takes ~2s, which can exceed the shutdown timeout. Add a cancel_client callback that closes just the WinHTTP request handle, unblocking the worker thread so it can process the failure and shut down cleanly. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 12 ++++++++++++ src/transports/sentry_http_transport.h | 2 ++ src/transports/sentry_http_transport_winhttp.c | 11 +++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 230df598b..aa7574f33 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -30,6 +30,7 @@ typedef struct { void (*free_client)(void *); int (*start_client)(void *, const sentry_options_t *); sentry_http_send_func_t send_func; + void (*cancel_client)(void *client); void (*shutdown_client)(void *client); sentry_retry_t *retry; } http_transport_state_t; @@ -311,6 +312,10 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry__retry_shutdown(state->retry); + if (state->cancel_client) { + state->cancel_client(state->client); + } + int rv = sentry__bgworker_shutdown(bgworker, timeout); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); @@ -414,6 +419,13 @@ sentry__http_transport_set_start_client(sentry_transport_t *transport, http_transport_get_state(transport)->start_client = start_client; } +void +sentry__http_transport_set_cancel_client( + sentry_transport_t *transport, void (*cancel_client)(void *)) +{ + http_transport_get_state(transport)->cancel_client = cancel_client; +} + void sentry__http_transport_set_shutdown_client( sentry_transport_t *transport, void (*shutdown_client)(void *)) diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index 30493d564..b186b9eeb 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -47,6 +47,8 @@ void sentry__http_transport_set_free_client( sentry_transport_t *transport, void (*free_client)(void *)); void sentry__http_transport_set_start_client(sentry_transport_t *transport, int (*start_client)(void *, const sentry_options_t *)); +void sentry__http_transport_set_cancel_client( + sentry_transport_t *transport, void (*cancel_client)(void *)); void sentry__http_transport_set_shutdown_client( sentry_transport_t *transport, void (*shutdown_client)(void *)); diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index e73de7405..e086af017 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -140,6 +140,16 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 0; } +static void +winhttp_client_cancel(void *_client) +{ + winhttp_client_t *client = _client; + if (client->request) { + WinHttpCloseHandle(client->request); + client->request = NULL; + } +} + static void winhttp_client_shutdown(void *_client) { @@ -333,6 +343,7 @@ sentry__transport_new_default(void) } sentry__http_transport_set_free_client(transport, winhttp_client_free); sentry__http_transport_set_start_client(transport, winhttp_client_start); + sentry__http_transport_set_cancel_client(transport, winhttp_client_cancel); sentry__http_transport_set_shutdown_client( transport, winhttp_client_shutdown); return transport; From db196795759a43e3ba83b5d6f00935f12b628f82 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 14:54:37 +0100 Subject: [PATCH 086/121] fix(winhttp): fix double-close race on client->request between cancel and worker Use InterlockedExchangePointer to atomically swap client->request to NULL in cancel, shutdown, and worker exit cleanup. Whichever thread wins the swap closes the handle; the loser gets NULL and skips. The worker also snapshots client->request into a local variable right after WinHttpOpenRequest and uses the local for all subsequent API calls, so it never reads NULL from the struct if cancel fires mid-function. Co-Authored-By: Claude Opus 4.6 --- .../sentry_http_transport_winhttp.c | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index e086af017..dcd6d3c98 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -144,9 +144,9 @@ static void winhttp_client_cancel(void *_client) { winhttp_client_t *client = _client; - if (client->request) { - WinHttpCloseHandle(client->request); - client->request = NULL; + HINTERNET request; + if ((request = InterlockedExchangePointer(&client->request, NULL))) { + WinHttpCloseHandle(request); } } @@ -167,9 +167,9 @@ winhttp_client_shutdown(void *_client) WinHttpCloseHandle(client->session); client->session = NULL; } - if (client->request) { - WinHttpCloseHandle(client->request); - client->request = NULL; + HINTERNET request; + if ((request = InterlockedExchangePointer(&client->request, NULL))) { + WinHttpCloseHandle(request); } } @@ -218,7 +218,8 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, client->request = WinHttpOpenRequest(client->connect, L"POST", url_components.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, is_secure ? WINHTTP_FLAG_SECURE : 0); - if (!client->request) { + HINTERNET request = client->request; + if (!request) { SENTRY_WARNF( "`WinHttpOpenRequest` failed with code `%d`", GetLastError()); goto exit; @@ -249,22 +250,22 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, } if (client->proxy_username && client->proxy_password) { - WinHttpSetCredentials(client->request, WINHTTP_AUTH_TARGET_PROXY, + WinHttpSetCredentials(request, WINHTTP_AUTH_TARGET_PROXY, WINHTTP_AUTH_SCHEME_BASIC, client->proxy_username, client->proxy_password, 0); } - if ((result = WinHttpSendRequest(client->request, headers, (DWORD)-1, - (LPVOID)req->body, (DWORD)req->body_len, (DWORD)req->body_len, - 0))) { - WinHttpReceiveResponse(client->request, NULL); + if ((result + = WinHttpSendRequest(request, headers, (DWORD)-1, (LPVOID)req->body, + (DWORD)req->body_len, (DWORD)req->body_len, 0))) { + WinHttpReceiveResponse(request, NULL); if (client->debug) { // this is basically the example from: // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpqueryheaders#examples DWORD dwSize = 0; LPVOID lpOutBuffer = NULL; - WinHttpQueryHeaders(client->request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, WINHTTP_NO_HEADER_INDEX); @@ -274,7 +275,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, // Now, use WinHttpQueryHeaders to retrieve the header. if (lpOutBuffer - && WinHttpQueryHeaders(client->request, + && WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, lpOutBuffer, &dwSize, WINHTTP_NO_HEADER_INDEX)) { @@ -292,17 +293,17 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, DWORD status_code = 0; DWORD status_code_size = sizeof(status_code); - WinHttpQueryHeaders(client->request, + WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX); resp->status_code = (int)status_code; - if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, + if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, L"x-sentry-rate-limits", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { resp->x_sentry_rate_limits = sentry__string_from_wstr(buf); - } else if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, + } else if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, L"retry-after", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { resp->retry_after = sentry__string_from_wstr(buf); @@ -316,9 +317,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, SENTRY_DEBUGF("request handled in %llums", now - started); exit: - if (client->request) { - HINTERNET request = client->request; - client->request = NULL; + if ((request = InterlockedExchangePointer(&client->request, NULL))) { WinHttpCloseHandle(request); } sentry_free(url); From 898308db578a66126b22bbcf30f7ca17b83c79f1 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 17:37:10 +0100 Subject: [PATCH 087/121] fix(winhttp): use on_timeout callback to unblock worker instead of cancel Replace the unconditional cancel_client call before bgworker_shutdown with an on_timeout callback that fires only when the shutdown timeout expires. This avoids aborting in-flight requests that would have completed within the timeout, while still unblocking the worker when it's stuck (e.g. WinHTTP connect to unreachable host). The callback closes session/connect handles to cancel pending WinHTTP operations, then the shutdown loop falls through to the !running check which joins the worker thread. This ensures handle_result runs and the retry counter is properly bumped on disk. Co-Authored-By: Claude Opus 4.6 --- src/sentry_sync.c | 23 +++++++++++----- src/sentry_sync.h | 14 +++++++++- src/transports/sentry_http_transport.c | 27 ++++++++----------- src/transports/sentry_http_transport.h | 2 -- .../sentry_http_transport_winhttp.c | 11 -------- 5 files changed, 41 insertions(+), 36 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index c0d903f03..350235a7a 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -425,7 +425,8 @@ shutdown_task(void *task_data, void *UNUSED(state)) } int -sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout) +sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, + void (*on_timeout)(void *), void *on_timeout_data) { if (!sentry__atomic_fetch(&bgw->running)) { SENTRY_WARN("trying to shut down non-running thread"); @@ -442,11 +443,21 @@ sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout) uint64_t now = sentry__monotonic_time(); if (now > started && now - started > timeout) { sentry__atomic_store(&bgw->running, 0); - sentry__thread_detach(bgw->thread_id); - sentry__mutex_unlock(&bgw->task_lock); - SENTRY_WARN( - "background thread failed to shut down cleanly within timeout"); - return 1; + if (on_timeout) { + // Unblock the worker (e.g. close transport handles) and + // let it finish in-flight work like handle_result. + sentry__mutex_unlock(&bgw->task_lock); + on_timeout(on_timeout_data); + on_timeout = NULL; + sentry__mutex_lock(&bgw->task_lock); + // fall through to !running check below + } else { + sentry__thread_detach(bgw->thread_id); + sentry__mutex_unlock(&bgw->task_lock); + SENTRY_WARN("background thread failed to shut down cleanly " + "within timeout"); + return 1; + } } if (!sentry__atomic_fetch(&bgw->running)) { diff --git a/src/sentry_sync.h b/src/sentry_sync.h index eab47cd59..d4f6001e7 100644 --- a/src/sentry_sync.h +++ b/src/sentry_sync.h @@ -469,8 +469,20 @@ int sentry__bgworker_flush(sentry_bgworker_t *bgw, uint64_t timeout); /** * This will try to shut down the background worker thread, with a `timeout`. * Returns 0 on success. + * + * The `_cb` variant accepts an `on_timeout` callback that is invoked when + * the timeout expires, just before detaching the thread. This gives the + * caller a chance to unblock the worker (e.g. by closing transport handles) + * so it can finish in-flight work. */ -int sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout); +int sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, + void (*on_timeout)(void *), void *on_timeout_data); + +static inline int +sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout) +{ + return sentry__bgworker_shutdown_cb(bgw, timeout, NULL, NULL); +} /** * This will set a preferable thread name for background worker. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index aa7574f33..237052689 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -30,7 +30,6 @@ typedef struct { void (*free_client)(void *); int (*start_client)(void *, const sentry_options_t *); sentry_http_send_func_t send_func; - void (*cancel_client)(void *client); void (*shutdown_client)(void *client); sentry_retry_t *retry; } http_transport_state_t; @@ -260,6 +259,15 @@ http_send_task(void *_envelope, void *_state) } } +static void +http_transport_shutdown_timeout(void *_state) +{ + http_transport_state_t *state = _state; + if (state->shutdown_client) { + state->shutdown_client(state->client); + } +} + static int http_transport_start(const sentry_options_t *options, void *transport_state) { @@ -312,16 +320,10 @@ http_transport_shutdown(uint64_t timeout, void *transport_state) sentry__retry_shutdown(state->retry); - if (state->cancel_client) { - state->cancel_client(state->client); - } - - int rv = sentry__bgworker_shutdown(bgworker, timeout); + int rv = sentry__bgworker_shutdown_cb( + bgworker, timeout, http_transport_shutdown_timeout, state); if (rv != 0) { sentry__retry_dump_queue(state->retry, http_send_task); - if (state->shutdown_client) { - state->shutdown_client(state->client); - } } return rv; } @@ -419,13 +421,6 @@ sentry__http_transport_set_start_client(sentry_transport_t *transport, http_transport_get_state(transport)->start_client = start_client; } -void -sentry__http_transport_set_cancel_client( - sentry_transport_t *transport, void (*cancel_client)(void *)) -{ - http_transport_get_state(transport)->cancel_client = cancel_client; -} - void sentry__http_transport_set_shutdown_client( sentry_transport_t *transport, void (*shutdown_client)(void *)) diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index b186b9eeb..30493d564 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -47,8 +47,6 @@ void sentry__http_transport_set_free_client( sentry_transport_t *transport, void (*free_client)(void *)); void sentry__http_transport_set_start_client(sentry_transport_t *transport, int (*start_client)(void *, const sentry_options_t *)); -void sentry__http_transport_set_cancel_client( - sentry_transport_t *transport, void (*cancel_client)(void *)); void sentry__http_transport_set_shutdown_client( sentry_transport_t *transport, void (*shutdown_client)(void *)); diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index dcd6d3c98..6783cdf65 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -140,16 +140,6 @@ winhttp_client_start(void *_client, const sentry_options_t *opts) return 0; } -static void -winhttp_client_cancel(void *_client) -{ - winhttp_client_t *client = _client; - HINTERNET request; - if ((request = InterlockedExchangePointer(&client->request, NULL))) { - WinHttpCloseHandle(request); - } -} - static void winhttp_client_shutdown(void *_client) { @@ -342,7 +332,6 @@ sentry__transport_new_default(void) } sentry__http_transport_set_free_client(transport, winhttp_client_free); sentry__http_transport_set_start_client(transport, winhttp_client_start); - sentry__http_transport_set_cancel_client(transport, winhttp_client_cancel); sentry__http_transport_set_shutdown_client( transport, winhttp_client_shutdown); return transport; From 7db327459c41241f97c8a66c2493a14b52bcf362 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 17:53:56 +0100 Subject: [PATCH 088/121] fix(winhttp): remove unnecessary local request snapshot in winhttp_send_task The local `HINTERNET request = client->request` snapshot was only needed for the cancel_client approach. Since shutdown_client only fires at the timeout point, mid-function reads of client->request are safe. Keep only the InterlockedExchangePointer in the exit block to prevent double-close. Co-Authored-By: Claude Opus 4.6 --- .../sentry_http_transport_winhttp.c | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index 6783cdf65..f358fa6cb 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -157,8 +157,8 @@ winhttp_client_shutdown(void *_client) WinHttpCloseHandle(client->session); client->session = NULL; } - HINTERNET request; - if ((request = InterlockedExchangePointer(&client->request, NULL))) { + HINTERNET request = InterlockedExchangePointer(&client->request, NULL); + if (request) { WinHttpCloseHandle(request); } } @@ -208,8 +208,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, client->request = WinHttpOpenRequest(client->connect, L"POST", url_components.lpszUrlPath, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, is_secure ? WINHTTP_FLAG_SECURE : 0); - HINTERNET request = client->request; - if (!request) { + if (!client->request) { SENTRY_WARNF( "`WinHttpOpenRequest` failed with code `%d`", GetLastError()); goto exit; @@ -240,22 +239,22 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, } if (client->proxy_username && client->proxy_password) { - WinHttpSetCredentials(request, WINHTTP_AUTH_TARGET_PROXY, + WinHttpSetCredentials(client->request, WINHTTP_AUTH_TARGET_PROXY, WINHTTP_AUTH_SCHEME_BASIC, client->proxy_username, client->proxy_password, 0); } - if ((result - = WinHttpSendRequest(request, headers, (DWORD)-1, (LPVOID)req->body, - (DWORD)req->body_len, (DWORD)req->body_len, 0))) { - WinHttpReceiveResponse(request, NULL); + if ((result = WinHttpSendRequest(client->request, headers, (DWORD)-1, + (LPVOID)req->body, (DWORD)req->body_len, (DWORD)req->body_len, + 0))) { + WinHttpReceiveResponse(client->request, NULL); if (client->debug) { // this is basically the example from: // https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-winhttpqueryheaders#examples DWORD dwSize = 0; LPVOID lpOutBuffer = NULL; - WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, + WinHttpQueryHeaders(client->request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, WINHTTP_NO_HEADER_INDEX); @@ -265,7 +264,7 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, // Now, use WinHttpQueryHeaders to retrieve the header. if (lpOutBuffer - && WinHttpQueryHeaders(request, + && WinHttpQueryHeaders(client->request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX, lpOutBuffer, &dwSize, WINHTTP_NO_HEADER_INDEX)) { @@ -283,17 +282,17 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, DWORD status_code = 0; DWORD status_code_size = sizeof(status_code); - WinHttpQueryHeaders(request, + WinHttpQueryHeaders(client->request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX); resp->status_code = (int)status_code; - if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, + if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, L"x-sentry-rate-limits", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { resp->x_sentry_rate_limits = sentry__string_from_wstr(buf); - } else if (WinHttpQueryHeaders(request, WINHTTP_QUERY_CUSTOM, + } else if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, L"retry-after", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { resp->retry_after = sentry__string_from_wstr(buf); @@ -306,8 +305,9 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, uint64_t now = sentry__monotonic_time(); SENTRY_DEBUGF("request handled in %llums", now - started); -exit: - if ((request = InterlockedExchangePointer(&client->request, NULL))) { +exit:; + HINTERNET request = InterlockedExchangePointer(&client->request, NULL); + if (request) { WinHttpCloseHandle(request); } sentry_free(url); From 5b3277993de1091cc8907a3eb26bdfb74bc5482b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 18:01:02 +0100 Subject: [PATCH 089/121] fix(sync): don't force running=0 before on_timeout callback Move sentry__atomic_store(&bgw->running, 0) from before the on_timeout callback to the else branch (detach path). This lets the worker's shutdown_task set running=0 naturally after finishing in-flight work, making the dump_queue safety-net reachable if the callback fails to unblock the worker within another 250ms cycle. Co-Authored-By: Claude Opus 4.6 --- src/sentry_sync.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 350235a7a..8e9067b99 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -442,7 +442,6 @@ sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, while (true) { uint64_t now = sentry__monotonic_time(); if (now > started && now - started > timeout) { - sentry__atomic_store(&bgw->running, 0); if (on_timeout) { // Unblock the worker (e.g. close transport handles) and // let it finish in-flight work like handle_result. @@ -452,6 +451,7 @@ sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, sentry__mutex_lock(&bgw->task_lock); // fall through to !running check below } else { + sentry__atomic_store(&bgw->running, 0); sentry__thread_detach(bgw->thread_id); sentry__mutex_unlock(&bgw->task_lock); SENTRY_WARN("background thread failed to shut down cleanly " From 9c8d803dd26b293f8a283e36221ae0f295f5f519 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 18:50:13 +0100 Subject: [PATCH 090/121] fix(test): disable transport retry in unit tests to fix valgrind flakiness Tests that directly call sentry__retry_send were racing with the transport's background retry worker polling the same cache directory. Co-Authored-By: Claude Opus 4.6 --- tests/unit/test_retry.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 94c4f638a..d707dd162 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -118,7 +118,7 @@ SENTRY_TEST(retry_throttle) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -150,7 +150,7 @@ SENTRY_TEST(retry_skew) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -178,7 +178,7 @@ SENTRY_TEST(retry_result) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -246,7 +246,7 @@ SENTRY_TEST(retry_session) SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_release(options, "test@1.0.0"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -274,7 +274,7 @@ SENTRY_TEST(retry_cache) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_options_set_cache_keep(options, 1); sentry_init(options); @@ -365,7 +365,7 @@ SENTRY_TEST(retry_backoff) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); @@ -424,7 +424,7 @@ SENTRY_TEST(retry_trigger) { SENTRY_TEST_OPTIONS_NEW(options); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); - sentry_options_set_http_retry(options, true); + sentry_options_set_http_retry(options, false); sentry_init(options); sentry_retry_t *retry = sentry__retry_new(options); From a91842a48b61fb470e05bf51ce114ad66d1cb206 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 26 Feb 2026 19:23:21 +0100 Subject: [PATCH 091/121] fix(retry): clear sealed_envelope after match to prevent address-reuse data loss After retry_enqueue writes an envelope and stores its address in sealed_envelope, the envelope is freed when the bgworker task completes. If a subsequent envelope is allocated at the same address and is still pending during shutdown, retry_dump_cb would incorrectly skip it, losing the envelope. Clear sealed_envelope after the first match so later iterations cannot false-match a reused address. Co-Authored-By: Claude Opus 4.6 --- src/sentry_retry.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 74502d2a4..6e2509a34 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -303,6 +303,8 @@ retry_dump_cb(void *_envelope, void *_retry) sentry_envelope_t *envelope = (sentry_envelope_t *)_envelope; if ((uintptr_t)envelope != retry->sealed_envelope) { sentry__run_write_cache(retry->run, envelope, 0); + } else { + retry->sealed_envelope = 0; } return true; } From c17329ad1cbd353feeed62d121640d0c5ebe1e88 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Mar 2026 14:38:56 +0100 Subject: [PATCH 092/121] ref: consolidate filename formatting into make_cache_path Co-Authored-By: Claude Opus 4.6 --- src/sentry_database.c | 18 ++++++++---------- src/sentry_database.h | 4 +++- tests/unit/test_retry.c | 28 ++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index a7114e3bc..e0826ceba 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -235,8 +235,12 @@ sentry__run_make_cache_path( const sentry_run_t *run, uint64_t ts, int count, const char *uuid) { char filename[128]; - snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", ts, - count, uuid); + if (count >= 0) { + snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", + ts, count, uuid); + } else { + snprintf(filename, sizeof(filename), "%.36s.envelope", uuid); + } return sentry__path_join_str(run->cache_path, filename); } @@ -249,7 +253,6 @@ sentry__run_move_cache( return false; } - char filename[128]; const char *src_name = sentry__path_filename(src); uint64_t parsed_ts; int parsed_count; @@ -258,14 +261,9 @@ sentry__run_move_cache( &parsed_count, &parsed_uuid) ? parsed_uuid : src_name; - if (retry_count >= 0) { - snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope", - sentry__usec_time() / 1000, retry_count, cache_name); - } else { - snprintf(filename, sizeof(filename), "%s", cache_name); - } - sentry_path_t *dst_path = sentry__path_join_str(run->cache_path, filename); + sentry_path_t *dst_path = sentry__run_make_cache_path( + run, sentry__usec_time() / 1000, retry_count, cache_name); if (!dst_path) { return false; } diff --git a/src/sentry_database.h b/src/sentry_database.h index 0e355f482..33e6735ce 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -87,7 +87,9 @@ bool sentry__run_move_cache( const sentry_run_t *run, const sentry_path_t *src, int retry_count); /** - * Builds a cache path: `/cache/--.envelope`. + * Builds a cache path. When count >= 0 the result is + * `/cache/--.envelope`, otherwise + * `/cache/.envelope`. */ sentry_path_t *sentry__run_make_cache_path( const sentry_run_t *run, uint64_t ts, int count, const char *uuid); diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index d707dd162..80dadc153 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -114,6 +114,34 @@ SENTRY_TEST(retry_filename) "123-00-abcdefab-1234-5678-9abc-def012345678.txt", &ts, &count, &uuid)); } +SENTRY_TEST(retry_make_cache_path) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#endif + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_http_retry(options, false); + sentry_init(options); + + const char *uuid = "abcdefab-1234-5678-9abc-def012345678"; + + // count >= 0 → retry format + sentry_path_t *path + = sentry__run_make_cache_path(options->run, 1000, 2, uuid); + TEST_CHECK_STRING_EQUAL(sentry__path_filename(path), + "1000-02-abcdefab-1234-5678-9abc-def012345678.envelope"); + sentry__path_free(path); + + // count < 0 → cache format + path = sentry__run_make_cache_path(options->run, 0, -1, uuid); + TEST_CHECK_STRING_EQUAL(sentry__path_filename(path), + "abcdefab-1234-5678-9abc-def012345678.envelope"); + sentry__path_free(path); + + sentry_close(); +} + SENTRY_TEST(retry_throttle) { SENTRY_TEST_OPTIONS_NEW(options); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 2318ff8be..97269c1d3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -195,6 +195,7 @@ XX(recursive_paths) XX(retry_backoff) XX(retry_cache) XX(retry_filename) +XX(retry_make_cache_path) XX(retry_result) XX(retry_session) XX(retry_skew) From 3461f52462c303c72d8cb3f79df6b8628ccfa75d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 9 Mar 2026 17:23:06 +0100 Subject: [PATCH 093/121] Adapt to sentry__session_new() change --- tests/unit/test_retry.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 80dadc153..8d099a875 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -4,6 +4,7 @@ #include "sentry_options.h" #include "sentry_path.h" #include "sentry_retry.h" +#include "sentry_scope.h" #include "sentry_session.h" #include "sentry_testsupport.h" #include "sentry_transport.h" @@ -283,7 +284,10 @@ SENTRY_TEST(retry_session) sentry__path_remove_all(options->run->cache_path); sentry__path_create_dir_all(options->run->cache_path); - sentry_session_t *session = sentry__session_new(); + sentry_session_t *session = NULL; + SENTRY_WITH_SCOPE (scope) { + session = sentry__session_new(scope); + } TEST_ASSERT(!!session); sentry_envelope_t *envelope = sentry__envelope_new(); TEST_ASSERT(!!envelope); From 36568c19231cd33446d003bc9d0adba8356e8917 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 10 Mar 2026 20:02:48 +0100 Subject: [PATCH 094/121] ref: cache envelopes only on failed HTTP send Move cache_keep logic into http_send_task so envelopes are cached only when sending actually fails, rather than unconditionally in process_old_runs. For non-HTTP transports, process_old_runs still handles caching since there is no http_send_task. - Add cache_keep/run fields to http_transport_state_t - Cache in http_send_task when send fails and retry is unavailable - Simplify can_cache in process_old_runs to check transport capability - Fix NULL transport dereference in sentry__transport_flush - Update sentry_options_set_cache_keep/http_retry docs Co-Authored-By: Claude Opus 4.6 --- include/sentry.h | 12 +++++++----- src/sentry_database.c | 3 +-- src/sentry_transport.c | 2 +- src/transports/sentry_http_transport.c | 7 +++++++ tests/unit/test_cache.c | 1 + 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 00bc3cab4..a2dc3613b 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1494,12 +1494,11 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( const sentry_options_t *opts); /** - * Enables or disables storing envelopes in a persistent cache. + * Enables or disables storing failed envelopes in a persistent cache. * - * When enabled, envelopes are written to a `cache/` subdirectory within the - * database directory and retained regardless of send success or failure. - * The cache is cleared on startup based on the cache_max_items, cache_max_size, - * and cache_max_age options. + * When enabled, envelopes that fail to send are written to a `cache/` + * subdirectory within the database directory. The cache is cleared on startup + * based on the cache_max_items, cache_max_size, and cache_max_age options. * * Disabled by default. */ @@ -2278,6 +2277,9 @@ SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs( /** * Enables or disables HTTP retry with exponential backoff for network failures. + * + * Only applicable for HTTP transports. + * * Enabled by default. */ SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retry( diff --git a/src/sentry_database.c b/src/sentry_database.c index e0826ceba..23b69fd31 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -367,8 +367,7 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } bool can_cache = options->cache_keep - && (!options->http_retry - || !sentry__transport_can_retry(options->transport)); + && !sentry__transport_can_retry(options->transport); sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; diff --git a/src/sentry_transport.c b/src/sentry_transport.c index 1b81cb652..f8fb34f1f 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -93,7 +93,7 @@ sentry__transport_startup( int sentry__transport_flush(sentry_transport_t *transport, uint64_t timeout) { - if (transport->flush_func && transport->running) { + if (transport && transport->flush_func && transport->running) { SENTRY_DEBUG("flushing transport"); return transport->flush_func(timeout, transport->state); } diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 237052689..1df7b7c00 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -32,6 +32,8 @@ typedef struct { sentry_http_send_func_t send_func; void (*shutdown_client)(void *client); sentry_retry_t *retry; + bool cache_keep; + sentry_run_t *run; } http_transport_state_t; #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -244,6 +246,7 @@ http_transport_state_free(void *_state) sentry_free(state->user_agent); sentry__rate_limiter_free(state->ratelimiter); sentry__retry_free(state->retry); + sentry__run_free(state->run); sentry_free(state); } @@ -256,6 +259,8 @@ http_send_task(void *_envelope, void *_state) int status_code = http_send_envelope(state, envelope); if (status_code < 0 && state->retry) { sentry__retry_enqueue(state->retry, envelope); + } else if (status_code < 0 && state->cache_keep) { + sentry__run_write_cache(state->run, envelope, -1); } } @@ -278,6 +283,8 @@ http_transport_start(const sentry_options_t *options, void *transport_state) state->dsn = sentry__dsn_incref(options->dsn); state->user_agent = sentry__string_clone(options->user_agent); + state->cache_keep = options->cache_keep; + state->run = sentry__run_incref(options->run); if (state->start_client) { int rv = state->start_client(state->client, options); diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index f49637bc4..7c31a290a 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -81,6 +81,7 @@ SENTRY_TEST(cache_keep) TEST_ASSERT(!sentry__path_is_file(cached_envelope_path)); sentry__process_old_runs(options, 0); + sentry_flush(5000); TEST_ASSERT(!sentry__path_is_file(old_envelope_path)); TEST_ASSERT(sentry__path_is_file(cached_envelope_path)); From 1d85dd8272bc538dc41784503a15d60bb37350fe Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 10 Mar 2026 21:06:34 +0100 Subject: [PATCH 095/121] Clarify bgworker shutdown timeout comment Co-Authored-By: Claude Opus 4.6 --- src/sentry_sync.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sentry_sync.c b/src/sentry_sync.c index 8e9067b99..bf6c9a9d8 100644 --- a/src/sentry_sync.c +++ b/src/sentry_sync.c @@ -443,8 +443,9 @@ sentry__bgworker_shutdown_cb(sentry_bgworker_t *bgw, uint64_t timeout, uint64_t now = sentry__monotonic_time(); if (now > started && now - started > timeout) { if (on_timeout) { - // Unblock the worker (e.g. close transport handles) and - // let it finish in-flight work like handle_result. + // fire on_timeout to cancel the ongoing task, and give the + // worker an extra loop cycle up to 250ms to handle the + // cancellation sentry__mutex_unlock(&bgw->task_lock); on_timeout(on_timeout_data); on_timeout = NULL; From 168a807c3192572feddd02331752daa886fe9c3c Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 14:50:15 +0100 Subject: [PATCH 096/121] Cache envelopes only on failed HTTP send Move envelope caching from process_old_runs (synchronous, before send) into http_send_task (on HTTP failure). This ensures envelopes are only cached when the send actually fails, rather than unconditionally. Add transport cleanup_func to submit cache cleanup as a FIFO task on the bg worker, guaranteeing it runs after pending send tasks from process_old_runs. Add explicit flush argument to sentry_example for deterministic test behavior across platforms with varying HTTP timeout characteristics. Co-Authored-By: Claude Opus 4.6 --- examples/example.c | 4 ++ include/sentry.h | 2 + src/sentry_core.c | 4 +- src/sentry_database.c | 8 --- src/sentry_transport.c | 19 ++++++ src/sentry_transport.h | 18 +++++ src/transports/sentry_http_transport.c | 19 ++++++ tests/test_integration_cache.py | 94 ++++++++++++++++---------- tests/test_unit.py | 4 +- tests/unit/test_cache.c | 1 + 10 files changed, 128 insertions(+), 45 deletions(-) diff --git a/examples/example.c b/examples/example.c index 652bdc29f..5111e47e6 100644 --- a/examples/example.c +++ b/examples/example.c @@ -943,6 +943,10 @@ main(int argc, char **argv) sentry_reinstall_backend(); } + if (has_arg(argc, argv, "flush")) { + sentry_flush(10000); + } + if (has_arg(argc, argv, "sleep")) { sleep_s(10); } diff --git a/include/sentry.h b/include/sentry.h index a2dc3613b..f20c8848d 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1500,6 +1500,8 @@ SENTRY_API int sentry_options_get_symbolize_stacktraces( * subdirectory within the database directory. The cache is cleared on startup * based on the cache_max_items, cache_max_size, and cache_max_age options. * + * Only applicable for HTTP transports. + * * Disabled by default. */ SENTRY_API void sentry_options_set_cache_keep( diff --git a/src/sentry_core.c b/src/sentry_core.c index 14dba4a2d..4880bd1a1 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -293,7 +293,9 @@ sentry_init(sentry_options_t *options) } if (options->cache_keep || options->http_retry) { - sentry__cleanup_cache(options); + if (!sentry__transport_submit_cleanup(options->transport, options)) { + sentry__cleanup_cache(options); + } } if (options->auto_session_tracking) { diff --git a/src/sentry_database.c b/src/sentry_database.c index 23b69fd31..05abb173b 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -366,9 +366,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) continue; } - bool can_cache = options->cache_keep - && !sentry__transport_can_retry(options->transport); - sentry_pathiter_t *run_iter = sentry__path_iter_directory(run_dir); const sentry_path_t *file; while (run_iter && (file = sentry__pathiter_next(run_iter)) != NULL) { @@ -413,11 +410,6 @@ sentry__process_old_runs(const sentry_options_t *options, uint64_t last_crash) } else if (sentry__path_ends_with(file, ".envelope")) { sentry_envelope_t *envelope = sentry__envelope_from_path(file); sentry__capture_envelope(options->transport, envelope); - - if (can_cache - && sentry__run_move_cache(options->run, file, -1)) { - continue; - } } sentry__path_remove(file); diff --git a/src/sentry_transport.c b/src/sentry_transport.c index f8fb34f1f..94ac6d906 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -11,6 +11,7 @@ struct sentry_transport_s { void (*free_func)(void *state); size_t (*dump_func)(sentry_run_t *run, void *state); void (*retry_func)(void *state); + void (*cleanup_func)(const sentry_options_t *options, void *state); void *state; bool running; }; @@ -169,3 +170,21 @@ sentry__transport_can_retry(sentry_transport_t *transport) { return transport && transport->retry_func; } + +void +sentry__transport_set_cleanup_func(sentry_transport_t *transport, + void (*cleanup_func)(const sentry_options_t *options, void *state)) +{ + transport->cleanup_func = cleanup_func; +} + +bool +sentry__transport_submit_cleanup( + sentry_transport_t *transport, const sentry_options_t *options) +{ + if (transport && transport->cleanup_func && transport->running) { + transport->cleanup_func(options, transport->state); + return true; + } + return false; +} diff --git a/src/sentry_transport.h b/src/sentry_transport.h index 5ed1e7b81..17427600d 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -61,4 +61,22 @@ void sentry__transport_set_retry_func( sentry_transport_t *transport, void (*retry_func)(void *state)); bool sentry__transport_can_retry(sentry_transport_t *transport); +/** + * Sets the cleanup function of the transport. + * + * This function submits cache cleanup as a task on the transport's background + * worker, so it runs after any pending send tasks from process_old_runs. + */ +void sentry__transport_set_cleanup_func(sentry_transport_t *transport, + void (*cleanup_func)(const sentry_options_t *options, void *state)); + +/** + * Submits cache cleanup to the transport's background worker. + * + * Returns true if cleanup was submitted, false if the transport does not + * support async cleanup (caller should run cleanup synchronously). + */ +bool sentry__transport_submit_cleanup( + sentry_transport_t *transport, const sentry_options_t *options); + #endif diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 1df7b7c00..257b4feff 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -376,6 +376,23 @@ http_transport_retry(void *transport_state) } } +static void +http_cleanup_cache_task(void *task_data, void *_state) +{ + (void)_state; + const sentry_options_t *options = task_data; + sentry__cleanup_cache(options); +} + +static void +http_transport_submit_cleanup( + const sentry_options_t *options, void *transport_state) +{ + sentry_bgworker_t *bgworker = transport_state; + sentry__bgworker_submit( + bgworker, http_cleanup_cache_task, NULL, (void *)options); +} + sentry_transport_t * sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) { @@ -410,6 +427,8 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) sentry_transport_set_shutdown_func(transport, http_transport_shutdown); sentry__transport_set_dump_func(transport, http_dump_queue); sentry__transport_set_retry_func(transport, http_transport_retry); + sentry__transport_set_cleanup_func( + transport, http_transport_submit_cleanup); return transport; } diff --git a/tests/test_integration_cache.py b/tests/test_integration_cache.py index 6ba9fd225..f704e302f 100644 --- a/tests/test_integration_cache.py +++ b/tests/test_integration_cache.py @@ -3,9 +3,14 @@ import pytest from . import run -from .conditions import has_breakpad, has_files +from .conditions import has_breakpad, has_files, has_http -pytestmark = pytest.mark.skipif(not has_files, reason="tests need local filesystem") +pytestmark = [ + pytest.mark.skipif(not has_files, reason="tests need local filesystem"), + pytest.mark.skipif(not has_http, reason="tests need http transport"), +] + +unreachable_dsn = "http://uiaeosnrtdy@127.0.0.1:19999/123456" @pytest.mark.parametrize("cache_keep", [True, False]) @@ -22,10 +27,9 @@ ], ) def test_cache_keep(cmake, backend, cache_keep): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) # capture run( @@ -33,6 +37,7 @@ def test_cache_keep(cmake, backend, cache_keep): "sentry_example", ["log", "crash"] + (["cache-keep"] if cache_keep else []), expect_failure=True, + env=env, ) assert not cache_dir.exists() or len(list(cache_dir.glob("*.envelope"))) == 0 @@ -42,6 +47,7 @@ def test_cache_keep(cmake, backend, cache_keep): tmp_path, "sentry_example", ["log", "no-setup"] + (["cache-keep"] if cache_keep else []), + env=env, ) assert cache_dir.exists() or cache_keep is False @@ -63,34 +69,42 @@ def test_cache_keep(cmake, backend, cache_keep): ], ) def test_cache_max_size(cmake, backend): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) - # 5 x 2mb for i in range(5): run( tmp_path, "sentry_example", ["log", "cache-keep", "crash"], expect_failure=True, + env=env, ) - if cache_dir.exists(): - cache_files = list(cache_dir.glob("*.envelope")) - for f in cache_files: - with open(f, "r+b") as file: - file.truncate(2 * 1024 * 1024) - + # flush + cache run( tmp_path, "sentry_example", - ["log", "cache-keep", "no-setup"], + ["log", "no-http-retry", "cache-keep", "flush", "no-setup"], + env=env, ) - # max 4mb + # 5 x 2mb assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + for f in cache_files: + with open(f, "r+b") as file: + file.truncate(2 * 1024 * 1024) + + # max 4mb + run( + tmp_path, + "sentry_example", + ["log", "no-http-retry", "cache-keep", "no-setup"], + env=env, + ) + cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) <= 2 assert sum(f.stat().st_size for f in cache_files) <= 4 * 1024 * 1024 @@ -109,19 +123,27 @@ def test_cache_max_size(cmake, backend): ], ) def test_cache_max_age(cmake, backend): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) for i in range(5): run( tmp_path, "sentry_example", - ["log", "cache-keep", "crash"], + ["log", "no-http-retry", "cache-keep", "crash"], expect_failure=True, + env=env, ) + # flush + cache + run( + tmp_path, + "sentry_example", + ["log", "no-http-retry", "cache-keep", "flush", "no-setup"], + env=env, + ) + # 2,4,6,8,10 days old assert cache_dir.exists() cache_files = list(cache_dir.glob("*.envelope")) @@ -129,16 +151,16 @@ def test_cache_max_age(cmake, backend): mtime = time.time() - ((i + 1) * 2 * 24 * 60 * 60) os.utime(str(f), (mtime, mtime)) - # 0 days old + # cleanup (max 5 days) run( tmp_path, "sentry_example", - ["log", "cache-keep", "no-setup"], + ["log", "no-http-retry", "cache-keep", "no-setup"], + env=env, ) - # max 5 days cache_files = list(cache_dir.glob("*.envelope")) - assert len(cache_files) == 3 + assert len(cache_files) == 2 for f in cache_files: assert time.time() - f.stat().st_mtime <= 5 * 24 * 60 * 60 @@ -156,10 +178,9 @@ def test_cache_max_age(cmake, backend): ], ) def test_cache_max_items(cmake, backend): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) for i in range(6): run( @@ -167,12 +188,15 @@ def test_cache_max_items(cmake, backend): "sentry_example", ["log", "cache-keep", "crash"], expect_failure=True, + env=env, ) + # flush + cache run( tmp_path, "sentry_example", - ["log", "cache-keep", "no-setup"], + ["log", "cache-keep", "flush", "no-setup"], + env=env, ) # max 5 items @@ -194,10 +218,9 @@ def test_cache_max_items(cmake, backend): ], ) def test_cache_max_items_with_retry(cmake, backend): - tmp_path = cmake( - ["sentry_example"], {"SENTRY_BACKEND": backend, "SENTRY_TRANSPORT": "none"} - ) + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": backend}) cache_dir = tmp_path.joinpath(".sentry-native/cache") + env = dict(os.environ, SENTRY_DSN=unreachable_dsn) # Create cache files via crash+restart cycles for i in range(4): @@ -206,13 +229,15 @@ def test_cache_max_items_with_retry(cmake, backend): "sentry_example", ["log", "cache-keep", "crash"], expect_failure=True, + env=env, ) - # Move envelopes into cache + # flush + cache run( tmp_path, "sentry_example", - ["log", "cache-keep", "no-setup"], + ["log", "cache-keep", "flush", "no-setup"], + env=env, ) # Pre-populate cache/ with retry-format envelope files @@ -227,6 +252,7 @@ def test_cache_max_items_with_retry(cmake, backend): tmp_path, "sentry_example", ["log", "cache-keep", "no-setup"], + env=env, ) # max 5 items total in cache/ diff --git a/tests/test_unit.py b/tests/test_unit.py index c38fffb91..daa571257 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -5,7 +5,7 @@ def test_unit(cmake, unittest): - if unittest in ["basic_transport_thread_name"]: + if unittest in ["basic_transport_thread_name", "cache_keep"]: pytest.skip("excluded from unit test-suite") cwd = cmake( ["sentry_test_unit"], @@ -33,7 +33,7 @@ def test_unit_transport(cmake, unittest): def test_unit_with_test_path(cmake, unittest): - if unittest in ["basic_transport_thread_name"]: + if unittest in ["basic_transport_thread_name", "cache_keep"]: pytest.skip("excluded from unit test-suite") cwd = cmake( ["sentry_test_unit"], diff --git a/tests/unit/test_cache.c b/tests/unit/test_cache.c index 7c31a290a..9cb4e045f 100644 --- a/tests/unit/test_cache.c +++ b/tests/unit/test_cache.c @@ -44,6 +44,7 @@ SENTRY_TEST(cache_keep) SKIP_TEST(); #endif SENTRY_TEST_OPTIONS_NEW(options); + TEST_ASSERT(!!options->transport); sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); sentry_options_set_cache_keep(options, true); sentry_options_set_http_retry(options, false); From c0f920d4026e5e476bc38d3495a6623485e364f7 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 15:27:52 +0100 Subject: [PATCH 097/121] Remove unused sentry__transport_can_retry Its only production use was the can_cache check in process_old_runs, which was removed in the previous commit. Co-Authored-By: Claude Opus 4.6 --- src/sentry_transport.c | 6 ------ src/sentry_transport.h | 1 - tests/unit/test_retry.c | 5 ++--- 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/sentry_transport.c b/src/sentry_transport.c index 94ac6d906..cc9542293 100644 --- a/src/sentry_transport.c +++ b/src/sentry_transport.c @@ -165,12 +165,6 @@ sentry__transport_set_retry_func( transport->retry_func = retry_func; } -bool -sentry__transport_can_retry(sentry_transport_t *transport) -{ - return transport && transport->retry_func; -} - void sentry__transport_set_cleanup_func(sentry_transport_t *transport, void (*cleanup_func)(const sentry_options_t *options, void *state)) diff --git a/src/sentry_transport.h b/src/sentry_transport.h index 17427600d..a4ce8197e 100644 --- a/src/sentry_transport.h +++ b/src/sentry_transport.h @@ -59,7 +59,6 @@ void *sentry__transport_get_state(sentry_transport_t *transport); void sentry__transport_set_retry_func( sentry_transport_t *transport, void (*retry_func)(void *state)); -bool sentry__transport_can_retry(sentry_transport_t *transport); /** * Sets the cleanup function of the transport. diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 8d099a875..118795d78 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -376,13 +376,12 @@ SENTRY_TEST(transport_retry) { // no retry_func → no-op sentry_transport_t *transport = sentry_transport_new(noop_send); - TEST_CHECK(!sentry__transport_can_retry(transport)); + retry_func_calls = 0; sentry_transport_retry(transport); + TEST_CHECK_INT_EQUAL(retry_func_calls, 0); // with retry_func → calls it - retry_func_calls = 0; sentry__transport_set_retry_func(transport, mock_retry_func); - TEST_CHECK(sentry__transport_can_retry(transport)); sentry_transport_retry(transport); TEST_CHECK_INT_EQUAL(retry_func_calls, 1); From bb06d6f2cc37a45856447a72b03aa4f30048c261 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 11 Mar 2026 16:27:45 +0100 Subject: [PATCH 098/121] fix: incref options in cleanup task to prevent use-after-free When bgworker_shutdown_cb times out, the thread is detached and sentry_close frees options while the cleanup task may still be queued. Incref options when submitting and use sentry_options_free as the task cleanup function to ensure safe lifetime. Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 257b4feff..aa18eec18 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -380,7 +380,7 @@ static void http_cleanup_cache_task(void *task_data, void *_state) { (void)_state; - const sentry_options_t *options = task_data; + sentry_options_t *options = task_data; sentry__cleanup_cache(options); } @@ -389,8 +389,9 @@ http_transport_submit_cleanup( const sentry_options_t *options, void *transport_state) { sentry_bgworker_t *bgworker = transport_state; - sentry__bgworker_submit( - bgworker, http_cleanup_cache_task, NULL, (void *)options); + sentry__bgworker_submit(bgworker, http_cleanup_cache_task, + (void (*)(void *))sentry_options_free, + sentry__options_incref((sentry_options_t *)options)); } sentry_transport_t * From 884044451e2eafc73374054d2c9a54a422caf23a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Mar 2026 15:23:37 +0100 Subject: [PATCH 099/121] Update sentry docs URL for network failure handling --- src/sentry_retry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 6e2509a34..d97063ac6 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -96,7 +96,7 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { // Only network failures (status_code < 0) trigger retries. HTTP responses // including 5xx (500, 502, 503, 504) are discarded: - // https://develop.sentry.dev/sdk/expected-features/#dealing-with-network-failures + // https://develop.sentry.dev/sdk/foundations/transport/offline-caching/#dealing-with-network-failures // network failure with retries remaining: bump count & re-enqueue if (item->count + 1 < SENTRY_RETRY_ATTEMPTS && status_code < 0) { From 93d884a04aa201d4d084dd2a38ea4791ce157387 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Mar 2026 16:36:52 +0100 Subject: [PATCH 100/121] Replace pointer comparison with envelope tag in retry seal check Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_envelope.c | 21 +++++++++++++++++++++ src/sentry_envelope.h | 5 +++++ src/sentry_retry.c | 8 ++++---- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 27a836765..3656b0252 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -7,6 +7,7 @@ #include "sentry_ratelimiter.h" #include "sentry_scope.h" #include "sentry_string.h" +#include "sentry_sync.h" #include "sentry_transport.h" #include "sentry_value.h" #include @@ -21,7 +22,20 @@ struct sentry_envelope_item_s { sentry_envelope_item_t *next; }; +static long +next_tag(void) +{ + static volatile long counter = 0; + long tag = sentry__atomic_fetch_and_add(&counter, 1); + if (tag == 0) { + // skip 0-sentinel on overflow + tag = sentry__atomic_fetch_and_add(&counter, 1); + } + return tag; +} + struct sentry_envelope_s { + long tag; bool is_raw; union { struct { @@ -199,6 +213,7 @@ sentry__envelope_new_with_dsn(const sentry_dsn_t *dsn) return NULL; } + rv->tag = next_tag(); rv->is_raw = false; rv->contents.items.first_item = NULL; rv->contents.items.last_item = NULL; @@ -236,6 +251,12 @@ sentry__envelope_from_path(const sentry_path_t *path) return envelope; } +long +sentry__envelope_get_tag(const sentry_envelope_t *envelope) +{ + return envelope->tag; +} + sentry_uuid_t sentry__envelope_get_event_id(const sentry_envelope_t *envelope) { diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index f0e9bbc73..b047bfc85 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -31,6 +31,11 @@ sentry_envelope_t *sentry__envelope_new_with_dsn(const sentry_dsn_t *dsn); */ sentry_envelope_t *sentry__envelope_from_path(const sentry_path_t *path); +/** + * Returns a unique non-zero tag assigned at envelope creation. + */ +long sentry__envelope_get_tag(const sentry_envelope_t *envelope); + /** * This returns the UUID of the event associated with this envelope. * If there is no event inside this envelope, the empty nil UUID will be diff --git a/src/sentry_retry.c b/src/sentry_retry.c index d97063ac6..c9fe931de 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -35,7 +35,7 @@ struct sentry_retry_s { sentry_retry_send_func_t send_cb; void *send_data; sentry_mutex_t sealed_lock; - uintptr_t sealed_envelope; + long sealed_tag; }; sentry_retry_t * @@ -301,10 +301,10 @@ retry_dump_cb(void *_envelope, void *_retry) { sentry_retry_t *retry = (sentry_retry_t *)_retry; sentry_envelope_t *envelope = (sentry_envelope_t *)_envelope; - if ((uintptr_t)envelope != retry->sealed_envelope) { + if (sentry__envelope_get_tag(envelope) != retry->sealed_tag) { sentry__run_write_cache(retry->run, envelope, 0); } else { - retry->sealed_envelope = 0; + retry->sealed_tag = 0; } return true; } @@ -350,7 +350,7 @@ sentry__retry_enqueue(sentry_retry_t *retry, const sentry_envelope_t *envelope) sentry__mutex_unlock(&retry->sealed_lock); return; } - retry->sealed_envelope = (uintptr_t)envelope; + retry->sealed_tag = sentry__envelope_get_tag(envelope); sentry__mutex_unlock(&retry->sealed_lock); sentry__atomic_compare_swap( From 93db90035832209e4d881588db3ad307e8cd62b6 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Tue, 17 Mar 2026 16:41:11 +0100 Subject: [PATCH 101/121] Add TODO for retry jitter and shorter poll interval --- src/sentry_retry.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sentry_retry.c b/src/sentry_retry.c index c9fe931de..16321c676 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -67,6 +67,8 @@ sentry__retry_free(sentry_retry_t *retry) uint64_t sentry__retry_backoff(int count) { + // TODO: consider adding jitter and shortening the poll interval to spread + // out retries when multiple envelopes (esp. large attachments) pile up. return (uint64_t)SENTRY_RETRY_INTERVAL << MIN(MAX(count, 0), 5); } From 8f104d2ea96d518f2bef8b740225cbb0f627a04f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Mar 2026 16:08:58 +0000 Subject: [PATCH 102/121] Initialize tag for raw envelopes loaded from disk --- src/sentry_envelope.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 3656b0252..e69c05eba 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -244,6 +244,7 @@ sentry__envelope_from_path(const sentry_path_t *path) return NULL; } + envelope->tag = next_tag(); envelope->is_raw = true; envelope->contents.raw.payload = buf; envelope->contents.raw.payload_len = buf_len; From 878ace52f70360e15966ad663346e9326849157f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 18 Mar 2026 21:16:22 +0100 Subject: [PATCH 103/121] Implement on_timeout for curl transport Use a progress callback (CURLOPT_XFERINFOFUNCTION) that checks an atomic shutdown flag. When http_transport_shutdown_timeout fires, it sets the flag via curl_client_shutdown, causing curl_easy_perform to abort promptly. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/transports/sentry_http_transport_curl.c | 22 +++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index eec48f8e4..1954c8e5b 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -4,6 +4,7 @@ #include "sentry_http_transport.h" #include "sentry_options.h" #include "sentry_string.h" +#include "sentry_sync.h" #include "sentry_transport.h" #include "sentry_utils.h" @@ -20,6 +21,7 @@ typedef struct { char *proxy; char *ca_certs; bool debug; + long shutdown; #ifdef SENTRY_PLATFORM_NX void *nx_state; #endif @@ -117,6 +119,22 @@ curl_client_start(void *_client, const sentry_options_t *options) return 0; } +static void +curl_client_shutdown(void *_client) +{ + curl_client_t *client = _client; + sentry__atomic_store(&client->shutdown, 1); +} + +static int +progress_callback(void *clientp, curl_off_t UNUSED(dltotal), + curl_off_t UNUSED(dlnow), curl_off_t UNUSED(ultotal), + curl_off_t UNUSED(ulnow)) +{ + curl_client_t *client = clientp; + return sentry__atomic_fetch(&client->shutdown) ? 1 : 0; +} + static size_t swallow_data( char *UNUSED(ptr), size_t size, size_t nmemb, void *UNUSED(userdata)) @@ -190,6 +208,9 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 15000L); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); + curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); + curl_easy_setopt(curl, CURLOPT_XFERINFODATA, client); char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; @@ -254,5 +275,6 @@ sentry__transport_new_default(void) } sentry__http_transport_set_free_client(transport, curl_client_free); sentry__http_transport_set_start_client(transport, curl_client_start); + sentry__http_transport_set_shutdown_client(transport, curl_client_shutdown); return transport; } From b16281e9b5700c2b3747480be731aeb84f0b2ff2 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 20 Feb 2026 18:14:58 +0100 Subject: [PATCH 104/121] fix(curl): trim HTTP response header values Co-Authored-By: Claude Opus 4.6 --- src/transports/sentry_http_transport_curl.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 1954c8e5b..e56cd95a1 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -3,6 +3,7 @@ #include "sentry_envelope.h" #include "sentry_http_transport.h" #include "sentry_options.h" +#include "sentry_slice.h" #include "sentry_string.h" #include "sentry_sync.h" #include "sentry_transport.h" @@ -156,10 +157,12 @@ header_callback(char *buffer, size_t size, size_t nitems, void *userdata) if (sep) { *sep = '\0'; sentry__string_ascii_lower(header); + sentry_slice_t value + = sentry__slice_trim(sentry__slice_from_str(sep + 1)); if (sentry__string_eq(header, "retry-after")) { - info->retry_after = sentry__string_clone(sep + 1); + info->retry_after = sentry__slice_to_owned(value); } else if (sentry__string_eq(header, "x-sentry-rate-limits")) { - info->x_sentry_rate_limits = sentry__string_clone(sep + 1); + info->x_sentry_rate_limits = sentry__slice_to_owned(value); } } From f18a4a394b2905a6779c3974f27ca526af8e6100 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 21 Feb 2026 11:55:26 +0100 Subject: [PATCH 105/121] feat(path): add sentry__path_copy with unit test Co-Authored-By: Claude Opus 4.6 --- CMakeLists.txt | 8 +++++ src/path/sentry_path_unix.c | 66 ++++++++++++++++++++++++++++++++++ src/path/sentry_path_windows.c | 9 +++++ src/sentry_path.h | 6 ++++ tests/unit/test_path.c | 49 +++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 6 files changed, 139 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1799e7446..5a3ee3c3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -420,6 +420,14 @@ endif() include(CheckTypeSize) check_type_size("long" CMAKE_SIZEOF_LONG) +if(LINUX) + include(CheckSymbolExists) + check_symbol_exists(copy_file_range "unistd.h" HAVE_COPY_FILE_RANGE) + if(HAVE_COPY_FILE_RANGE) + target_compile_definitions(sentry PRIVATE SENTRY_HAVE_COPY_FILE_RANGE) + endif() +endif() + # https://gitlab.kitware.com/cmake/cmake/issues/18393 if(SENTRY_BUILD_SHARED_LIBS) if(APPLE) diff --git a/src/path/sentry_path_unix.c b/src/path/sentry_path_unix.c index 4b1069f1b..752025bf1 100644 --- a/src/path/sentry_path_unix.c +++ b/src/path/sentry_path_unix.c @@ -21,6 +21,7 @@ #include #ifdef SENTRY_PLATFORM_DARWIN +# include # include #endif @@ -333,6 +334,71 @@ sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst) return status == 0 ? 0 : 1; } +int +sentry__path_copy(const sentry_path_t *src, const sentry_path_t *dst) +{ +#ifdef SENTRY_PLATFORM_DARWIN + return copyfile(src->path, dst->path, NULL, COPYFILE_DATA) == 0 ? 0 : 1; +#else + int src_fd = open(src->path, O_RDONLY); + if (src_fd < 0) { + return 1; + } + int dst_fd = open(dst->path, O_WRONLY | O_CREAT | O_TRUNC, + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH); + if (dst_fd < 0) { + close(src_fd); + return 1; + } + + int rv = 0; + +# ifdef SENTRY_HAVE_COPY_FILE_RANGE + while (true) { + ssize_t n + = copy_file_range(src_fd, NULL, dst_fd, NULL, SIZE_MAX / 2, 0); + if (n > 0) { + continue; + } else if (n == 0) { + goto done; + } else if (errno == EAGAIN || errno == EINTR) { + continue; + } else if (errno == ENOSYS || errno == EXDEV || errno == EOPNOTSUPP + || errno == EINVAL) { + break; + } else { + rv = 1; + goto done; + } + } +# endif + + { + char buf[16384]; + while (true) { + ssize_t n = read(src_fd, buf, sizeof(buf)); + if (n < 0 && (errno == EAGAIN || errno == EINTR)) { + continue; + } else if (n <= 0) { + rv = n < 0; + break; + } + if (write_loop(dst_fd, buf, (size_t)n) != 0) { + rv = 1; + break; + } + } + } + +# ifdef SENTRY_HAVE_COPY_FILE_RANGE +done: +# endif + close(src_fd); + close(dst_fd); + return rv; +#endif +} + int sentry__path_create_dir_all(const sentry_path_t *path) { diff --git a/src/path/sentry_path_windows.c b/src/path/sentry_path_windows.c index 7f49e082a..5ccda3301 100644 --- a/src/path/sentry_path_windows.c +++ b/src/path/sentry_path_windows.c @@ -524,6 +524,15 @@ sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst) return MoveFileExW(src_w, dst_w, MOVEFILE_REPLACE_EXISTING) ? 0 : 1; } +int +sentry__path_copy(const sentry_path_t *src, const sentry_path_t *dst) +{ + if (!src->path_w || !dst->path_w) { + return 1; + } + return CopyFileW(src->path_w, dst->path_w, FALSE) ? 0 : 1; +} + int sentry__path_create_dir_all(const sentry_path_t *path) { diff --git a/src/sentry_path.h b/src/sentry_path.h index 484a6f531..40d7e331d 100644 --- a/src/sentry_path.h +++ b/src/sentry_path.h @@ -159,6 +159,12 @@ int sentry__path_remove_all(const sentry_path_t *path); */ int sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst); +/** + * Copy the file from `src` to `dst`. + * Returns 0 on success. + */ +int sentry__path_copy(const sentry_path_t *src, const sentry_path_t *dst); + /** * This will create the directory referred to by `path`, and any non-existing * parent directory. diff --git a/tests/unit/test_path.c b/tests/unit/test_path.c index 1013f6093..8ed526eeb 100644 --- a/tests/unit/test_path.c +++ b/tests/unit/test_path.c @@ -339,3 +339,52 @@ SENTRY_TEST(path_rename) sentry__path_free(dst); sentry__path_free(src); } + +SENTRY_TEST(path_copy) +{ +#if defined(SENTRY_PLATFORM_NX) + SKIP_TEST(); +#endif + sentry_path_t *src + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".copy-src"); + TEST_ASSERT(!!src); + sentry_path_t *dst + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".copy-dst"); + TEST_ASSERT(!!dst); + + // cleanup + sentry__path_remove_all(src); + sentry__path_remove_all(dst); + + // copy file with content preserved + sentry__path_write_buffer(src, "hello", 5); + TEST_CHECK(sentry__path_copy(src, dst) == 0); + TEST_CHECK(sentry__path_is_file(src)); + TEST_CHECK(sentry__path_is_file(dst)); + size_t len = 0; + char *buf = sentry__path_read_to_buffer(dst, &len); + TEST_ASSERT(!!buf); + TEST_CHECK(len == 5); + TEST_CHECK(memcmp(buf, "hello", 5) == 0); + sentry_free(buf); + sentry__path_remove(dst); + + // overwrite existing dst + sentry__path_write_buffer(dst, "dst-data", 8); + TEST_CHECK(sentry__path_copy(src, dst) == 0); + TEST_CHECK(sentry__path_is_file(src)); + buf = sentry__path_read_to_buffer(dst, &len); + TEST_ASSERT(!!buf); + TEST_CHECK(len == 5); + TEST_CHECK(memcmp(buf, "hello", 5) == 0); + sentry_free(buf); + sentry__path_remove(dst); + + // copy nonexistent src + sentry__path_remove(src); + TEST_CHECK(sentry__path_copy(src, dst) != 0); + + sentry__path_remove_all(dst); + sentry__path_free(dst); + sentry__path_free(src); +} diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 97269c1d3..d30dc50ea 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -172,6 +172,7 @@ XX(os_releases_snapshot) XX(overflow_spans) XX(page_allocator) XX(path_basics) +XX(path_copy) XX(path_current_exe) XX(path_directory) XX(path_from_str_n_wo_null_termination) From e0894ae6c2aabce6821697d9fa2c86162a77477b Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 4 Mar 2026 21:18:50 +0100 Subject: [PATCH 106/121] feat(path): add sentry__path_get_dir_size helper Co-Authored-By: Claude Opus 4.6 --- src/path/sentry_path.c | 13 +++++++++++++ src/sentry_path.h | 6 ++++++ tests/unit/test_path.c | 30 ++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 4 files changed, 50 insertions(+) diff --git a/src/path/sentry_path.c b/src/path/sentry_path.c index 4bdbe6234..3ef291745 100644 --- a/src/path/sentry_path.c +++ b/src/path/sentry_path.c @@ -79,6 +79,19 @@ sentry__path_remove_all(const sentry_path_t *path) return sentry__path_remove(path); } +size_t +sentry__path_get_dir_size(const sentry_path_t *path) +{ + size_t total = 0; + sentry_pathiter_t *iter = sentry__path_iter_directory(path); + const sentry_path_t *entry; + while (iter && (entry = sentry__pathiter_next(iter)) != NULL) { + total += sentry__path_get_size(entry); + } + sentry__pathiter_free(iter); + return total; +} + sentry_filelock_t * sentry__filelock_new(sentry_path_t *path) { diff --git a/src/sentry_path.h b/src/sentry_path.h index 40d7e331d..4ef1b1248 100644 --- a/src/sentry_path.h +++ b/src/sentry_path.h @@ -183,6 +183,12 @@ int sentry__path_touch(const sentry_path_t *path); */ size_t sentry__path_get_size(const sentry_path_t *path); +/** + * This will return the total size of all files in the directory at `path`, + * or 0 if the directory does not exist or is empty. + */ +size_t sentry__path_get_dir_size(const sentry_path_t *path); + /** * This will return the last modification time of the file at `path`, or 0 on * failure. diff --git a/tests/unit/test_path.c b/tests/unit/test_path.c index 8ed526eeb..a2e7989a1 100644 --- a/tests/unit/test_path.c +++ b/tests/unit/test_path.c @@ -340,6 +340,36 @@ SENTRY_TEST(path_rename) sentry__path_free(src); } +SENTRY_TEST(path_dir_size) +{ + sentry_path_t *dir + = sentry__path_from_str(SENTRY_TEST_PATH_PREFIX ".dir-size"); + TEST_ASSERT(!!dir); + + sentry__path_remove_all(dir); + + TEST_CHECK(sentry__path_get_dir_size(dir) == 0); + + sentry__path_create_dir_all(dir); + + sentry_path_t *file_a = sentry__path_join_str(dir, "a.txt"); + TEST_ASSERT(!!file_a); + sentry__path_write_buffer(file_a, "hello", 5); + + sentry_path_t *file_b = sentry__path_join_str(dir, "b.txt"); + TEST_ASSERT(!!file_b); + sentry__path_write_buffer(file_b, "world!", 6); + + TEST_CHECK(sentry__path_get_dir_size(dir) == 11); + + TEST_CHECK(sentry__path_get_dir_size(file_a) == 0); + + sentry__path_remove_all(dir); + sentry__path_free(file_b); + sentry__path_free(file_a); + sentry__path_free(dir); +} + SENTRY_TEST(path_copy) { #if defined(SENTRY_PLATFORM_NX) diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index d30dc50ea..34d49b316 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -174,6 +174,7 @@ XX(page_allocator) XX(path_basics) XX(path_copy) XX(path_current_exe) +XX(path_dir_size) XX(path_directory) XX(path_from_str_n_wo_null_termination) XX(path_from_str_null) From eca309e865f01e611dda5095cba5cf9da75be954 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 8 Mar 2026 11:41:50 +0100 Subject: [PATCH 107/121] feat(transport): add streaming file upload and TUS request preparation Add body_path to prepared HTTP requests for streaming file uploads from disk. Implement TUS protocol request formatting, upload URL construction, response cleanup helper, Location header parsing, and curl debug logging. Co-Authored-By: Claude Opus 4.6 --- src/sentry_utils.c | 12 +++ src/sentry_utils.h | 5 ++ src/transports/sentry_http_transport.c | 87 ++++++++++++++++++- src/transports/sentry_http_transport.h | 6 ++ src/transports/sentry_http_transport_curl.c | 87 ++++++++++++++++++- .../sentry_http_transport_winhttp.c | 51 +++++++++-- tests/unit/test_envelopes.c | 61 +++++++++++++ tests/unit/tests.inc | 2 + 8 files changed, 298 insertions(+), 13 deletions(-) diff --git a/src/sentry_utils.c b/src/sentry_utils.c index 6de8d80d3..40786242c 100644 --- a/src/sentry_utils.c +++ b/src/sentry_utils.c @@ -389,6 +389,18 @@ sentry__dsn_get_envelope_url(const sentry_dsn_t *dsn) return sentry__stringbuilder_into_string(&sb); } +char * +sentry__dsn_get_upload_url(const sentry_dsn_t *dsn) +{ + if (!dsn || !dsn->is_valid) { + return NULL; + } + sentry_stringbuilder_t sb; + init_string_builder_for_url(&sb, dsn); + sentry__stringbuilder_append(&sb, "/upload/"); + return sentry__stringbuilder_into_string(&sb); +} + char * sentry__dsn_get_minidump_url(const sentry_dsn_t *dsn, const char *user_agent) { diff --git a/src/sentry_utils.h b/src/sentry_utils.h index 75ea87d4c..6777ccd4b 100644 --- a/src/sentry_utils.h +++ b/src/sentry_utils.h @@ -110,6 +110,11 @@ char *sentry__dsn_get_auth_header( */ char *sentry__dsn_get_envelope_url(const sentry_dsn_t *dsn); +/** + * Returns the TUS upload endpoint url as a newly allocated string. + */ +char *sentry__dsn_get_upload_url(const sentry_dsn_t *dsn); + /** * Returns the minidump endpoint url used for uploads done by the out-of-process * crashpad backend as a newly allocated string. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index aa18eec18..572effd21 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -16,6 +16,8 @@ #include #define ENVELOPE_MIME "application/x-sentry-envelope" +#define TUS_MIME "application/offset+octet-stream" +#define TUS_MAX_HTTP_HEADERS 4 #ifdef SENTRY_TRANSPORT_COMPRESSION # define MAX_HTTP_HEADERS 4 #else @@ -141,6 +143,7 @@ sentry__prepare_http_request(sentry_envelope_t *envelope, req->method = "POST"; req->url = sentry__dsn_get_envelope_url(dsn); + req->body_path = NULL; sentry_prepared_http_header_t *h; h = &req->headers[req->headers_len++]; @@ -184,9 +187,80 @@ sentry__prepared_http_request_free(sentry_prepared_http_request_t *req) if (req->body_owned) { sentry_free(req->body); } + sentry__path_free(req->body_path); sentry_free(req); } +static sentry_prepared_http_request_t * +prepare_tus_request_common( + size_t upload_size, const sentry_dsn_t *dsn, const char *user_agent) +{ + if (!dsn || !dsn->is_valid) { + return NULL; + } + + sentry_prepared_http_request_t *req + = SENTRY_MAKE(sentry_prepared_http_request_t); + if (!req) { + return NULL; + } + memset(req, 0, sizeof(*req)); + + req->headers = sentry_malloc( + sizeof(sentry_prepared_http_header_t) * TUS_MAX_HTTP_HEADERS); + if (!req->headers) { + sentry_free(req); + return NULL; + } + req->headers_len = 0; + + req->method = "POST"; + req->url = sentry__dsn_get_upload_url(dsn); + + sentry_prepared_http_header_t *h; + h = &req->headers[req->headers_len++]; + h->key = "x-sentry-auth"; + h->value = sentry__dsn_get_auth_header(dsn, user_agent); + + h = &req->headers[req->headers_len++]; + h->key = "content-type"; + h->value = sentry__string_clone(TUS_MIME); + + h = &req->headers[req->headers_len++]; + h->key = "tus-resumable"; + h->value = sentry__string_clone("1.0.0"); + + h = &req->headers[req->headers_len++]; + h->key = "upload-length"; + h->value = sentry__uint64_to_string((uint64_t)upload_size); + + return req; +} + +static sentry_prepared_http_request_t * +prepare_tus_request(const sentry_path_t *path, size_t file_size, + const sentry_dsn_t *dsn, const char *user_agent) +{ + if (!path) { + return NULL; + } + sentry_prepared_http_request_t *req + = prepare_tus_request_common(file_size, dsn, user_agent); + if (req) { + req->body_path = sentry__path_clone(path); + req->body_len = file_size; + } + return req; +} + +static void +http_response_cleanup(sentry_http_response_t *resp) +{ + sentry_free(resp->retry_after); + sentry_free(resp->x_sentry_rate_limits); + sentry_free(resp->location); +} + static int http_send_request( http_transport_state_t *state, sentry_prepared_http_request_t *req) @@ -195,8 +269,7 @@ http_send_request( memset(&resp, 0, sizeof(resp)); if (!state->send_func(state->client, req, &resp)) { - sentry_free(resp.retry_after); - sentry_free(resp.x_sentry_rate_limits); + http_response_cleanup(&resp); return -1; } @@ -210,8 +283,7 @@ http_send_request( sentry__rate_limiter_update_from_429(state->ratelimiter); } - sentry_free(resp.retry_after); - sentry_free(resp.x_sentry_rate_limits); + http_response_cleanup(&resp); return resp.status_code; } @@ -461,4 +533,11 @@ sentry__http_transport_get_bgworker(sentry_transport_t *transport) { return sentry__transport_get_state(transport); } + +sentry_prepared_http_request_t * +sentry__prepare_tus_request(const sentry_path_t *path, size_t file_size, + const sentry_dsn_t *dsn, const char *user_agent) +{ + return prepare_tus_request(path, file_size, dsn, user_agent); +} #endif diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index 30493d564..c89204800 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -2,6 +2,7 @@ #define SENTRY_HTTP_TRANSPORT_H_INCLUDED #include "sentry_boot.h" +#include "sentry_path.h" #include "sentry_ratelimiter.h" #include "sentry_sync.h" #include "sentry_transport.h" @@ -19,6 +20,7 @@ typedef struct sentry_prepared_http_request_s { char *body; size_t body_len; bool body_owned; + sentry_path_t *body_path; } sentry_prepared_http_request_t; sentry_prepared_http_request_t *sentry__prepare_http_request( @@ -31,6 +33,7 @@ typedef struct { int status_code; char *retry_after; char *x_sentry_rate_limits; + char *location; } sentry_http_response_t; typedef bool (*sentry_http_send_func_t)(void *client, @@ -52,6 +55,9 @@ void sentry__http_transport_set_shutdown_client( #ifdef SENTRY_UNITTEST void *sentry__http_transport_get_bgworker(sentry_transport_t *transport); +sentry_prepared_http_request_t *sentry__prepare_tus_request( + const sentry_path_t *path, size_t file_size, const sentry_dsn_t *dsn, + const char *user_agent); #endif #endif diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index e56cd95a1..b9b414f65 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -11,6 +11,7 @@ #include #include +#include #include #ifdef SENTRY_PLATFORM_NX @@ -143,6 +144,57 @@ swallow_data( return size * nmemb; } +static int +debug_function(CURL *UNUSED(handle), curl_infotype type, char *data, + size_t size, void *UNUSED(userdata)) +{ + const char *prefix; + switch (type) { + case CURLINFO_TEXT: + prefix = "* "; + break; + case CURLINFO_HEADER_OUT: + prefix = "> "; + break; + case CURLINFO_HEADER_IN: + prefix = "< "; + break; + case CURLINFO_DATA_OUT: + case CURLINFO_DATA_IN: { + const char *dir = type == CURLINFO_DATA_OUT ? "Send" : "Recv"; + size_t len = size; + while (len > 0 && (data[len - 1] == '\n' || data[len - 1] == '\r')) { + len--; + } + if (len >= 2 && data[0] == '{' && data[len - 1] == '}') { + fprintf(stderr, "%s %s (%zu bytes): %.*s\n", + type == CURLINFO_DATA_OUT ? "=>" : "<=", dir, size, (int)len, + data); + } else { + fprintf(stderr, "%s %s (%zu bytes)\n", + type == CURLINFO_DATA_OUT ? "=>" : "<=", dir, size); + } + return 0; + } + default: + return 0; + } + + const char *pos = data; + const char *end = data + size; + while (pos < end) { + const char *eol = memchr(pos, '\n', (size_t)(end - pos)); + if (eol) { + fprintf(stderr, "%s%.*s\n", prefix, (int)(eol - pos), pos); + pos = eol + 1; + } else { + fprintf(stderr, "%s%.*s\n", prefix, (int)(end - pos), pos); + break; + } + } + return 0; +} + static size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) { @@ -163,6 +215,8 @@ header_callback(char *buffer, size_t size, size_t nitems, void *userdata) info->retry_after = sentry__slice_to_owned(value); } else if (sentry__string_eq(header, "x-sentry-rate-limits")) { info->x_sentry_rate_limits = sentry__slice_to_owned(value); + } else if (sentry__string_eq(header, "location")) { + info->location = sentry__slice_to_owned(value); } } @@ -170,6 +224,12 @@ header_callback(char *buffer, size_t size, size_t nitems, void *userdata) return bytes; } +static size_t +file_read_callback(char *buffer, size_t size, size_t nitems, void *userdata) +{ + return fread(buffer, size, nitems, (FILE *)userdata); +} + static bool curl_send_task(void *_client, sentry_prepared_http_request_t *req, sentry_http_response_t *resp) @@ -199,22 +259,40 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_reset(curl); if (client->debug) { curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_function); curl_easy_setopt(curl, CURLOPT_WRITEDATA, stderr); // CURLOPT_WRITEFUNCTION will `fwrite` by default } else { curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, swallow_data); } curl_easy_setopt(curl, CURLOPT_URL, req->url); - curl_easy_setopt(curl, CURLOPT_POST, (long)1); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); - curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); - curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); curl_easy_setopt(curl, CURLOPT_USERAGENT, SENTRY_SDK_USER_AGENT); curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, 15000L); curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, progress_callback); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, client); + FILE *body_file = NULL; + if (req->body_path) { + body_file = fopen(req->body_path->path, "rb"); + if (!body_file) { + SENTRY_WARN("failed to open body_path for upload"); + curl_slist_free_all(headers); + return false; + } + curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); + curl_easy_setopt(curl, CURLOPT_READFUNCTION, file_read_callback); + curl_easy_setopt(curl, CURLOPT_READDATA, body_file); + curl_easy_setopt( + curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)req->body_len); + } else { + curl_easy_setopt(curl, CURLOPT_POST, (long)1); + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); + curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); + } + char error_buf[CURL_ERROR_SIZE]; error_buf[0] = 0; curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error_buf); @@ -257,6 +335,9 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, } } + if (body_file) { + fclose(body_file); + } curl_slist_free_all(headers); return rv == CURLE_OK; } diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index f358fa6cb..559a3c016 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -244,9 +244,45 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, client->proxy_password, 0); } - if ((result = WinHttpSendRequest(client->request, headers, (DWORD)-1, - (LPVOID)req->body, (DWORD)req->body_len, (DWORD)req->body_len, - 0))) { + if (req->body_path) { + HANDLE hFile = CreateFileW(req->body_path->path_w, GENERIC_READ, + FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile == INVALID_HANDLE_VALUE) { + SENTRY_WARN("failed to open body_path for upload"); + goto exit; + } + + result = WinHttpSendRequest(client->request, headers, (DWORD)-1, NULL, + 0, (DWORD)req->body_len, 0); + if (result) { + char chunk[65536]; + DWORD bytes_read = 0; + while (ReadFile(hFile, chunk, sizeof(chunk), &bytes_read, NULL) + && bytes_read > 0) { + DWORD bytes_written = 0; + if (!WinHttpWriteData( + client->request, chunk, bytes_read, &bytes_written)) { + SENTRY_WARNF("`WinHttpWriteData` failed with code `%d`", + GetLastError()); + result = false; + break; + } + } + } else { + SENTRY_WARNF( + "`WinHttpSendRequest` failed with code `%d`", GetLastError()); + } + CloseHandle(hFile); + } else { + result = WinHttpSendRequest(client->request, headers, (DWORD)-1, + (LPVOID)req->body, (DWORD)req->body_len, (DWORD)req->body_len, 0); + if (!result) { + SENTRY_WARNF( + "`WinHttpSendRequest` failed with code `%d`", GetLastError()); + } + } + + if (result) { WinHttpReceiveResponse(client->request, NULL); if (client->debug) { @@ -297,9 +333,12 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, WINHTTP_NO_HEADER_INDEX)) { resp->retry_after = sentry__string_from_wstr(buf); } - } else { - SENTRY_WARNF( - "`WinHttpSendRequest` failed with code `%d`", GetLastError()); + + buf_size = sizeof(buf); + if (WinHttpQueryHeaders(client->request, WINHTTP_QUERY_CUSTOM, + L"location", buf, &buf_size, WINHTTP_NO_HEADER_INDEX)) { + resp->location = sentry__string_from_wstr(buf); + } } uint64_t now = sentry__monotonic_time(); diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index ba160a29e..1bd7ab7ae 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -725,6 +725,67 @@ SENTRY_TEST(deserialize_envelope_empty) SENTRY_TEST(deserialize_envelope_invalid) { TEST_CHECK(!sentry_envelope_deserialize("", 0)); + SENTRY_TEST(tus_upload_url) + { + SENTRY_TEST_DSN_NEW_DEFAULT(dsn); + + char *url = sentry__dsn_get_upload_url(dsn); + TEST_CHECK_STRING_EQUAL( + url, "https://sentry.invalid:443/api/42/upload/"); + sentry_free(url); + sentry__dsn_decref(dsn); + + TEST_CHECK(!sentry__dsn_get_upload_url(NULL)); + } + + SENTRY_TEST(tus_request_preparation) + { + SENTRY_TEST_DSN_NEW_DEFAULT(dsn); + + const char *test_file_str + = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + TEST_CHECK_INT_EQUAL( + sentry__path_write_buffer(test_file_path, "test-data", 9), 0); + + sentry_prepared_http_request_t *req + = sentry__prepare_tus_request(test_file_path, 9, dsn, NULL); + TEST_CHECK(!!req); + TEST_CHECK_STRING_EQUAL(req->method, "POST"); + TEST_CHECK_STRING_EQUAL( + req->url, "https://sentry.invalid:443/api/42/upload/"); + TEST_CHECK(!!req->body_path); + TEST_CHECK_INT_EQUAL(req->body_len, 9); + TEST_CHECK(!req->body); + + bool has_tus_resumable = false; + bool has_upload_length = false; + bool has_content_type = false; + for (size_t i = 0; i < req->headers_len; i++) { + if (strcmp(req->headers[i].key, "tus-resumable") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "1.0.0"); + has_tus_resumable = true; + } + if (strcmp(req->headers[i].key, "upload-length") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "9"); + has_upload_length = true; + } + if (strcmp(req->headers[i].key, "content-type") == 0) { + TEST_CHECK_STRING_EQUAL( + req->headers[i].value, "application/offset+octet-stream"); + has_content_type = true; + } + } + TEST_CHECK(has_tus_resumable); + TEST_CHECK(has_upload_length); + TEST_CHECK(has_content_type); + + sentry__prepared_http_request_free(req); + sentry__path_remove(test_file_path); + sentry__path_free(test_file_path); + sentry__dsn_decref(dsn); + } + TEST_CHECK(!sentry_envelope_deserialize("{}", 0)); TEST_CHECK(!sentry_envelope_deserialize("\n", 1)); TEST_CHECK(!sentry_envelope_deserialize("{}\n{}", 5)); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 34d49b316..afa3973c5 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -255,6 +255,8 @@ XX(transactions_skip_before_send) XX(transport_retry) XX(transport_sampling_transactions) XX(transport_sampling_transactions_set_trace) +XX(tus_request_preparation) +XX(tus_upload_url) XX(txn_data) XX(txn_data_n) XX(txn_name) From 485eeb8834970cfae210bc8656c0c6c6deffa769 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sun, 8 Mar 2026 11:43:32 +0100 Subject: [PATCH 108/121] feat(tus): large attachment upload via TUS protocol Cache attachments >= 100 MB to disk at envelope preparation time, then upload them via TUS protocol before sending the envelope. The envelope carries lightweight attachment-ref items that point to the uploaded location instead of embedding the full payload. Key changes: - Add attachment helpers (get_size, get_filename, type_to_string) - Add envelope attachment-ref item type with location/content_type - Cache large attachments to /cache// with refs.json metadata - TUS upload with 404 fallback, retry integration, cache cleanup - Skip large attachments in envelope serialization (size threshold) - Fix envelope deserialization deadlock on bgworker thread Co-Authored-By: Claude Opus 4.6 --- examples/example.c | 19 + src/backends/sentry_backend_breakpad.cpp | 10 + src/backends/sentry_backend_crashpad.cpp | 5 + src/backends/sentry_backend_inproc.c | 3 +- src/sentry_attachment.c | 29 ++ src/sentry_attachment.h | 22 + src/sentry_core.c | 39 +- src/sentry_database.c | 139 +++++++ src/sentry_database.h | 12 + src/sentry_envelope.c | 79 +++- src/sentry_envelope.h | 11 + src/sentry_retry.c | 15 +- src/sentry_retry.h | 2 +- src/transports/sentry_http_transport.c | 174 +++++++- tests/__init__.py | 22 +- tests/test_integration_tus.py | 277 +++++++++++++ tests/unit/test_envelopes.c | 493 ++++++++++++++++++++--- tests/unit/test_retry.c | 3 +- tests/unit/tests.inc | 6 + 19 files changed, 1264 insertions(+), 96 deletions(-) create mode 100644 tests/test_integration_tus.py diff --git a/examples/example.c b/examples/example.c index 5111e47e6..1988e8351 100644 --- a/examples/example.c +++ b/examples/example.c @@ -524,6 +524,25 @@ main(int argc, char **argv) sentry_options_add_attachment(options, "./CMakeCache.txt"); } + if (has_arg(argc, argv, "large-attachment")) { + const char *large_file = ".sentry-large-attachment"; + FILE *f = fopen(large_file, "wb"); + if (f) { + // 100 MB = TUS upload threshold + char zeros[4096]; + memset(zeros, 0, sizeof(zeros)); + size_t remaining = 100 * 1024 * 1024; + while (remaining > 0) { + size_t chunk + = remaining < sizeof(zeros) ? remaining : sizeof(zeros); + fwrite(zeros, 1, chunk, f); + remaining -= chunk; + } + fclose(f); + sentry_options_add_attachment(options, large_file); + } + } + if (has_arg(argc, argv, "stdout")) { sentry_options_set_transport( options, sentry_transport_new(print_envelope)); diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index 537f58b3c..acf17ce1f 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -178,6 +178,16 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, sentry__envelope_item_set_header(item, "filename", sentry_value_new_string(sentry__path_filename(dump_path))); + } else { + sentry_uuid_t event_id + = sentry__envelope_get_event_id(envelope); + sentry_attachment_t tmp; + memset(&tmp, 0, sizeof(tmp)); + tmp.path = dump_path; + tmp.type = MINIDUMP; + tmp.next = nullptr; + sentry__cache_large_attachments( + options->run->cache_path, &event_id, &tmp, nullptr); } if (options->attach_screenshot) { diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index ba7420734..78af953e5 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -529,6 +529,11 @@ report_to_envelope(const crashpad::CrashReportDatabase::Report &report, if (sentry__envelope_add_event(envelope, event)) { sentry__envelope_add_attachments(envelope, attachments); + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + if (options->run) { + sentry__cache_large_attachments(options->run->cache_path, + &event_id, attachments, options->run->run_path); + } } else { sentry_value_decref(event); sentry_envelope_free(envelope); diff --git a/src/backends/sentry_backend_inproc.c b/src/backends/sentry_backend_inproc.c index 53aa0af54..650244df5 100644 --- a/src/backends/sentry_backend_inproc.c +++ b/src/backends/sentry_backend_inproc.c @@ -1156,8 +1156,9 @@ process_ucontext_deferred(const sentry_ucontext_t *uctx, } TEST_CRASH_POINT("before_capture"); if (should_handle) { + sentry_uuid_t event_id; sentry_envelope_t *envelope = sentry__prepare_event(options, event, - NULL, !options->on_crash_func && !skip_hooks, NULL); + &event_id, !options->on_crash_func && !skip_hooks, NULL); // TODO(tracing): Revisit when investigating transaction flushing // during hard crashes. diff --git a/src/sentry_attachment.c b/src/sentry_attachment.c index 4873d0a8d..68231c1d1 100644 --- a/src/sentry_attachment.c +++ b/src/sentry_attachment.c @@ -258,3 +258,32 @@ sentry__attachments_extend( attachments_ptr, attachment_clone(it), it->type, it->content_type); } } + +size_t +sentry__attachment_get_size(const sentry_attachment_t *attachment) +{ + return attachment->buf ? attachment->buf_len + : sentry__path_get_size(attachment->path); +} + +const char * +sentry__attachment_get_filename(const sentry_attachment_t *attachment) +{ + return sentry__path_filename( + attachment->filename ? attachment->filename : attachment->path); +} + +const char * +sentry__attachment_type_to_string(sentry_attachment_type_t attachment_type) +{ + switch (attachment_type) { + case ATTACHMENT: + return "event.attachment"; + case MINIDUMP: + return "event.minidump"; + case VIEW_HIERARCHY: + return "event.view_hierarchy"; + default: + return "event.attachment"; + } +} diff --git a/src/sentry_attachment.h b/src/sentry_attachment.h index 553a33bed..ec38416ae 100644 --- a/src/sentry_attachment.h +++ b/src/sentry_attachment.h @@ -5,6 +5,8 @@ #include "sentry_path.h" +#define SENTRY_LARGE_ATTACHMENT_SIZE (100 * 1024 * 1024) // 100 MB + /** * The attachment_type. */ @@ -89,4 +91,24 @@ void sentry__attachments_remove( void sentry__attachments_extend( sentry_attachment_t **attachments_ptr, sentry_attachment_t *attachments); +/** + * Returns the size in bytes of the attachment's data (buffer length or file + * size). + */ +size_t sentry__attachment_get_size(const sentry_attachment_t *attachment); + +/** + * Returns the filename string for the attachment (basename of `filename` if + * set, otherwise basename of `path`). + */ +const char *sentry__attachment_get_filename( + const sentry_attachment_t *attachment); + +/** + * Returns the Sentry envelope attachment_type string for the given type, + * e.g. "event.attachment", "event.minidump", "event.view_hierarchy". + */ +const char *sentry__attachment_type_to_string( + sentry_attachment_type_t attachment_type); + #endif diff --git a/src/sentry_core.c b/src/sentry_core.c index 4880bd1a1..48f6fa1c6 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -727,12 +727,12 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, } SENTRY_WITH_SCOPE (scope) { - if (all_attachments) { - // all attachments merged from multiple scopes - sentry__envelope_add_attachments(envelope, all_attachments); - } else { - // only global scope has attachments - sentry__envelope_add_attachments(envelope, scope->attachments); + const sentry_attachment_t *atts + = all_attachments ? all_attachments : scope->attachments; + sentry__envelope_add_attachments(envelope, atts); + if (options->run) { + sentry__cache_large_attachments(options->run->cache_path, event_id, + atts, options->run->run_path); } } @@ -819,6 +819,13 @@ prepare_user_feedback(sentry_value_t user_feedback, sentry_hint_t *hint) if (hint && hint->attachments) { sentry__envelope_add_attachments(envelope, hint->attachments); + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + SENTRY_WITH_OPTIONS (options) { + if (options->run) { + sentry__cache_large_attachments(options->run->cache_path, + &event_id, hint->attachments, options->run->run_path); + } + } } return envelope; @@ -1753,18 +1760,30 @@ sentry_capture_minidump_n(const char *path, size_t path_len) } else { // the minidump is added as an attachment, with the type // `event.minidump` + size_t dump_size = sentry__path_get_size(dump_path); + bool is_large = dump_size >= SENTRY_LARGE_ATTACHMENT_SIZE; sentry_envelope_item_t *item = sentry__envelope_add_from_path( envelope, dump_path, "attachment"); - if (!item) { - sentry_envelope_free(envelope); - } else { + if (item) { sentry__envelope_item_set_header(item, "attachment_type", sentry_value_new_string("event.minidump")); - sentry__envelope_item_set_header(item, "filename", sentry_value_new_string(sentry__path_filename(dump_path))); + } else if (is_large && options->run) { + sentry_attachment_t tmp; + memset(&tmp, 0, sizeof(tmp)); + tmp.path = dump_path; + tmp.type = MINIDUMP; + tmp.next = NULL; + sentry__cache_large_attachments( + options->run->cache_path, &event_id, &tmp, NULL); + } else { + sentry_envelope_free(envelope); + envelope = NULL; + } + if (envelope) { sentry__capture_envelope(options->transport, envelope); SENTRY_INFOF( diff --git a/src/sentry_database.c b/src/sentry_database.c index 05abb173b..c37d94cfc 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -1,5 +1,6 @@ #include "sentry_database.h" #include "sentry_alloc.h" +#include "sentry_attachment.h" #include "sentry_envelope.h" #include "sentry_json.h" #include "sentry_options.h" @@ -164,6 +165,105 @@ write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope, return true; } +void +sentry__cache_large_attachments(const sentry_path_t *cache_path, + const sentry_uuid_t *event_id, const sentry_attachment_t *attachments, + const sentry_path_t *run_path) +{ + if (!cache_path || !event_id || !attachments) { + return; + } + + char uuid_str[37]; + sentry_uuid_as_string(event_id, uuid_str); + + sentry_path_t *event_dir = sentry__path_join_str(cache_path, uuid_str); + if (!event_dir) { + return; + } + + sentry_value_t refs = sentry_value_new_list(); + bool any_cached = false; + + for (const sentry_attachment_t *att = attachments; att; att = att->next) { + size_t file_size = sentry__attachment_get_size(att); + if (file_size < SENTRY_LARGE_ATTACHMENT_SIZE) { + continue; + } + + if (!any_cached) { + if (sentry__path_create_dir_all(event_dir) != 0) { + sentry__path_free(event_dir); + sentry_value_decref(refs); + return; + } + any_cached = true; + } + + const char *filename = sentry__attachment_get_filename(att); + sentry_path_t *dst = sentry__path_join_str(event_dir, filename); + if (!dst) { + continue; + } + + int rv; + if (att->buf) { + rv = sentry__path_write_buffer(dst, att->buf, att->buf_len); + } else { + sentry_path_t *src_dir = sentry__path_dir(att->path); + bool is_run_owned + = run_path && src_dir && sentry__path_eq(src_dir, run_path); + sentry__path_free(src_dir); + rv = is_run_owned ? sentry__path_rename(att->path, dst) + : sentry__path_copy(att->path, dst); + } + sentry__path_free(dst); + + if (rv != 0) { + continue; + } + + sentry_value_t obj = sentry_value_new_object(); + sentry_value_set_by_key( + obj, "filename", sentry_value_new_string(filename)); + if (att->content_type) { + sentry_value_set_by_key(obj, "content_type", + sentry_value_new_string(att->content_type)); + } + if (att->type != ATTACHMENT) { + sentry_value_set_by_key(obj, "attachment_type", + sentry_value_new_string( + sentry__attachment_type_to_string(att->type))); + } + sentry_value_set_by_key(obj, "attachment_length", + sentry_value_new_uint64((uint64_t)file_size)); + sentry_value_append(refs, obj); + } + + if (sentry_value_get_length(refs) > 0) { + sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); + if (jw) { + sentry__jsonwriter_write_value(jw, refs); + size_t buf_len = 0; + char *buf = sentry__jsonwriter_into_string(jw, &buf_len); + if (buf) { + sentry_path_t *refs_path + = sentry__path_join_str(event_dir, "refs.json"); + if (refs_path) { + int rv = sentry__path_write_buffer(refs_path, buf, buf_len); + if (rv != 0) { + SENTRY_WARN("writing refs.json failed"); + } + sentry__path_free(refs_path); + } + sentry_free(buf); + } + } + } + sentry_value_decref(refs); + sentry__path_free(event_dir); +} + bool sentry__run_write_envelope( const sentry_run_t *run, const sentry_envelope_t *envelope) @@ -453,6 +553,25 @@ compare_cache_entries_newest_first(const void *a, const void *b) return 0; } +static sentry_path_t * +envelope_attachment_dir(const sentry_path_t *cache_dir, const char *filename) +{ + uint64_t ts; + int count; + const char *uuid; + if (!sentry__parse_cache_filename(filename, &ts, &count, &uuid)) { + size_t len = strlen(filename); + if (len != 45 || strcmp(filename + 36, ".envelope") != 0) { + return NULL; + } + uuid = filename; + } + char uuid_buf[37]; + memcpy(uuid_buf, uuid, 36); + uuid_buf[36] = '\0'; + return sentry__path_join_str(cache_dir, uuid_buf); +} + void sentry__cleanup_cache(const sentry_options_t *options) { @@ -503,6 +622,17 @@ sentry__cleanup_cache(const sentry_options_t *options) } entries[entries_count].mtime = sentry__path_get_mtime(entry); entries[entries_count].size = sentry__path_get_size(entry); + + const char *fname = sentry__path_filename(entry); + if (fname) { + sentry_path_t *att_dir = envelope_attachment_dir(cache_dir, fname); + if (att_dir) { + entries[entries_count].size + += sentry__path_get_dir_size(att_dir); + sentry__path_free(att_dir); + } + } + entries_count++; } sentry__pathiter_free(iter); @@ -539,6 +669,15 @@ sentry__cleanup_cache(const sentry_options_t *options) } if (should_prune) { + const char *fname = sentry__path_filename(entries[i].path); + if (fname) { + sentry_path_t *att_dir + = envelope_attachment_dir(cache_dir, fname); + if (att_dir) { + sentry__path_remove_all(att_dir); + sentry__path_free(att_dir); + } + } sentry__path_remove_all(entries[i].path); } sentry__path_free(entries[i].path); diff --git a/src/sentry_database.h b/src/sentry_database.h index 33e6735ce..d73888676 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -3,6 +3,7 @@ #include "sentry_boot.h" +#include "sentry_attachment.h" #include "sentry_path.h" #include "sentry_session.h" @@ -138,4 +139,15 @@ bool sentry__has_crash_marker(const sentry_options_t *options); */ bool sentry__clear_crash_marker(const sentry_options_t *options); +/** + * Cache large attachments (>= SENTRY_LARGE_ATTACHMENT_SIZE) to + * `//` and write `refs.json` metadata. + * + * When `run_path` is non-NULL and a file attachment's parent directory + * matches it, the file is renamed instead of copied. + */ +void sentry__cache_large_attachments(const sentry_path_t *cache_path, + const sentry_uuid_t *event_id, const sentry_attachment_t *attachments, + const sentry_path_t *run_path); + #endif diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index e69c05eba..8b451f7e8 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -1,5 +1,6 @@ #include "sentry_envelope.h" #include "sentry_alloc.h" +#include "sentry_attachment.h" #include "sentry_core.h" #include "sentry_json.h" #include "sentry_options.h" @@ -605,20 +606,53 @@ sentry__envelope_add_session( envelope, payload, payload_len, "session"); } -static const char * -str_from_attachment_type(sentry_attachment_type_t attachment_type) +sentry_envelope_item_t * +sentry__envelope_add_attachment_ref(sentry_envelope_t *envelope, + const char *location, const char *filename, const char *content_type, + const char *attachment_type, sentry_value_t attachment_length) { - switch (attachment_type) { - case ATTACHMENT: - return "event.attachment"; - case MINIDUMP: - return "event.minidump"; - case VIEW_HIERARCHY: - return "event.view_hierarchy"; - default: - UNREACHABLE("Unknown attachment type"); - return "event.attachment"; + sentry_envelope_item_t *item = envelope_add_item(envelope); + if (!item) { + return NULL; + } + sentry__envelope_item_set_header( + item, "type", sentry_value_new_string("attachment")); + sentry__envelope_item_set_header(item, "content_type", + sentry_value_new_string("application/vnd.sentry.attachment-ref")); + if (filename) { + sentry__envelope_item_set_header( + item, "filename", sentry_value_new_string(filename)); + } + if (attachment_type) { + sentry__envelope_item_set_header( + item, "attachment_type", sentry_value_new_string(attachment_type)); + } + sentry__envelope_item_set_header( + item, "attachment_length", attachment_length); + + if (location || content_type) { + sentry_value_t obj = sentry_value_new_object(); + if (location) { + sentry_value_set_by_key( + obj, "location", sentry_value_new_string(location)); + } + if (content_type) { + sentry_value_set_by_key( + obj, "content_type", sentry_value_new_string(content_type)); + } + sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); + sentry__jsonwriter_write_value(jw, obj); + sentry_value_decref(obj); + size_t payload_len = 0; + char *payload = sentry__jsonwriter_into_string(jw, &payload_len); + sentry_free(item->payload); + item->payload = payload; + item->payload_len = payload_len; + sentry__envelope_item_set_header( + item, "length", sentry_value_new_int32((int32_t)payload_len)); } + + return item; } sentry_envelope_item_t * @@ -629,6 +663,12 @@ sentry__envelope_add_attachment( return NULL; } + size_t file_size = sentry__attachment_get_size(attachment); + + if (file_size >= SENTRY_LARGE_ATTACHMENT_SIZE) { + return NULL; + } + sentry_envelope_item_t *item = NULL; if (attachment->buf) { item = sentry__envelope_add_from_buffer( @@ -643,15 +683,14 @@ sentry__envelope_add_attachment( if (attachment->type != ATTACHMENT) { // don't need to set the default sentry__envelope_item_set_header(item, "attachment_type", sentry_value_new_string( - str_from_attachment_type(attachment->type))); + sentry__attachment_type_to_string(attachment->type))); } if (attachment->content_type) { sentry__envelope_item_set_header(item, "content_type", sentry_value_new_string(attachment->content_type)); } sentry__envelope_item_set_header(item, "filename", - sentry_value_new_string(sentry__path_filename( - attachment->filename ? attachment->filename : attachment->path))); + sentry_value_new_string(sentry__attachment_get_filename(attachment))); return item; } @@ -688,6 +727,10 @@ sentry__envelope_add_from_path( if (!envelope) { return NULL; } + size_t file_size = sentry__path_get_size(path); + if (file_size >= SENTRY_LARGE_ATTACHMENT_SIZE) { + return NULL; + } size_t buf_len; char *buf = sentry__path_read_to_buffer(path, &buf_len); if (!buf) { @@ -878,7 +921,11 @@ sentry_envelope_deserialize(const char *buf, size_t buf_len) return NULL; } - sentry_envelope_t *envelope = sentry__envelope_new(); + // Use sentry__envelope_new_with_dsn(NULL) instead of sentry__envelope_new() + // because the DSN is part of the serialized headers and will be restored by + // deserialization. This avoids acquiring the options lock, which would + // deadlock if called from a bgworker thread during shutdown. + sentry_envelope_t *envelope = sentry__envelope_new_with_dsn(NULL); if (!envelope) { goto fail; } diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index b047bfc85..4a270ae29 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -97,6 +97,17 @@ sentry_envelope_item_t *sentry__envelope_add_session( sentry_envelope_item_t *sentry__envelope_add_attachment( sentry_envelope_t *envelope, const sentry_attachment_t *attachment); +/** + * Add an attachment-ref item to this envelope. + * + * Builds a JSON payload containing `location` and/or `content_type` (omitting + * keys when NULL), and sets the standard attachment-ref item headers. + */ +sentry_envelope_item_t *sentry__envelope_add_attachment_ref( + sentry_envelope_t *envelope, const char *location, const char *filename, + const char *content_type, const char *attachment_type, + sentry_value_t attachment_length); + /** * Add attachments to this envelope. */ diff --git a/src/sentry_retry.c b/src/sentry_retry.c index 16321c676..a63e22921 100644 --- a/src/sentry_retry.c +++ b/src/sentry_retry.c @@ -93,6 +93,17 @@ compare_retry_items(const void *a, const void *b) return strcmp(ia->uuid, ib->uuid); } +static void +remove_attachment_dir(const sentry_retry_t *retry, const retry_item_t *item) +{ + sentry_path_t *att_dir + = sentry__path_join_str(retry->run->cache_path, item->uuid); + if (att_dir) { + sentry__path_remove_all(att_dir); + sentry__path_free(att_dir); + } +} + static bool handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) { @@ -131,11 +142,13 @@ handle_result(sentry_retry_t *retry, const retry_item_t *item, int status_code) if (exhausted && retry->cache_keep) { if (!sentry__run_move_cache(retry->run, item->path, -1)) { sentry__path_remove(item->path); + remove_attachment_dir(retry, item); } return false; } sentry__path_remove(item->path); + remove_attachment_dir(retry, item); return false; } @@ -212,7 +225,7 @@ sentry__retry_send(sentry_retry_t *retry, uint64_t before, } else { SENTRY_DEBUGF("retrying envelope (%d/%d)", items[i].count + 1, SENTRY_RETRY_ATTEMPTS); - int status_code = send_cb(envelope, data); + int status_code = send_cb(envelope, items[i].uuid, data); sentry_envelope_free(envelope); if (!handle_result(retry, &items[i], status_code)) { total--; diff --git a/src/sentry_retry.h b/src/sentry_retry.h index 5e5222097..fe9aefa6d 100644 --- a/src/sentry_retry.h +++ b/src/sentry_retry.h @@ -8,7 +8,7 @@ typedef struct sentry_retry_s sentry_retry_t; typedef int (*sentry_retry_send_func_t)( - sentry_envelope_t *envelope, void *data); + sentry_envelope_t *envelope, const char *uuid, void *data); sentry_retry_t *sentry__retry_new(const sentry_options_t *options); void sentry__retry_free(sentry_retry_t *retry); diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 572effd21..c6524494e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -8,6 +8,7 @@ #include "sentry_string.h" #include "sentry_transport.h" #include "sentry_utils.h" +#include "sentry_value.h" #ifdef SENTRY_TRANSPORT_COMPRESSION # include "zlib.h" @@ -27,6 +28,7 @@ typedef struct { sentry_dsn_t *dsn; char *user_agent; + sentry_run_t *run; sentry_rate_limiter_t *ratelimiter; void *client; void (*free_client)(void *); @@ -35,7 +37,7 @@ typedef struct { void (*shutdown_client)(void *client); sentry_retry_t *retry; bool cache_keep; - sentry_run_t *run; + bool has_tus; } http_transport_state_t; #ifdef SENTRY_TRANSPORT_COMPRESSION @@ -287,9 +289,168 @@ http_send_request( return resp.status_code; } +static bool +tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, + sentry_value_t entry, sentry_envelope_t *send_envelope) +{ + const char *filename + = sentry_value_as_string(sentry_value_get_by_key(entry, "filename")); + if (!filename || *filename == '\0') { + return false; + } + + sentry_path_t *att_file = sentry__path_join_str(att_dir, filename); + size_t file_size = att_file ? sentry__path_get_size(att_file) : 0; + if (!att_file || file_size == 0) { + sentry__path_free(att_file); + return false; + } + + sentry_prepared_http_request_t *req = prepare_tus_request( + att_file, file_size, state->dsn, state->user_agent); + if (!req) { + sentry__path_free(att_file); + return false; + } + + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + bool ok = state->send_func(state->client, req, &resp); + sentry__prepared_http_request_free(req); + + if (!ok) { + sentry__path_free(att_file); + http_response_cleanup(&resp); + return false; + } + + if (resp.status_code == 404) { + state->has_tus = false; + SENTRY_WARN("TUS upload returned 404, disabling TUS"); + sentry__path_free(att_file); + http_response_cleanup(&resp); + return false; + } + + if (resp.status_code != 201 || !resp.location) { + sentry__path_free(att_file); + http_response_cleanup(&resp); + return false; + } + + const char *ref_ct = sentry_value_as_string( + sentry_value_get_by_key(entry, "content_type")); + const char *att_type = sentry_value_as_string( + sentry_value_get_by_key(entry, "attachment_type")); + sentry_value_t att_length + = sentry_value_get_by_key(entry, "attachment_length"); + sentry_value_incref(att_length); + sentry__envelope_add_attachment_ref(send_envelope, resp.location, filename, + *ref_ct ? ref_ct : NULL, *att_type ? att_type : NULL, att_length); + + sentry__path_remove(att_file); + sentry__path_free(att_file); + http_response_cleanup(&resp); + return true; +} + +static void +tus_resolve_and_send(http_transport_state_t *state, const char *uuid) +{ + if (!state->has_tus || !uuid || !state->run) { + return; + } + + sentry_path_t *att_dir + = sentry__path_join_str(state->run->cache_path, uuid); + if (!att_dir || !sentry__path_is_dir(att_dir)) { + sentry__path_free(att_dir); + return; + } + + sentry_path_t *refs_path = sentry__path_join_str(att_dir, "refs.json"); + if (!refs_path) { + sentry__path_free(att_dir); + return; + } + if (!sentry__path_is_file(refs_path)) { + sentry__path_free(refs_path); + sentry__path_free(att_dir); + return; + } + + size_t buf_len = 0; + char *buf = sentry__path_read_to_buffer(refs_path, &buf_len); + if (!buf) { + sentry__path_free(refs_path); + sentry__path_free(att_dir); + return; + } + sentry_value_t refs = sentry__value_from_json(buf, buf_len); + sentry_free(buf); + if (sentry_value_get_type(refs) != SENTRY_VALUE_TYPE_LIST) { + sentry_value_decref(refs); + sentry__path_free(refs_path); + sentry__path_free(att_dir); + return; + } + + sentry_uuid_t event_id = sentry_uuid_from_string(uuid); + sentry_envelope_t *send_envelope + = sentry__envelope_new_with_dsn(state->dsn); + if (!send_envelope) { + sentry_value_decref(refs); + sentry__path_free(refs_path); + sentry__path_free(att_dir); + return; + } + sentry__envelope_set_event_id(send_envelope, &event_id); + + bool has_refs = false; + size_t count = sentry_value_get_length(refs); + for (size_t i = 0; i < count; i++) { + sentry_value_t entry = sentry_value_get_by_index(refs, i); + if (tus_upload_ref(state, att_dir, entry, send_envelope)) { + has_refs = true; + } + if (!state->has_tus) { + break; + } + } + + sentry_value_decref(refs); + + if (has_refs) { + sentry_prepared_http_request_t *req = sentry__prepare_http_request( + send_envelope, state->dsn, state->ratelimiter, state->user_agent); + if (req) { + http_send_request(state, req); + sentry__prepared_http_request_free(req); + } + } + sentry_envelope_free(send_envelope); + + sentry__path_remove(refs_path); + sentry__path_free(refs_path); + sentry__path_remove(att_dir); + sentry__path_free(att_dir); +} + static int -http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope) +http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope, + const char *uuid) { + char uuid_buf[37]; + if (!uuid) { + sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); + if (!sentry_uuid_is_nil(&event_id)) { + sentry_uuid_as_string(&event_id, uuid_buf); + uuid = uuid_buf; + } + } + + tus_resolve_and_send(state, uuid); + sentry_prepared_http_request_t *req = sentry__prepare_http_request( envelope, state->dsn, state->ratelimiter, state->user_agent); if (!req) { @@ -301,10 +462,10 @@ http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope) } static int -retry_send_cb(sentry_envelope_t *envelope, void *_state) +retry_send_cb(sentry_envelope_t *envelope, const char *uuid, void *_state) { http_transport_state_t *state = _state; - return http_send_envelope(state, envelope); + return http_send_envelope(state, envelope, uuid); } static void @@ -316,9 +477,9 @@ http_transport_state_free(void *_state) } sentry__dsn_decref(state->dsn); sentry_free(state->user_agent); + sentry__run_free(state->run); sentry__rate_limiter_free(state->ratelimiter); sentry__retry_free(state->retry); - sentry__run_free(state->run); sentry_free(state); } @@ -328,7 +489,7 @@ http_send_task(void *_envelope, void *_state) sentry_envelope_t *envelope = _envelope; http_transport_state_t *state = _state; - int status_code = http_send_envelope(state, envelope); + int status_code = http_send_envelope(state, envelope, NULL); if (status_code < 0 && state->retry) { sentry__retry_enqueue(state->retry, envelope); } else if (status_code < 0 && state->cache_keep) { @@ -475,6 +636,7 @@ sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) } memset(state, 0, sizeof(http_transport_state_t)); state->ratelimiter = sentry__rate_limiter_new(); + state->has_tus = true; state->client = client; state->send_func = send_func; diff --git a/tests/__init__.py b/tests/__init__.py index cf84e8c42..00ae16097 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -343,15 +343,19 @@ def deserialize_from( headers = json.loads(line) length = headers["length"] payload = f.read(length) - if headers.get("type") in [ - "event", - "feedback", - "session", - "transaction", - "user_report", - "log", - "trace_metric", - ]: + if ( + headers.get("type") + in [ + "event", + "feedback", + "session", + "transaction", + "user_report", + "log", + "trace_metric", + ] + or headers.get("content_type") == "application/vnd.sentry.attachment-ref" + ): rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload))) else: rv = cls(headers=headers, payload=payload) diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py new file mode 100644 index 000000000..e923744e4 --- /dev/null +++ b/tests/test_integration_tus.py @@ -0,0 +1,277 @@ +import json +import os + +import pytest + +from . import ( + make_dsn, + run, + Envelope, + SENTRY_VERSION, +) +from .conditions import has_http + +pytestmark = pytest.mark.skipif(not has_http, reason="tests need http") + +# fmt: off +auth_header = ( + f"Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/{SENTRY_VERSION}" +) +# fmt: on + + +def test_tus_upload_large_attachment(cmake, httpserver): + tmp_path = cmake( + ["sentry_example"], + {"SENTRY_BACKEND": "none", "SENTRY_TRANSPORT_COMPRESSION": "Off"}, + ) + + location = "/api/123456/upload/abc123def456789/?length=104857600&signature=xyz" + + httpserver.expect_oneshot_request( + "/api/123456/upload/", + headers={"tus-resumable": "1.0.0"}, + ).respond_with_data( + "OK", + status=201, + headers={"Location": location}, + ) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "no-setup", "large-attachment", "capture-event"], + env=env, + ) + + assert len(httpserver.log) == 3 + + # Find the upload request and envelope requests + upload_req = None + envelope_reqs = [] + for entry in httpserver.log: + req = entry[0] + if "/upload/" in req.path: + upload_req = req + elif "/envelope/" in req.path: + envelope_reqs.append(req) + + assert upload_req is not None + assert len(envelope_reqs) == 2 + + # Verify TUS upload request headers + assert upload_req.headers.get("tus-resumable") == "1.0.0" + assert upload_req.headers.get("content-type") == "application/offset+octet-stream" + upload_length = upload_req.headers.get("upload-length") + assert upload_length is not None + assert int(upload_length) == 100 * 1024 * 1024 + + # One envelope has the resolved attachment-refs, the other is the original + attachment_ref = None + for envelope_req in envelope_reqs: + body = envelope_req.get_data() + envelope = Envelope.deserialize(body) + for item in envelope: + if ( + item.headers.get("content_type") + == "application/vnd.sentry.attachment-ref" + ): + if hasattr(item.payload, "json") and "location" in item.payload.json: + attachment_ref = item + break + + assert attachment_ref is not None + assert attachment_ref.payload.json["location"] == location + assert attachment_ref.headers.get("attachment_length") == 100 * 1024 * 1024 + + # large attachment files should be cleaned up after send + cache_dir = os.path.join(tmp_path, ".sentry-native", "cache") + if os.path.isdir(cache_dir): + subdirs = [ + d + for d in os.listdir(cache_dir) + if os.path.isdir(os.path.join(cache_dir, d)) + ] + assert subdirs == [] + + +def test_tus_upload_404_disables(cmake, httpserver): + tmp_path = cmake( + ["sentry_example"], + {"SENTRY_BACKEND": "none", "SENTRY_TRANSPORT_COMPRESSION": "Off"}, + ) + + httpserver.expect_oneshot_request( + "/api/123456/upload/", + ).respond_with_data("Not Found", status=404) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "no-setup", "large-attachment", "capture-event"], + env=env, + ) + + assert len(httpserver.log) == 2 + + # Find the envelope request + envelope_req = None + for entry in httpserver.log: + req = entry[0] + if "/envelope/" in req.path: + envelope_req = req + + assert envelope_req is not None + + # TUS 404: attachment-refs were cached but not resolved, so envelope has + # no attachment-ref items + body = envelope_req.get_data() + envelope = Envelope.deserialize(body) + for item in envelope: + assert ( + item.headers.get("content_type") != "application/vnd.sentry.attachment-ref" + ) + + +def test_tus_crash_restart(cmake, httpserver): + tmp_path = cmake( + ["sentry_example"], + {"SENTRY_BACKEND": "inproc", "SENTRY_TRANSPORT_COMPRESSION": "Off"}, + ) + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + # First run: crash with large attachment (no server expectations needed) + run( + tmp_path, + "sentry_example", + ["log", "large-attachment", "crash"], + expect_failure=True, + env=env, + ) + + # Verify large attachment was persisted to disk in cache// + cache_dir = os.path.join(tmp_path, ".sentry-native", "cache") + assert os.path.isdir(cache_dir) + att_dirs = [ + d for d in os.listdir(cache_dir) if os.path.isdir(os.path.join(cache_dir, d)) + ] + assert len(att_dirs) > 0 + att_dir = os.path.join(cache_dir, att_dirs[0]) + att_files = [ + f + for f in os.listdir(att_dir) + if not f.endswith(".json") and os.path.isfile(os.path.join(att_dir, f)) + ] + refs_files = [f for f in os.listdir(att_dir) if f == "refs.json"] + assert len(att_files) > 0 + assert len(refs_files) > 0 + att_size = os.path.getsize(os.path.join(att_dir, att_files[0])) + assert att_size >= 100 * 1024 * 1024 + + location = "/api/123456/upload/abc123def456789/?length=104857600&signature=xyz" + + # Second run: restart picks up crash and uploads via TUS + httpserver.expect_oneshot_request( + "/api/123456/upload/", + headers={"tus-resumable": "1.0.0"}, + ).respond_with_data( + "OK", + status=201, + headers={"Location": location}, + ) + + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_example", + ["log", "no-setup"], + env=env, + ) + + assert len(httpserver.log) == 3 + + upload_req = None + envelope_reqs = [] + for entry in httpserver.log: + req = entry[0] + if "/upload/" in req.path: + upload_req = req + elif "/envelope/" in req.path: + envelope_reqs.append(req) + + assert upload_req is not None + assert len(envelope_reqs) == 2 + + # Verify TUS upload request headers + assert upload_req.headers.get("tus-resumable") == "1.0.0" + assert upload_req.headers.get("content-type") == "application/offset+octet-stream" + assert int(upload_req.headers.get("upload-length")) == 100 * 1024 * 1024 + + # One envelope has the resolved attachment-refs, the other is the original + attachment_ref = None + for envelope_req in envelope_reqs: + body = envelope_req.get_data() + envelope = Envelope.deserialize(body) + for item in envelope: + if ( + item.headers.get("content_type") + == "application/vnd.sentry.attachment-ref" + ): + if hasattr(item.payload, "json") and "location" in item.payload.json: + attachment_ref = item + break + + assert attachment_ref is not None + assert attachment_ref.payload.json["location"] == location + assert attachment_ref.headers.get("attachment_length") == 100 * 1024 * 1024 + + # Large attachment dirs should be cleaned up after send + remaining_dirs = [ + d for d in os.listdir(cache_dir) if os.path.isdir(os.path.join(cache_dir, d)) + ] + assert remaining_dirs == [] + + +def test_small_attachment_no_tus(cmake, httpserver): + tmp_path = cmake( + ["sentry_example"], + {"SENTRY_BACKEND": "none", "SENTRY_TRANSPORT_COMPRESSION": "Off"}, + ) + + httpserver.expect_oneshot_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + run( + tmp_path, + "sentry_example", + ["log", "no-setup", "attachment", "capture-event"], + env=env, + ) + + # Only 1 request - no TUS upload for small attachments + assert len(httpserver.log) == 1 + req = httpserver.log[0][0] + assert "/envelope/" in req.path diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index 1bd7ab7ae..faf55a0fc 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -1,6 +1,10 @@ +#include "sentry_attachment.h" +#include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_json.h" +#include "sentry_options.h" #include "sentry_path.h" +#include "sentry_string.h" #include "sentry_testsupport.h" #include "sentry_utils.h" #include "sentry_value.h" @@ -722,70 +726,412 @@ SENTRY_TEST(deserialize_envelope_empty) test_deserialize_envelope_empty(buf, buf_len - 1); } -SENTRY_TEST(deserialize_envelope_invalid) +SENTRY_TEST(tus_upload_url) { - TEST_CHECK(!sentry_envelope_deserialize("", 0)); - SENTRY_TEST(tus_upload_url) + SENTRY_TEST_DSN_NEW_DEFAULT(dsn); + + char *url = sentry__dsn_get_upload_url(dsn); + TEST_CHECK_STRING_EQUAL(url, "https://sentry.invalid:443/api/42/upload/"); + sentry_free(url); + sentry__dsn_decref(dsn); + + TEST_CHECK(!sentry__dsn_get_upload_url(NULL)); +} + +SENTRY_TEST(tus_request_preparation) +{ + SENTRY_TEST_DSN_NEW_DEFAULT(dsn); + + const char *test_file_str = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + TEST_CHECK_INT_EQUAL( + sentry__path_write_buffer(test_file_path, "test-data", 9), 0); + + sentry_prepared_http_request_t *req + = sentry__prepare_tus_request(test_file_path, 9, dsn, NULL); + TEST_CHECK(!!req); + TEST_CHECK_STRING_EQUAL(req->method, "POST"); + TEST_CHECK_STRING_EQUAL( + req->url, "https://sentry.invalid:443/api/42/upload/"); + TEST_CHECK(!!req->body_path); + TEST_CHECK_INT_EQUAL(req->body_len, 9); + TEST_CHECK(!req->body); + + bool has_tus_resumable = false; + bool has_upload_length = false; + bool has_content_type = false; + for (size_t i = 0; i < req->headers_len; i++) { + if (strcmp(req->headers[i].key, "tus-resumable") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "1.0.0"); + has_tus_resumable = true; + } + if (strcmp(req->headers[i].key, "upload-length") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "9"); + has_upload_length = true; + } + if (strcmp(req->headers[i].key, "content-type") == 0) { + TEST_CHECK_STRING_EQUAL( + req->headers[i].value, "application/offset+octet-stream"); + has_content_type = true; + } + } + TEST_CHECK(has_tus_resumable); + TEST_CHECK(has_upload_length); + TEST_CHECK(has_content_type); + + sentry__prepared_http_request_free(req); + sentry__path_remove(test_file_path); + sentry__path_free(test_file_path); + sentry__dsn_decref(dsn); +} + +SENTRY_TEST(attachment_ref_creation) +{ + const char *test_file_str + = SENTRY_TEST_PATH_PREFIX "sentry_test_attachment_ref"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + + // Small file: should be added to envelope { - SENTRY_TEST_DSN_NEW_DEFAULT(dsn); + sentry_envelope_t *envelope = sentry__envelope_new(); + char small_data[] = "small"; + TEST_CHECK_INT_EQUAL(sentry__path_write_buffer(test_file_path, + small_data, sizeof(small_data) - 1), + 0); - char *url = sentry__dsn_get_upload_url(dsn); + sentry_attachment_t *attachment + = sentry__attachment_from_path(sentry__path_clone(test_file_path)); + sentry_envelope_item_t *item + = sentry__envelope_add_attachment(envelope, attachment); + + TEST_CHECK(!!item); + TEST_CHECK_INT_EQUAL(sentry__envelope_get_item_count(envelope), 1); + size_t payload_len = 0; TEST_CHECK_STRING_EQUAL( - url, "https://sentry.invalid:443/api/42/upload/"); - sentry_free(url); - sentry__dsn_decref(dsn); + sentry__envelope_item_get_payload(item, &payload_len), "small"); - TEST_CHECK(!sentry__dsn_get_upload_url(NULL)); + sentry__attachment_free(attachment); + sentry_envelope_free(envelope); } - SENTRY_TEST(tus_request_preparation) + // Large file (>= threshold): should be skipped by envelope { - SENTRY_TEST_DSN_NEW_DEFAULT(dsn); + sentry_envelope_t *envelope = sentry__envelope_new(); + size_t large_size = 100 * 1024 * 1024; + FILE *f = fopen(test_file_str, "wb"); + TEST_CHECK(!!f); + fseek(f, (long)(large_size - 1), SEEK_SET); + fputc(0, f); + fclose(f); + + sentry_attachment_t *attachment + = sentry__attachment_from_path(sentry__path_clone(test_file_path)); + sentry_envelope_item_t *item + = sentry__envelope_add_attachment(envelope, attachment); + + TEST_CHECK(!item); + TEST_CHECK_INT_EQUAL(sentry__envelope_get_item_count(envelope), 0); + + sentry__attachment_free(attachment); + sentry_envelope_free(envelope); + } - const char *test_file_str - = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; - sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); - TEST_CHECK_INT_EQUAL( - sentry__path_write_buffer(test_file_path, "test-data", 9), 0); + sentry__path_remove(test_file_path); + sentry__path_free(test_file_path); +} + +SENTRY_TEST(attachment_ref_from_path) +{ + const char *test_file_str + = SENTRY_TEST_PATH_PREFIX "sentry_test_attachment_ref_from_path"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + + // Small file: should be added to envelope + { + sentry_envelope_t *envelope = sentry__envelope_new(); + char small_data[] = "small"; + TEST_CHECK_INT_EQUAL(sentry__path_write_buffer(test_file_path, + small_data, sizeof(small_data) - 1), + 0); + + sentry_envelope_item_t *item = sentry__envelope_add_from_path( + envelope, test_file_path, "attachment"); - sentry_prepared_http_request_t *req - = sentry__prepare_tus_request(test_file_path, 9, dsn, NULL); - TEST_CHECK(!!req); - TEST_CHECK_STRING_EQUAL(req->method, "POST"); + TEST_CHECK(!!item); + TEST_CHECK_INT_EQUAL(sentry__envelope_get_item_count(envelope), 1); + size_t payload_len = 0; TEST_CHECK_STRING_EQUAL( - req->url, "https://sentry.invalid:443/api/42/upload/"); - TEST_CHECK(!!req->body_path); - TEST_CHECK_INT_EQUAL(req->body_len, 9); - TEST_CHECK(!req->body); - - bool has_tus_resumable = false; - bool has_upload_length = false; - bool has_content_type = false; - for (size_t i = 0; i < req->headers_len; i++) { - if (strcmp(req->headers[i].key, "tus-resumable") == 0) { - TEST_CHECK_STRING_EQUAL(req->headers[i].value, "1.0.0"); - has_tus_resumable = true; - } - if (strcmp(req->headers[i].key, "upload-length") == 0) { - TEST_CHECK_STRING_EQUAL(req->headers[i].value, "9"); - has_upload_length = true; - } - if (strcmp(req->headers[i].key, "content-type") == 0) { - TEST_CHECK_STRING_EQUAL( - req->headers[i].value, "application/offset+octet-stream"); - has_content_type = true; - } - } - TEST_CHECK(has_tus_resumable); - TEST_CHECK(has_upload_length); - TEST_CHECK(has_content_type); - - sentry__prepared_http_request_free(req); - sentry__path_remove(test_file_path); - sentry__path_free(test_file_path); - sentry__dsn_decref(dsn); + sentry__envelope_item_get_payload(item, &payload_len), "small"); + + sentry_envelope_free(envelope); + } + + // Large file (>= threshold): should return NULL + { + sentry_envelope_t *envelope = sentry__envelope_new(); + size_t large_size = 100 * 1024 * 1024; + FILE *f = fopen(test_file_str, "wb"); + TEST_CHECK(!!f); + fseek(f, (long)(large_size - 1), SEEK_SET); + fputc(0, f); + fclose(f); + + sentry_envelope_item_t *item = sentry__envelope_add_from_path( + envelope, test_file_path, "attachment"); + + TEST_CHECK(!item); + TEST_CHECK_INT_EQUAL(sentry__envelope_get_item_count(envelope), 0); + + sentry_envelope_free(envelope); + } + + sentry__path_remove(test_file_path); + sentry__path_free(test_file_path); +} + +SENTRY_TEST(attachment_ref_copy) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_init(options); + + sentry_uuid_t event_id + = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); + + const char *test_file_str + = SENTRY_TEST_PATH_PREFIX "sentry_test_large_attachment"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + size_t large_size = 100 * 1024 * 1024; + FILE *f = fopen(test_file_str, "wb"); + TEST_CHECK(!!f); + fseek(f, (long)(large_size - 1), SEEK_SET); + fputc(0, f); + fclose(f); + + sentry_attachment_t *attachment + = sentry__attachment_from_path(sentry__path_clone(test_file_path)); + sentry_attachment_set_content_type(attachment, "application/x-dmp"); + + // cache_large_attachments copies the file (not under run_path) + sentry_path_t *db_path = NULL; + SENTRY_WITH_OPTIONS (opts) { + db_path = sentry__path_clone(opts->database_path); + sentry__cache_large_attachments( + opts->run->cache_path, &event_id, attachment, NULL); } + // original file still exists (copied, not moved) + TEST_CHECK(sentry__path_is_file(test_file_path)); + + // attachment file exists in cache dir + sentry_path_t *cache_dir = sentry__path_join_str(db_path, "cache"); + sentry_path_t *event_dir = sentry__path_join_str( + cache_dir, "c993afb6-b4ac-48a6-b61b-2558e601d65d"); + sentry__path_free(cache_dir); + sentry_path_t *att_file + = sentry__path_join_str(event_dir, "sentry_test_large_attachment"); + TEST_CHECK(sentry__path_is_file(att_file)); + + // refs.json exists with correct metadata + sentry_path_t *refs_path = sentry__path_join_str(event_dir, "refs.json"); + TEST_CHECK(sentry__path_is_file(refs_path)); + + size_t refs_len = 0; + char *refs_buf = sentry__path_read_to_buffer(refs_path, &refs_len); + TEST_CHECK(!!refs_buf); + if (refs_buf) { + sentry_value_t refs = sentry__value_from_json(refs_buf, refs_len); + sentry_free(refs_buf); + TEST_CHECK(sentry_value_get_type(refs) == SENTRY_VALUE_TYPE_LIST); + TEST_CHECK(sentry_value_get_length(refs) == 1); + sentry_value_t ref_entry = sentry_value_get_by_index(refs, 0); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ref_entry, "filename")), + "sentry_test_large_attachment"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ref_entry, "content_type")), + "application/x-dmp"); + sentry_value_decref(refs); + } + + sentry__path_free(refs_path); + sentry__path_free(att_file); + sentry__path_free(event_dir); + sentry__path_free(db_path); + sentry__attachment_free(attachment); + sentry__path_remove(test_file_path); + sentry__path_free(test_file_path); + sentry_close(); +} + +SENTRY_TEST(attachment_ref_move) +{ + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_init(options); + + sentry_uuid_t event_id + = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); + + // create large file inside the run directory (SDK-owned) + sentry_path_t *run_path = NULL; + sentry_path_t *db_path = NULL; + SENTRY_WITH_OPTIONS (opts) { + run_path = sentry__path_clone(opts->run->run_path); + db_path = sentry__path_clone(opts->database_path); + } + TEST_CHECK(!!run_path); + sentry_path_t *src_path + = sentry__path_join_str(run_path, "test_minidump.dmp"); + + size_t large_size = 100 * 1024 * 1024; +#ifdef SENTRY_PLATFORM_WINDOWS + FILE *f = _wfopen(src_path->path_w, L"wb"); +#else + FILE *f = fopen(src_path->path, "wb"); +#endif + TEST_CHECK(!!f); + fseek(f, (long)(large_size - 1), SEEK_SET); + fputc(0, f); + fclose(f); + + sentry_attachment_t *attachment + = sentry__attachment_from_path(sentry__path_clone(src_path)); + + // cache with run_path → file is renamed (moved) + SENTRY_WITH_OPTIONS (opts) { + sentry__cache_large_attachments( + opts->run->cache_path, &event_id, attachment, run_path); + } + + // run-dir file is gone (renamed) + TEST_CHECK(!sentry__path_is_file(src_path)); + + // attachment file moved to cache dir + sentry_path_t *cache_dir = sentry__path_join_str(db_path, "cache"); + sentry_path_t *event_dir = sentry__path_join_str( + cache_dir, "c993afb6-b4ac-48a6-b61b-2558e601d65d"); + sentry__path_free(cache_dir); + sentry_path_t *att_file + = sentry__path_join_str(event_dir, "test_minidump.dmp"); + TEST_CHECK(sentry__path_is_file(att_file)); + + // refs.json exists + sentry_path_t *refs_path = sentry__path_join_str(event_dir, "refs.json"); + TEST_CHECK(sentry__path_is_file(refs_path)); + + sentry__path_free(refs_path); + sentry__path_free(att_file); + sentry__path_free(event_dir); + sentry__path_free(db_path); + sentry__attachment_free(attachment); + sentry__path_free(src_path); + sentry__path_free(run_path); + sentry_close(); +} + +static void +send_restore_envelope(sentry_envelope_t *envelope, void *data) +{ + int *called = data; + *called += 1; + sentry_envelope_free(envelope); +} + +SENTRY_TEST(attachment_ref_restore) +{ +#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS) + SKIP_TEST(); +#endif + const char *db_str = SENTRY_TEST_PATH_PREFIX ".sentry-native"; + + sentry_uuid_t event_id + = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); + + // set up old run dir with a stripped envelope (no attachment-ref items) + sentry_path_t *db_path = sentry__path_from_str(db_str); + sentry_path_t *old_run_path = sentry__path_join_str(db_path, "old.run"); + TEST_ASSERT(sentry__path_create_dir_all(old_run_path) == 0); + + // build attachment + refs.json in db/cache// + sentry_path_t *cache_dir = sentry__path_join_str(db_path, "cache"); + sentry_path_t *event_att_dir = sentry__path_join_str( + cache_dir, "c993afb6-b4ac-48a6-b61b-2558e601d65d"); + sentry__path_free(cache_dir); + TEST_ASSERT(sentry__path_create_dir_all(event_att_dir) == 0); + + sentry_path_t *att_file + = sentry__path_join_str(event_att_dir, "test_minidump.dmp"); + size_t large_size = 100 * 1024 * 1024; + FILE *f = fopen(att_file->path, "wb"); + TEST_ASSERT(!!f); + fseek(f, (long)(large_size - 1), SEEK_SET); + fputc(0, f); + fclose(f); + + // write refs.json + sentry_value_t refs = sentry_value_new_list(); + sentry_value_t ref_obj = sentry_value_new_object(); + sentry_value_set_by_key( + ref_obj, "filename", sentry_value_new_string("test_minidump.dmp")); + sentry_value_set_by_key( + ref_obj, "attachment_type", sentry_value_new_string("event.minidump")); + sentry_value_set_by_key(ref_obj, "attachment_length", + sentry_value_new_uint64((uint64_t)large_size)); + sentry_value_append(refs, ref_obj); + sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); + sentry__jsonwriter_write_value(jw, refs); + size_t refs_buf_len = 0; + char *refs_buf = sentry__jsonwriter_into_string(jw, &refs_buf_len); + sentry_value_decref(refs); + sentry_path_t *refs_path + = sentry__path_join_str(event_att_dir, "refs.json"); + TEST_ASSERT( + sentry__path_write_buffer(refs_path, refs_buf, refs_buf_len) == 0); + sentry_free(refs_buf); + + // build envelope with only the event (attachment-refs stripped at persist) + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry_value_t event = sentry_value_new_object(); + sentry_value_set_by_key( + event, "event_id", sentry__value_new_uuid(&event_id)); + sentry__envelope_add_event(envelope, event); + TEST_ASSERT(sentry__envelope_get_item_count(envelope) == 1); + + // write stripped envelope to old run dir + sentry_path_t *envelope_path = sentry__path_join_str( + old_run_path, "c993afb6-b4ac-48a6-b61b-2558e601d65d.envelope"); + TEST_ASSERT(sentry_envelope_write_to_path(envelope, envelope_path) == 0); + sentry_envelope_free(envelope); + + // init sentry with function transport — process_old_runs runs during init + int called = 0; + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_transport_t *transport = sentry_transport_new(send_restore_envelope); + sentry_transport_set_state(transport, &called); + sentry_options_set_transport(options, transport); + sentry_init(options); + + // transport callback should have been called with the raw envelope + TEST_CHECK(called >= 1); + + // attachment + refs.json remain in cache for transport to resolve via TUS + TEST_CHECK(sentry__path_is_file(att_file)); + TEST_CHECK(sentry__path_is_file(refs_path)); + + sentry__path_free(envelope_path); + sentry__path_free(refs_path); + sentry__path_free(att_file); + sentry__path_free(event_att_dir); + sentry__path_free(old_run_path); + sentry__path_free(db_path); + sentry_close(); +} + +SENTRY_TEST(deserialize_envelope_invalid) +{ + TEST_CHECK(!sentry_envelope_deserialize("", 0)); TEST_CHECK(!sentry_envelope_deserialize("{}", 0)); TEST_CHECK(!sentry_envelope_deserialize("\n", 1)); TEST_CHECK(!sentry_envelope_deserialize("{}\n{}", 5)); @@ -796,3 +1142,48 @@ SENTRY_TEST(deserialize_envelope_invalid) snprintf(buf, sizeof(buf), "{}\n{\"length\":%zu}\n", SIZE_MAX); TEST_CHECK(!sentry_envelope_deserialize(buf, strlen(buf))); } + +static bool +tus_mock_send(void *client, sentry_prepared_http_request_t *req, + sentry_http_response_t *resp) +{ + (void)client; + (void)req; + resp->status_code = 201; + resp->location = sentry__string_clone("https://sentry.invalid/upload/abc"); + return true; +} + +SENTRY_TEST(tus_file_attachment_preserves_original) +{ + const char *test_file_str + = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_preserve"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + + size_t large_size = 100 * 1024 * 1024; + FILE *f = fopen(test_file_str, "wb"); + TEST_CHECK(!!f); + fseek(f, (long)(large_size - 1), SEEK_SET); + fputc(0, f); + fclose(f); + + sentry_transport_t *transport + = sentry__http_transport_new(NULL, tus_mock_send); + TEST_CHECK(!!transport); + + SENTRY_TEST_OPTIONS_NEW(options); + sentry_options_set_dsn(options, "https://foo@sentry.invalid/42"); + sentry_options_set_transport(options, transport); + sentry_options_add_attachment(options, test_file_str); + sentry_init(options); + + sentry_capture_event( + sentry_value_new_message_event(SENTRY_LEVEL_INFO, NULL, "test")); + + sentry_close(); + + TEST_CHECK(sentry__path_is_file(test_file_path)); + + sentry__path_remove(test_file_path); + sentry__path_free(test_file_path); +} diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index 118795d78..736a2e590 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -74,9 +74,10 @@ typedef struct { } retry_test_ctx_t; static int -test_send_cb(sentry_envelope_t *envelope, void *_ctx) +test_send_cb(sentry_envelope_t *envelope, const char *uuid, void *_ctx) { (void)envelope; + (void)uuid; retry_test_ctx_t *ctx = _ctx; ctx->count++; return ctx->status_code; diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index afa3973c5..e13634f25 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -2,6 +2,11 @@ XX(assert_sdk_name) XX(assert_sdk_user_agent) XX(assert_sdk_version) XX(attachment_properties) +XX(attachment_ref_copy) +XX(attachment_ref_creation) +XX(attachment_ref_from_path) +XX(attachment_ref_move) +XX(attachment_ref_restore) XX(attachments_add_dedupe) XX(attachments_add_remove) XX(attachments_bytes) @@ -255,6 +260,7 @@ XX(transactions_skip_before_send) XX(transport_retry) XX(transport_sampling_transactions) XX(transport_sampling_transactions_set_trace) +XX(tus_file_attachment_preserves_original) XX(tus_request_preparation) XX(tus_upload_url) XX(txn_data) From 3928f0521d138a7be08e829c144036b2b2e787f4 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Mar 2026 17:32:58 +0100 Subject: [PATCH 109/121] refactor: centralize large attachment check in sentry__is_large_attachment() Restore the helper that was dropped during the tus-separate refactor, keeping the commented-out testing hook for small .dmp files. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_attachment.h | 9 +++++++++ src/sentry_core.c | 2 +- src/sentry_database.c | 2 +- src/sentry_envelope.c | 4 ++-- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/sentry_attachment.h b/src/sentry_attachment.h index ec38416ae..8e3d9d8c6 100644 --- a/src/sentry_attachment.h +++ b/src/sentry_attachment.h @@ -7,6 +7,15 @@ #define SENTRY_LARGE_ATTACHMENT_SIZE (100 * 1024 * 1024) // 100 MB +static inline bool +sentry__is_large_attachment(const sentry_path_t *path, size_t file_size) +{ + // TODO: for temporarily testing with <1 MB minidumps + // return file_size < 1024 * 1024 && sentry__path_ends_with(path, ".dmp"); + (void)path; + return file_size >= SENTRY_LARGE_ATTACHMENT_SIZE; +} + /** * The attachment_type. */ diff --git a/src/sentry_core.c b/src/sentry_core.c index 48f6fa1c6..81900b7ae 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -1761,7 +1761,7 @@ sentry_capture_minidump_n(const char *path, size_t path_len) // the minidump is added as an attachment, with the type // `event.minidump` size_t dump_size = sentry__path_get_size(dump_path); - bool is_large = dump_size >= SENTRY_LARGE_ATTACHMENT_SIZE; + bool is_large = sentry__is_large_attachment(dump_path, dump_size); sentry_envelope_item_t *item = sentry__envelope_add_from_path( envelope, dump_path, "attachment"); diff --git a/src/sentry_database.c b/src/sentry_database.c index c37d94cfc..0a9c2de86 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -187,7 +187,7 @@ sentry__cache_large_attachments(const sentry_path_t *cache_path, for (const sentry_attachment_t *att = attachments; att; att = att->next) { size_t file_size = sentry__attachment_get_size(att); - if (file_size < SENTRY_LARGE_ATTACHMENT_SIZE) { + if (!sentry__is_large_attachment(att->path, file_size)) { continue; } diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 8b451f7e8..cbeddb1ee 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -665,7 +665,7 @@ sentry__envelope_add_attachment( size_t file_size = sentry__attachment_get_size(attachment); - if (file_size >= SENTRY_LARGE_ATTACHMENT_SIZE) { + if (sentry__is_large_attachment(attachment->path, file_size)) { return NULL; } @@ -728,7 +728,7 @@ sentry__envelope_add_from_path( return NULL; } size_t file_size = sentry__path_get_size(path); - if (file_size >= SENTRY_LARGE_ATTACHMENT_SIZE) { + if (sentry__is_large_attachment(path, file_size)) { return NULL; } size_t buf_len; From a9e5b92bc4faf0ff9357283b600ee319a902b2c5 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Sat, 14 Mar 2026 20:21:24 +0100 Subject: [PATCH 110/121] feat(tus): split into separate creation and upload requests Switch from TUS "Creation With Upload" (single POST with body) to TUS "Creation" (POST without body) + upload (PATCH with body). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/transports/sentry_http_transport.c | 91 +++++++++++++++++++++----- src/transports/sentry_http_transport.h | 7 +- tests/test_integration_tus.py | 58 ++++++++++++---- tests/unit/test_envelopes.c | 51 ++++++++++++--- 4 files changed, 166 insertions(+), 41 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index c6524494e..0ec081422 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -224,10 +224,6 @@ prepare_tus_request_common( h->key = "x-sentry-auth"; h->value = sentry__dsn_get_auth_header(dsn, user_agent); - h = &req->headers[req->headers_len++]; - h->key = "content-type"; - h->value = sentry__string_clone(TUS_MIME); - h = &req->headers[req->headers_len++]; h->key = "tus-resumable"; h->value = sentry__string_clone("1.0.0"); @@ -240,18 +236,46 @@ prepare_tus_request_common( } static sentry_prepared_http_request_t * -prepare_tus_request(const sentry_path_t *path, size_t file_size, - const sentry_dsn_t *dsn, const char *user_agent) +prepare_tus_upload_request( + const char *location, const sentry_path_t *path, size_t file_size) { - if (!path) { + if (!location || !path) { return NULL; } + sentry_prepared_http_request_t *req - = prepare_tus_request_common(file_size, dsn, user_agent); - if (req) { - req->body_path = sentry__path_clone(path); - req->body_len = file_size; + = SENTRY_MAKE(sentry_prepared_http_request_t); + if (!req) { + return NULL; + } + memset(req, 0, sizeof(*req)); + + req->headers = sentry_malloc( + sizeof(sentry_prepared_http_header_t) * TUS_MAX_HTTP_HEADERS); + if (!req->headers) { + sentry_free(req); + return NULL; } + req->headers_len = 0; + + req->method = "PATCH"; + req->url = sentry__string_clone(location); + req->body_path = sentry__path_clone(path); + req->body_len = file_size; + + sentry_prepared_http_header_t *h; + h = &req->headers[req->headers_len++]; + h->key = "tus-resumable"; + h->value = sentry__string_clone("1.0.0"); + + h = &req->headers[req->headers_len++]; + h->key = "content-type"; + h->value = sentry__string_clone(TUS_MIME); + + h = &req->headers[req->headers_len++]; + h->key = "upload-offset"; + h->value = sentry__string_clone("0"); + return req; } @@ -306,8 +330,9 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, return false; } - sentry_prepared_http_request_t *req = prepare_tus_request( - att_file, file_size, state->dsn, state->user_agent); + // Step 1: TUS creation request (POST, no body) + sentry_prepared_http_request_t *req + = prepare_tus_request_common(file_size, state->dsn, state->user_agent); if (!req) { sentry__path_free(att_file); return false; @@ -338,6 +363,29 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, return false; } + // Step 2: TUS upload request (PATCH with file body) + req = prepare_tus_upload_request(resp.location, att_file, file_size); + char *location = resp.location; + resp.location = NULL; + http_response_cleanup(&resp); + + if (!req) { + sentry__path_free(att_file); + sentry_free(location); + return false; + } + + memset(&resp, 0, sizeof(resp)); + ok = state->send_func(state->client, req, &resp); + sentry__prepared_http_request_free(req); + http_response_cleanup(&resp); + + if (!ok || resp.status_code != 204) { + sentry__path_free(att_file); + sentry_free(location); + return false; + } + const char *ref_ct = sentry_value_as_string( sentry_value_get_by_key(entry, "content_type")); const char *att_type = sentry_value_as_string( @@ -345,12 +393,12 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, sentry_value_t att_length = sentry_value_get_by_key(entry, "attachment_length"); sentry_value_incref(att_length); - sentry__envelope_add_attachment_ref(send_envelope, resp.location, filename, + sentry__envelope_add_attachment_ref(send_envelope, location, filename, *ref_ct ? ref_ct : NULL, *att_type ? att_type : NULL, att_length); sentry__path_remove(att_file); sentry__path_free(att_file); - http_response_cleanup(&resp); + sentry_free(location); return true; } @@ -697,9 +745,16 @@ sentry__http_transport_get_bgworker(sentry_transport_t *transport) } sentry_prepared_http_request_t * -sentry__prepare_tus_request(const sentry_path_t *path, size_t file_size, - const sentry_dsn_t *dsn, const char *user_agent) +sentry__prepare_tus_create_request( + size_t file_size, const sentry_dsn_t *dsn, const char *user_agent) +{ + return prepare_tus_request_common(file_size, dsn, user_agent); +} + +sentry_prepared_http_request_t * +sentry__prepare_tus_upload_request( + const char *location, const sentry_path_t *path, size_t file_size) { - return prepare_tus_request(path, file_size, dsn, user_agent); + return prepare_tus_upload_request(location, path, file_size); } #endif diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index c89204800..b8d1f79ae 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -55,9 +55,10 @@ void sentry__http_transport_set_shutdown_client( #ifdef SENTRY_UNITTEST void *sentry__http_transport_get_bgworker(sentry_transport_t *transport); -sentry_prepared_http_request_t *sentry__prepare_tus_request( - const sentry_path_t *path, size_t file_size, const sentry_dsn_t *dsn, - const char *user_agent); +sentry_prepared_http_request_t *sentry__prepare_tus_create_request( + size_t file_size, const sentry_dsn_t *dsn, const char *user_agent); +sentry_prepared_http_request_t *sentry__prepare_tus_upload_request( + const char *location, const sentry_path_t *path, size_t file_size); #endif #endif diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index e923744e4..c090669a0 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -26,8 +26,11 @@ def test_tus_upload_large_attachment(cmake, httpserver): {"SENTRY_BACKEND": "none", "SENTRY_TRANSPORT_COMPRESSION": "Off"}, ) - location = "/api/123456/upload/abc123def456789/?length=104857600&signature=xyz" + upload_uri = "/api/123456/upload/abc123def456789/" + upload_qs = "length=104857600&signature=xyz" + location = httpserver.url_for(upload_uri) + "?" + upload_qs + # TUS creation request (POST, no body) -> 201 + Location httpserver.expect_oneshot_request( "/api/123456/upload/", headers={"tus-resumable": "1.0.0"}, @@ -37,6 +40,14 @@ def test_tus_upload_large_attachment(cmake, httpserver): headers={"Location": location}, ) + # TUS upload request (PATCH with body) -> 204 + httpserver.expect_oneshot_request( + upload_uri, + method="PATCH", + headers={"tus-resumable": "1.0.0"}, + query_string=upload_qs, + ).respond_with_data("", status=204) + httpserver.expect_request( "/api/123456/envelope/", headers={"x-sentry-auth": auth_header}, @@ -51,27 +62,35 @@ def test_tus_upload_large_attachment(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 3 + assert len(httpserver.log) == 4 - # Find the upload request and envelope requests + # Find the create, upload, and envelope requests + create_req = None upload_req = None envelope_reqs = [] for entry in httpserver.log: req = entry[0] - if "/upload/" in req.path: + if req.path == "/api/123456/upload/" and req.method == "POST": + create_req = req + elif upload_uri in req.path and req.method == "PATCH": upload_req = req elif "/envelope/" in req.path: envelope_reqs.append(req) + assert create_req is not None assert upload_req is not None assert len(envelope_reqs) == 2 + # Verify TUS creation request headers + assert create_req.headers.get("tus-resumable") == "1.0.0" + upload_length = create_req.headers.get("upload-length") + assert upload_length is not None + assert int(upload_length) == 100 * 1024 * 1024 + # Verify TUS upload request headers assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" - upload_length = upload_req.headers.get("upload-length") - assert upload_length is not None - assert int(upload_length) == 100 * 1024 * 1024 + assert upload_req.headers.get("upload-offset") == "0" # One envelope has the resolved attachment-refs, the other is the original attachment_ref = None @@ -183,7 +202,9 @@ def test_tus_crash_restart(cmake, httpserver): att_size = os.path.getsize(os.path.join(att_dir, att_files[0])) assert att_size >= 100 * 1024 * 1024 - location = "/api/123456/upload/abc123def456789/?length=104857600&signature=xyz" + upload_uri = "/api/123456/upload/abc123def456789/" + upload_qs = "length=104857600&signature=xyz" + location = httpserver.url_for(upload_uri) + "?" + upload_qs # Second run: restart picks up crash and uploads via TUS httpserver.expect_oneshot_request( @@ -195,6 +216,13 @@ def test_tus_crash_restart(cmake, httpserver): headers={"Location": location}, ) + httpserver.expect_oneshot_request( + upload_uri, + method="PATCH", + headers={"tus-resumable": "1.0.0"}, + query_string=upload_qs, + ).respond_with_data("", status=204) + httpserver.expect_request( "/api/123456/envelope/", headers={"x-sentry-auth": auth_header}, @@ -207,24 +235,32 @@ def test_tus_crash_restart(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 3 + assert len(httpserver.log) == 4 + create_req = None upload_req = None envelope_reqs = [] for entry in httpserver.log: req = entry[0] - if "/upload/" in req.path: + if req.path == "/api/123456/upload/" and req.method == "POST": + create_req = req + elif upload_uri in req.path and req.method == "PATCH": upload_req = req elif "/envelope/" in req.path: envelope_reqs.append(req) + assert create_req is not None assert upload_req is not None assert len(envelope_reqs) == 2 + # Verify TUS creation request headers + assert create_req.headers.get("tus-resumable") == "1.0.0" + assert int(create_req.headers.get("upload-length")) == 100 * 1024 * 1024 + # Verify TUS upload request headers assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" - assert int(upload_req.headers.get("upload-length")) == 100 * 1024 * 1024 + assert upload_req.headers.get("upload-offset") == "0" # One envelope has the resolved attachment-refs, the other is the original attachment_ref = None diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index faf55a0fc..a84c14ee8 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -742,19 +742,15 @@ SENTRY_TEST(tus_request_preparation) { SENTRY_TEST_DSN_NEW_DEFAULT(dsn); - const char *test_file_str = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; - sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); - TEST_CHECK_INT_EQUAL( - sentry__path_write_buffer(test_file_path, "test-data", 9), 0); - + // Test creation request (POST, no body) sentry_prepared_http_request_t *req - = sentry__prepare_tus_request(test_file_path, 9, dsn, NULL); + = sentry__prepare_tus_create_request(9, dsn, NULL); TEST_CHECK(!!req); TEST_CHECK_STRING_EQUAL(req->method, "POST"); TEST_CHECK_STRING_EQUAL( req->url, "https://sentry.invalid:443/api/42/upload/"); - TEST_CHECK(!!req->body_path); - TEST_CHECK_INT_EQUAL(req->body_len, 9); + TEST_CHECK(!req->body_path); + TEST_CHECK_INT_EQUAL(req->body_len, 0); TEST_CHECK(!req->body); bool has_tus_resumable = false; @@ -769,15 +765,52 @@ SENTRY_TEST(tus_request_preparation) TEST_CHECK_STRING_EQUAL(req->headers[i].value, "9"); has_upload_length = true; } + if (strcmp(req->headers[i].key, "content-type") == 0) { + has_content_type = true; + } + } + TEST_CHECK(has_tus_resumable); + TEST_CHECK(has_upload_length); + TEST_CHECK(!has_content_type); + + sentry__prepared_http_request_free(req); + + // Test upload request (PATCH with body) + const char *test_file_str = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + TEST_CHECK_INT_EQUAL( + sentry__path_write_buffer(test_file_path, "test-data", 9), 0); + + const char *location = "https://sentry.invalid/api/42/upload/abc123/"; + req = sentry__prepare_tus_upload_request(location, test_file_path, 9); + TEST_CHECK(!!req); + TEST_CHECK_STRING_EQUAL(req->method, "PATCH"); + TEST_CHECK_STRING_EQUAL(req->url, location); + TEST_CHECK(!!req->body_path); + TEST_CHECK_INT_EQUAL(req->body_len, 9); + TEST_CHECK(!req->body); + + has_tus_resumable = false; + has_content_type = false; + bool has_upload_offset = false; + for (size_t i = 0; i < req->headers_len; i++) { + if (strcmp(req->headers[i].key, "tus-resumable") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "1.0.0"); + has_tus_resumable = true; + } if (strcmp(req->headers[i].key, "content-type") == 0) { TEST_CHECK_STRING_EQUAL( req->headers[i].value, "application/offset+octet-stream"); has_content_type = true; } + if (strcmp(req->headers[i].key, "upload-offset") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "0"); + has_upload_offset = true; + } } TEST_CHECK(has_tus_resumable); - TEST_CHECK(has_upload_length); TEST_CHECK(has_content_type); + TEST_CHECK(has_upload_offset); sentry__prepared_http_request_free(req); sentry__path_remove(test_file_path); From c5ea0595bf505f273ce505f3e971c78cb3127a37 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Mon, 16 Mar 2026 12:27:23 +0100 Subject: [PATCH 111/121] Revert "feat(tus): split into separate creation and upload requests" This temporarily reverts commit 5a4097f1db4ae749370b0d82a2dc447ce5e8fc78 until it's supported on the server side. --- src/transports/sentry_http_transport.c | 91 +++++--------------------- src/transports/sentry_http_transport.h | 7 +- tests/test_integration_tus.py | 58 ++++------------ tests/unit/test_envelopes.c | 51 +++------------ 4 files changed, 41 insertions(+), 166 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 0ec081422..c6524494e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -224,6 +224,10 @@ prepare_tus_request_common( h->key = "x-sentry-auth"; h->value = sentry__dsn_get_auth_header(dsn, user_agent); + h = &req->headers[req->headers_len++]; + h->key = "content-type"; + h->value = sentry__string_clone(TUS_MIME); + h = &req->headers[req->headers_len++]; h->key = "tus-resumable"; h->value = sentry__string_clone("1.0.0"); @@ -236,46 +240,18 @@ prepare_tus_request_common( } static sentry_prepared_http_request_t * -prepare_tus_upload_request( - const char *location, const sentry_path_t *path, size_t file_size) +prepare_tus_request(const sentry_path_t *path, size_t file_size, + const sentry_dsn_t *dsn, const char *user_agent) { - if (!location || !path) { + if (!path) { return NULL; } - sentry_prepared_http_request_t *req - = SENTRY_MAKE(sentry_prepared_http_request_t); - if (!req) { - return NULL; - } - memset(req, 0, sizeof(*req)); - - req->headers = sentry_malloc( - sizeof(sentry_prepared_http_header_t) * TUS_MAX_HTTP_HEADERS); - if (!req->headers) { - sentry_free(req); - return NULL; + = prepare_tus_request_common(file_size, dsn, user_agent); + if (req) { + req->body_path = sentry__path_clone(path); + req->body_len = file_size; } - req->headers_len = 0; - - req->method = "PATCH"; - req->url = sentry__string_clone(location); - req->body_path = sentry__path_clone(path); - req->body_len = file_size; - - sentry_prepared_http_header_t *h; - h = &req->headers[req->headers_len++]; - h->key = "tus-resumable"; - h->value = sentry__string_clone("1.0.0"); - - h = &req->headers[req->headers_len++]; - h->key = "content-type"; - h->value = sentry__string_clone(TUS_MIME); - - h = &req->headers[req->headers_len++]; - h->key = "upload-offset"; - h->value = sentry__string_clone("0"); - return req; } @@ -330,9 +306,8 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, return false; } - // Step 1: TUS creation request (POST, no body) - sentry_prepared_http_request_t *req - = prepare_tus_request_common(file_size, state->dsn, state->user_agent); + sentry_prepared_http_request_t *req = prepare_tus_request( + att_file, file_size, state->dsn, state->user_agent); if (!req) { sentry__path_free(att_file); return false; @@ -363,29 +338,6 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, return false; } - // Step 2: TUS upload request (PATCH with file body) - req = prepare_tus_upload_request(resp.location, att_file, file_size); - char *location = resp.location; - resp.location = NULL; - http_response_cleanup(&resp); - - if (!req) { - sentry__path_free(att_file); - sentry_free(location); - return false; - } - - memset(&resp, 0, sizeof(resp)); - ok = state->send_func(state->client, req, &resp); - sentry__prepared_http_request_free(req); - http_response_cleanup(&resp); - - if (!ok || resp.status_code != 204) { - sentry__path_free(att_file); - sentry_free(location); - return false; - } - const char *ref_ct = sentry_value_as_string( sentry_value_get_by_key(entry, "content_type")); const char *att_type = sentry_value_as_string( @@ -393,12 +345,12 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, sentry_value_t att_length = sentry_value_get_by_key(entry, "attachment_length"); sentry_value_incref(att_length); - sentry__envelope_add_attachment_ref(send_envelope, location, filename, + sentry__envelope_add_attachment_ref(send_envelope, resp.location, filename, *ref_ct ? ref_ct : NULL, *att_type ? att_type : NULL, att_length); sentry__path_remove(att_file); sentry__path_free(att_file); - sentry_free(location); + http_response_cleanup(&resp); return true; } @@ -745,16 +697,9 @@ sentry__http_transport_get_bgworker(sentry_transport_t *transport) } sentry_prepared_http_request_t * -sentry__prepare_tus_create_request( - size_t file_size, const sentry_dsn_t *dsn, const char *user_agent) -{ - return prepare_tus_request_common(file_size, dsn, user_agent); -} - -sentry_prepared_http_request_t * -sentry__prepare_tus_upload_request( - const char *location, const sentry_path_t *path, size_t file_size) +sentry__prepare_tus_request(const sentry_path_t *path, size_t file_size, + const sentry_dsn_t *dsn, const char *user_agent) { - return prepare_tus_upload_request(location, path, file_size); + return prepare_tus_request(path, file_size, dsn, user_agent); } #endif diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index b8d1f79ae..c89204800 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -55,10 +55,9 @@ void sentry__http_transport_set_shutdown_client( #ifdef SENTRY_UNITTEST void *sentry__http_transport_get_bgworker(sentry_transport_t *transport); -sentry_prepared_http_request_t *sentry__prepare_tus_create_request( - size_t file_size, const sentry_dsn_t *dsn, const char *user_agent); -sentry_prepared_http_request_t *sentry__prepare_tus_upload_request( - const char *location, const sentry_path_t *path, size_t file_size); +sentry_prepared_http_request_t *sentry__prepare_tus_request( + const sentry_path_t *path, size_t file_size, const sentry_dsn_t *dsn, + const char *user_agent); #endif #endif diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index c090669a0..e923744e4 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -26,11 +26,8 @@ def test_tus_upload_large_attachment(cmake, httpserver): {"SENTRY_BACKEND": "none", "SENTRY_TRANSPORT_COMPRESSION": "Off"}, ) - upload_uri = "/api/123456/upload/abc123def456789/" - upload_qs = "length=104857600&signature=xyz" - location = httpserver.url_for(upload_uri) + "?" + upload_qs + location = "/api/123456/upload/abc123def456789/?length=104857600&signature=xyz" - # TUS creation request (POST, no body) -> 201 + Location httpserver.expect_oneshot_request( "/api/123456/upload/", headers={"tus-resumable": "1.0.0"}, @@ -40,14 +37,6 @@ def test_tus_upload_large_attachment(cmake, httpserver): headers={"Location": location}, ) - # TUS upload request (PATCH with body) -> 204 - httpserver.expect_oneshot_request( - upload_uri, - method="PATCH", - headers={"tus-resumable": "1.0.0"}, - query_string=upload_qs, - ).respond_with_data("", status=204) - httpserver.expect_request( "/api/123456/envelope/", headers={"x-sentry-auth": auth_header}, @@ -62,35 +51,27 @@ def test_tus_upload_large_attachment(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 4 + assert len(httpserver.log) == 3 - # Find the create, upload, and envelope requests - create_req = None + # Find the upload request and envelope requests upload_req = None envelope_reqs = [] for entry in httpserver.log: req = entry[0] - if req.path == "/api/123456/upload/" and req.method == "POST": - create_req = req - elif upload_uri in req.path and req.method == "PATCH": + if "/upload/" in req.path: upload_req = req elif "/envelope/" in req.path: envelope_reqs.append(req) - assert create_req is not None assert upload_req is not None assert len(envelope_reqs) == 2 - # Verify TUS creation request headers - assert create_req.headers.get("tus-resumable") == "1.0.0" - upload_length = create_req.headers.get("upload-length") - assert upload_length is not None - assert int(upload_length) == 100 * 1024 * 1024 - # Verify TUS upload request headers assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" - assert upload_req.headers.get("upload-offset") == "0" + upload_length = upload_req.headers.get("upload-length") + assert upload_length is not None + assert int(upload_length) == 100 * 1024 * 1024 # One envelope has the resolved attachment-refs, the other is the original attachment_ref = None @@ -202,9 +183,7 @@ def test_tus_crash_restart(cmake, httpserver): att_size = os.path.getsize(os.path.join(att_dir, att_files[0])) assert att_size >= 100 * 1024 * 1024 - upload_uri = "/api/123456/upload/abc123def456789/" - upload_qs = "length=104857600&signature=xyz" - location = httpserver.url_for(upload_uri) + "?" + upload_qs + location = "/api/123456/upload/abc123def456789/?length=104857600&signature=xyz" # Second run: restart picks up crash and uploads via TUS httpserver.expect_oneshot_request( @@ -216,13 +195,6 @@ def test_tus_crash_restart(cmake, httpserver): headers={"Location": location}, ) - httpserver.expect_oneshot_request( - upload_uri, - method="PATCH", - headers={"tus-resumable": "1.0.0"}, - query_string=upload_qs, - ).respond_with_data("", status=204) - httpserver.expect_request( "/api/123456/envelope/", headers={"x-sentry-auth": auth_header}, @@ -235,32 +207,24 @@ def test_tus_crash_restart(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 4 + assert len(httpserver.log) == 3 - create_req = None upload_req = None envelope_reqs = [] for entry in httpserver.log: req = entry[0] - if req.path == "/api/123456/upload/" and req.method == "POST": - create_req = req - elif upload_uri in req.path and req.method == "PATCH": + if "/upload/" in req.path: upload_req = req elif "/envelope/" in req.path: envelope_reqs.append(req) - assert create_req is not None assert upload_req is not None assert len(envelope_reqs) == 2 - # Verify TUS creation request headers - assert create_req.headers.get("tus-resumable") == "1.0.0" - assert int(create_req.headers.get("upload-length")) == 100 * 1024 * 1024 - # Verify TUS upload request headers assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" - assert upload_req.headers.get("upload-offset") == "0" + assert int(upload_req.headers.get("upload-length")) == 100 * 1024 * 1024 # One envelope has the resolved attachment-refs, the other is the original attachment_ref = None diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index a84c14ee8..faf55a0fc 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -742,15 +742,19 @@ SENTRY_TEST(tus_request_preparation) { SENTRY_TEST_DSN_NEW_DEFAULT(dsn); - // Test creation request (POST, no body) + const char *test_file_str = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + TEST_CHECK_INT_EQUAL( + sentry__path_write_buffer(test_file_path, "test-data", 9), 0); + sentry_prepared_http_request_t *req - = sentry__prepare_tus_create_request(9, dsn, NULL); + = sentry__prepare_tus_request(test_file_path, 9, dsn, NULL); TEST_CHECK(!!req); TEST_CHECK_STRING_EQUAL(req->method, "POST"); TEST_CHECK_STRING_EQUAL( req->url, "https://sentry.invalid:443/api/42/upload/"); - TEST_CHECK(!req->body_path); - TEST_CHECK_INT_EQUAL(req->body_len, 0); + TEST_CHECK(!!req->body_path); + TEST_CHECK_INT_EQUAL(req->body_len, 9); TEST_CHECK(!req->body); bool has_tus_resumable = false; @@ -765,52 +769,15 @@ SENTRY_TEST(tus_request_preparation) TEST_CHECK_STRING_EQUAL(req->headers[i].value, "9"); has_upload_length = true; } - if (strcmp(req->headers[i].key, "content-type") == 0) { - has_content_type = true; - } - } - TEST_CHECK(has_tus_resumable); - TEST_CHECK(has_upload_length); - TEST_CHECK(!has_content_type); - - sentry__prepared_http_request_free(req); - - // Test upload request (PATCH with body) - const char *test_file_str = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; - sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); - TEST_CHECK_INT_EQUAL( - sentry__path_write_buffer(test_file_path, "test-data", 9), 0); - - const char *location = "https://sentry.invalid/api/42/upload/abc123/"; - req = sentry__prepare_tus_upload_request(location, test_file_path, 9); - TEST_CHECK(!!req); - TEST_CHECK_STRING_EQUAL(req->method, "PATCH"); - TEST_CHECK_STRING_EQUAL(req->url, location); - TEST_CHECK(!!req->body_path); - TEST_CHECK_INT_EQUAL(req->body_len, 9); - TEST_CHECK(!req->body); - - has_tus_resumable = false; - has_content_type = false; - bool has_upload_offset = false; - for (size_t i = 0; i < req->headers_len; i++) { - if (strcmp(req->headers[i].key, "tus-resumable") == 0) { - TEST_CHECK_STRING_EQUAL(req->headers[i].value, "1.0.0"); - has_tus_resumable = true; - } if (strcmp(req->headers[i].key, "content-type") == 0) { TEST_CHECK_STRING_EQUAL( req->headers[i].value, "application/offset+octet-stream"); has_content_type = true; } - if (strcmp(req->headers[i].key, "upload-offset") == 0) { - TEST_CHECK_STRING_EQUAL(req->headers[i].value, "0"); - has_upload_offset = true; - } } TEST_CHECK(has_tus_resumable); + TEST_CHECK(has_upload_length); TEST_CHECK(has_content_type); - TEST_CHECK(has_upload_offset); sentry__prepared_http_request_free(req); sentry__path_remove(test_file_path); From 0b609aa6a415004fccf592494136388fd2ed9d77 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 18 Mar 2026 14:33:46 +0100 Subject: [PATCH 112/121] rename refs.json to __sentry-attachments.json to avoid conflicts The cache directory will contain actual attachment files alongside this metadata file, so use a namespaced name to prevent collisions. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_database.c | 6 +++--- src/sentry_database.h | 2 +- src/transports/sentry_http_transport.c | 3 ++- tests/test_integration_tus.py | 2 +- tests/unit/test_envelopes.c | 19 +++++++++++-------- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/sentry_database.c b/src/sentry_database.c index 0a9c2de86..2fc64ece3 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -247,12 +247,12 @@ sentry__cache_large_attachments(const sentry_path_t *cache_path, size_t buf_len = 0; char *buf = sentry__jsonwriter_into_string(jw, &buf_len); if (buf) { - sentry_path_t *refs_path - = sentry__path_join_str(event_dir, "refs.json"); + sentry_path_t *refs_path = sentry__path_join_str( + event_dir, "__sentry-attachments.json"); if (refs_path) { int rv = sentry__path_write_buffer(refs_path, buf, buf_len); if (rv != 0) { - SENTRY_WARN("writing refs.json failed"); + SENTRY_WARN("writing __sentry-attachments.json failed"); } sentry__path_free(refs_path); } diff --git a/src/sentry_database.h b/src/sentry_database.h index d73888676..06bba23b9 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -141,7 +141,7 @@ bool sentry__clear_crash_marker(const sentry_options_t *options); /** * Cache large attachments (>= SENTRY_LARGE_ATTACHMENT_SIZE) to - * `//` and write `refs.json` metadata. + * `//` and write `__sentry-attachments.json` metadata. * * When `run_path` is non-NULL and a file attachment's parent directory * matches it, the file is renamed instead of copied. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index c6524494e..918c03c19 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -368,7 +368,8 @@ tus_resolve_and_send(http_transport_state_t *state, const char *uuid) return; } - sentry_path_t *refs_path = sentry__path_join_str(att_dir, "refs.json"); + sentry_path_t *refs_path + = sentry__path_join_str(att_dir, "__sentry-attachments.json"); if (!refs_path) { sentry__path_free(att_dir); return; diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index e923744e4..e1b861870 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -177,7 +177,7 @@ def test_tus_crash_restart(cmake, httpserver): for f in os.listdir(att_dir) if not f.endswith(".json") and os.path.isfile(os.path.join(att_dir, f)) ] - refs_files = [f for f in os.listdir(att_dir) if f == "refs.json"] + refs_files = [f for f in os.listdir(att_dir) if f == "__sentry-attachments.json"] assert len(att_files) > 0 assert len(refs_files) > 0 att_size = os.path.getsize(os.path.join(att_dir, att_files[0])) diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index faf55a0fc..ed48cfed5 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -932,8 +932,9 @@ SENTRY_TEST(attachment_ref_copy) = sentry__path_join_str(event_dir, "sentry_test_large_attachment"); TEST_CHECK(sentry__path_is_file(att_file)); - // refs.json exists with correct metadata - sentry_path_t *refs_path = sentry__path_join_str(event_dir, "refs.json"); + // __sentry-attachments.json exists with correct metadata + sentry_path_t *refs_path + = sentry__path_join_str(event_dir, "__sentry-attachments.json"); TEST_CHECK(sentry__path_is_file(refs_path)); size_t refs_len = 0; @@ -1016,8 +1017,9 @@ SENTRY_TEST(attachment_ref_move) = sentry__path_join_str(event_dir, "test_minidump.dmp"); TEST_CHECK(sentry__path_is_file(att_file)); - // refs.json exists - sentry_path_t *refs_path = sentry__path_join_str(event_dir, "refs.json"); + // __sentry-attachments.json exists + sentry_path_t *refs_path + = sentry__path_join_str(event_dir, "__sentry-attachments.json"); TEST_CHECK(sentry__path_is_file(refs_path)); sentry__path_free(refs_path); @@ -1053,7 +1055,7 @@ SENTRY_TEST(attachment_ref_restore) sentry_path_t *old_run_path = sentry__path_join_str(db_path, "old.run"); TEST_ASSERT(sentry__path_create_dir_all(old_run_path) == 0); - // build attachment + refs.json in db/cache// + // build attachment + __sentry-attachments.json in db/cache// sentry_path_t *cache_dir = sentry__path_join_str(db_path, "cache"); sentry_path_t *event_att_dir = sentry__path_join_str( cache_dir, "c993afb6-b4ac-48a6-b61b-2558e601d65d"); @@ -1069,7 +1071,7 @@ SENTRY_TEST(attachment_ref_restore) fputc(0, f); fclose(f); - // write refs.json + // write __sentry-attachments.json sentry_value_t refs = sentry_value_new_list(); sentry_value_t ref_obj = sentry_value_new_object(); sentry_value_set_by_key( @@ -1085,7 +1087,7 @@ SENTRY_TEST(attachment_ref_restore) char *refs_buf = sentry__jsonwriter_into_string(jw, &refs_buf_len); sentry_value_decref(refs); sentry_path_t *refs_path - = sentry__path_join_str(event_att_dir, "refs.json"); + = sentry__path_join_str(event_att_dir, "__sentry-attachments.json"); TEST_ASSERT( sentry__path_write_buffer(refs_path, refs_buf, refs_buf_len) == 0); sentry_free(refs_buf); @@ -1116,7 +1118,8 @@ SENTRY_TEST(attachment_ref_restore) // transport callback should have been called with the raw envelope TEST_CHECK(called >= 1); - // attachment + refs.json remain in cache for transport to resolve via TUS + // attachment + __sentry-attachments.json remain in cache for transport to + // resolve via TUS TEST_CHECK(sentry__path_is_file(att_file)); TEST_CHECK(sentry__path_is_file(refs_path)); From 8bae6b392fe3ddf7a13d75dd9754bb110a1c3d9a Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Wed, 18 Mar 2026 16:12:01 +0100 Subject: [PATCH 113/121] refactor(tus): cache external attachments by type, choose upload method at transport time Replace sentry__is_large_attachment(path, size) with sentry__attachment_is_external(att) which returns true for MINIDUMP type or size >= SENTRY_LARGE_ATTACHMENT_SIZE. Minidumps are always cached as external files; other large attachments continue to be cached by size. Rename sentry__cache_large_attachments to sentry__cache_external_attachments. At transport time, resolve_and_send_external_attachments checks each cached attachment: large files with TUS support go via TUS upload, smaller files are inlined into the envelope. For raw envelopes (read from disk on restart), sentry__envelope_append_raw_attachment builds the item bytes and appends them directly to the raw payload buffer. External attachment cleanup is deferred until after successful send so files survive for retry on network failure. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/backends/sentry_backend_breakpad.cpp | 2 +- src/backends/sentry_backend_crashpad.cpp | 2 +- src/sentry_attachment.h | 33 +++--- src/sentry_core.c | 10 +- src/sentry_database.c | 4 +- src/sentry_database.h | 4 +- src/sentry_envelope.c | 79 ++++++++++++++- src/sentry_envelope.h | 8 ++ src/transports/sentry_http_transport.c | 122 ++++++++++++++++++----- tests/unit/test_envelopes.c | 35 +++---- 10 files changed, 228 insertions(+), 71 deletions(-) diff --git a/src/backends/sentry_backend_breakpad.cpp b/src/backends/sentry_backend_breakpad.cpp index acf17ce1f..a894cab42 100644 --- a/src/backends/sentry_backend_breakpad.cpp +++ b/src/backends/sentry_backend_breakpad.cpp @@ -186,7 +186,7 @@ breakpad_backend_callback(const google_breakpad::MinidumpDescriptor &descriptor, tmp.path = dump_path; tmp.type = MINIDUMP; tmp.next = nullptr; - sentry__cache_large_attachments( + sentry__cache_external_attachments( options->run->cache_path, &event_id, &tmp, nullptr); } diff --git a/src/backends/sentry_backend_crashpad.cpp b/src/backends/sentry_backend_crashpad.cpp index 78af953e5..29b90662d 100644 --- a/src/backends/sentry_backend_crashpad.cpp +++ b/src/backends/sentry_backend_crashpad.cpp @@ -531,7 +531,7 @@ report_to_envelope(const crashpad::CrashReportDatabase::Report &report, sentry__envelope_add_attachments(envelope, attachments); sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); if (options->run) { - sentry__cache_large_attachments(options->run->cache_path, + sentry__cache_external_attachments(options->run->cache_path, &event_id, attachments, options->run->run_path); } } else { diff --git a/src/sentry_attachment.h b/src/sentry_attachment.h index 8e3d9d8c6..a211573e9 100644 --- a/src/sentry_attachment.h +++ b/src/sentry_attachment.h @@ -7,15 +7,6 @@ #define SENTRY_LARGE_ATTACHMENT_SIZE (100 * 1024 * 1024) // 100 MB -static inline bool -sentry__is_large_attachment(const sentry_path_t *path, size_t file_size) -{ - // TODO: for temporarily testing with <1 MB minidumps - // return file_size < 1024 * 1024 && sentry__path_ends_with(path, ".dmp"); - (void)path; - return file_size >= SENTRY_LARGE_ATTACHMENT_SIZE; -} - /** * The attachment_type. */ @@ -51,6 +42,24 @@ struct sentry_attachment_s { sentry_attachment_t *next; // Linked list pointer }; +/** + * Returns the size in bytes of the attachment's data (buffer length or file + * size). + */ +size_t sentry__attachment_get_size(const sentry_attachment_t *attachment); + +/** + * Returns true if the attachment should be cached as an external file. + * Minidumps are always external; other attachments are external when they + * exceed the large attachment size threshold. + */ +static inline bool +sentry__attachment_is_external(const sentry_attachment_t *att) +{ + return att->type == MINIDUMP + || sentry__attachment_get_size(att) >= SENTRY_LARGE_ATTACHMENT_SIZE; +} + /** * Creates a new file attachment. Takes ownership of `path`. */ @@ -100,12 +109,6 @@ void sentry__attachments_remove( void sentry__attachments_extend( sentry_attachment_t **attachments_ptr, sentry_attachment_t *attachments); -/** - * Returns the size in bytes of the attachment's data (buffer length or file - * size). - */ -size_t sentry__attachment_get_size(const sentry_attachment_t *attachment); - /** * Returns the filename string for the attachment (basename of `filename` if * set, otherwise basename of `path`). diff --git a/src/sentry_core.c b/src/sentry_core.c index 81900b7ae..d3996bb07 100644 --- a/src/sentry_core.c +++ b/src/sentry_core.c @@ -731,8 +731,8 @@ sentry__prepare_event(const sentry_options_t *options, sentry_value_t event, = all_attachments ? all_attachments : scope->attachments; sentry__envelope_add_attachments(envelope, atts); if (options->run) { - sentry__cache_large_attachments(options->run->cache_path, event_id, - atts, options->run->run_path); + sentry__cache_external_attachments(options->run->cache_path, + event_id, atts, options->run->run_path); } } @@ -822,7 +822,7 @@ prepare_user_feedback(sentry_value_t user_feedback, sentry_hint_t *hint) sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope); SENTRY_WITH_OPTIONS (options) { if (options->run) { - sentry__cache_large_attachments(options->run->cache_path, + sentry__cache_external_attachments(options->run->cache_path, &event_id, hint->attachments, options->run->run_path); } } @@ -1761,7 +1761,7 @@ sentry_capture_minidump_n(const char *path, size_t path_len) // the minidump is added as an attachment, with the type // `event.minidump` size_t dump_size = sentry__path_get_size(dump_path); - bool is_large = sentry__is_large_attachment(dump_path, dump_size); + bool is_large = dump_size >= SENTRY_LARGE_ATTACHMENT_SIZE; sentry_envelope_item_t *item = sentry__envelope_add_from_path( envelope, dump_path, "attachment"); @@ -1776,7 +1776,7 @@ sentry_capture_minidump_n(const char *path, size_t path_len) tmp.path = dump_path; tmp.type = MINIDUMP; tmp.next = NULL; - sentry__cache_large_attachments( + sentry__cache_external_attachments( options->run->cache_path, &event_id, &tmp, NULL); } else { sentry_envelope_free(envelope); diff --git a/src/sentry_database.c b/src/sentry_database.c index 2fc64ece3..481c3256c 100644 --- a/src/sentry_database.c +++ b/src/sentry_database.c @@ -166,7 +166,7 @@ write_envelope(const sentry_path_t *path, const sentry_envelope_t *envelope, } void -sentry__cache_large_attachments(const sentry_path_t *cache_path, +sentry__cache_external_attachments(const sentry_path_t *cache_path, const sentry_uuid_t *event_id, const sentry_attachment_t *attachments, const sentry_path_t *run_path) { @@ -187,7 +187,7 @@ sentry__cache_large_attachments(const sentry_path_t *cache_path, for (const sentry_attachment_t *att = attachments; att; att = att->next) { size_t file_size = sentry__attachment_get_size(att); - if (!sentry__is_large_attachment(att->path, file_size)) { + if (!sentry__attachment_is_external(att)) { continue; } diff --git a/src/sentry_database.h b/src/sentry_database.h index 06bba23b9..574845992 100644 --- a/src/sentry_database.h +++ b/src/sentry_database.h @@ -140,13 +140,13 @@ bool sentry__has_crash_marker(const sentry_options_t *options); bool sentry__clear_crash_marker(const sentry_options_t *options); /** - * Cache large attachments (>= SENTRY_LARGE_ATTACHMENT_SIZE) to + * Cache external attachments (e.g. minidumps) to * `//` and write `__sentry-attachments.json` metadata. * * When `run_path` is non-NULL and a file attachment's parent directory * matches it, the file is renamed instead of copied. */ -void sentry__cache_large_attachments(const sentry_path_t *cache_path, +void sentry__cache_external_attachments(const sentry_path_t *cache_path, const sentry_uuid_t *event_id, const sentry_attachment_t *attachments, const sentry_path_t *run_path); diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index cbeddb1ee..ef4a1004e 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -663,9 +663,7 @@ sentry__envelope_add_attachment( return NULL; } - size_t file_size = sentry__attachment_get_size(attachment); - - if (sentry__is_large_attachment(attachment->path, file_size)) { + if (sentry__attachment_is_external(attachment)) { return NULL; } @@ -728,7 +726,7 @@ sentry__envelope_add_from_path( return NULL; } size_t file_size = sentry__path_get_size(path); - if (sentry__is_large_attachment(path, file_size)) { + if (file_size >= SENTRY_LARGE_ATTACHMENT_SIZE) { return NULL; } size_t buf_len; @@ -741,6 +739,79 @@ sentry__envelope_add_from_path( return envelope_add_from_owned_buffer(envelope, buf, buf_len, type); } +bool +sentry__envelope_append_raw_attachment(sentry_envelope_t *envelope, + const sentry_path_t *path, const char *filename, + const char *attachment_type, const char *content_type) +{ + if (!envelope || !envelope->is_raw || !path) { + return false; + } + + size_t file_len; + char *file_buf = sentry__path_read_to_buffer(path, &file_len); + if (!file_buf) { + return false; + } + + sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); + if (!jw) { + sentry_free(file_buf); + return false; + } + sentry_value_t headers = sentry_value_new_object(); + sentry_value_set_by_key( + headers, "type", sentry_value_new_string("attachment")); + sentry_value_set_by_key( + headers, "length", sentry_value_new_int32((int32_t)file_len)); + if (filename) { + sentry_value_set_by_key( + headers, "filename", sentry_value_new_string(filename)); + } + if (attachment_type) { + sentry_value_set_by_key(headers, "attachment_type", + sentry_value_new_string(attachment_type)); + } + if (content_type) { + sentry_value_set_by_key( + headers, "content_type", sentry_value_new_string(content_type)); + } + sentry__jsonwriter_write_value(jw, headers); + sentry_value_decref(headers); + size_t header_len = 0; + char *header_buf = sentry__jsonwriter_into_string(jw, &header_len); + if (!header_buf) { + sentry_free(file_buf); + return false; + } + + size_t old_len = envelope->contents.raw.payload_len; + size_t new_len = old_len + 1 + header_len + 1 + file_len; + char *new_payload = sentry_malloc(new_len); + if (!new_payload) { + sentry_free(header_buf); + sentry_free(file_buf); + return false; + } + + char *p = new_payload; + memcpy(p, envelope->contents.raw.payload, old_len); + p += old_len; + *p++ = '\n'; + memcpy(p, header_buf, header_len); + p += header_len; + *p++ = '\n'; + memcpy(p, file_buf, file_len); + + sentry_free(envelope->contents.raw.payload); + envelope->contents.raw.payload = new_payload; + envelope->contents.raw.payload_len = new_len; + + sentry_free(header_buf); + sentry_free(file_buf); + return true; +} + static void sentry__envelope_serialize_headers_into_stringbuilder( const sentry_envelope_t *envelope, sentry_stringbuilder_t *sb) diff --git a/src/sentry_envelope.h b/src/sentry_envelope.h index 4a270ae29..202f4ff58 100644 --- a/src/sentry_envelope.h +++ b/src/sentry_envelope.h @@ -121,6 +121,14 @@ void sentry__envelope_add_attachments( sentry_envelope_item_t *sentry__envelope_add_from_path( sentry_envelope_t *envelope, const sentry_path_t *path, const char *type); +/** + * Append an attachment item to a raw envelope by reading `path` and + * building the item header/payload bytes directly into the raw buffer. + */ +bool sentry__envelope_append_raw_attachment(sentry_envelope_t *envelope, + const sentry_path_t *path, const char *filename, + const char *attachment_type, const char *content_type); + /** * This will add the given buffer as a new envelope item of type `type`. */ diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 918c03c19..9924f81f6 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -1,5 +1,6 @@ #include "sentry_http_transport.h" #include "sentry_alloc.h" +#include "sentry_attachment.h" #include "sentry_database.h" #include "sentry_envelope.h" #include "sentry_options.h" @@ -291,7 +292,7 @@ http_send_request( static bool tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, - sentry_value_t entry, sentry_envelope_t *send_envelope) + sentry_value_t entry, sentry_envelope_t *tus_envelope) { const char *filename = sentry_value_as_string(sentry_value_get_by_key(entry, "filename")); @@ -345,7 +346,7 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, sentry_value_t att_length = sentry_value_get_by_key(entry, "attachment_length"); sentry_value_incref(att_length); - sentry__envelope_add_attachment_ref(send_envelope, resp.location, filename, + sentry__envelope_add_attachment_ref(tus_envelope, resp.location, filename, *ref_ct ? ref_ct : NULL, *att_type ? att_type : NULL, att_length); sentry__path_remove(att_file); @@ -354,10 +355,56 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, return true; } +static bool +inline_cached_attachment(const sentry_path_t *att_dir, sentry_value_t entry, + sentry_envelope_t *envelope) +{ + const char *filename + = sentry_value_as_string(sentry_value_get_by_key(entry, "filename")); + if (!filename || *filename == '\0') { + return false; + } + + const char *att_type = sentry_value_as_string( + sentry_value_get_by_key(entry, "attachment_type")); + const char *ref_ct = sentry_value_as_string( + sentry_value_get_by_key(entry, "content_type")); + + sentry_path_t *att_file = sentry__path_join_str(att_dir, filename); + if (!att_file) { + return false; + } + + // Try structured envelope first, fall back to raw append + sentry_envelope_item_t *item + = sentry__envelope_add_from_path(envelope, att_file, "attachment"); + if (item) { + if (*att_type) { + sentry__envelope_item_set_header( + item, "attachment_type", sentry_value_new_string(att_type)); + } + if (*ref_ct) { + sentry__envelope_item_set_header( + item, "content_type", sentry_value_new_string(ref_ct)); + } + sentry__envelope_item_set_header( + item, "filename", sentry_value_new_string(filename)); + } else if (!sentry__envelope_append_raw_attachment(envelope, att_file, + filename, *att_type ? att_type : NULL, + *ref_ct ? ref_ct : NULL)) { + sentry__path_free(att_file); + return false; + } + + sentry__path_free(att_file); + return true; +} + static void -tus_resolve_and_send(http_transport_state_t *state, const char *uuid) +resolve_and_send_external_attachments(http_transport_state_t *state, + const char *uuid, sentry_envelope_t *envelope) { - if (!state->has_tus || !uuid || !state->run) { + if (!uuid || !state->run) { return; } @@ -397,46 +444,68 @@ tus_resolve_and_send(http_transport_state_t *state, const char *uuid) } sentry_uuid_t event_id = sentry_uuid_from_string(uuid); - sentry_envelope_t *send_envelope - = sentry__envelope_new_with_dsn(state->dsn); - if (!send_envelope) { - sentry_value_decref(refs); - sentry__path_free(refs_path); - sentry__path_free(att_dir); - return; - } - sentry__envelope_set_event_id(send_envelope, &event_id); + sentry_envelope_t *tus_envelope = NULL; - bool has_refs = false; size_t count = sentry_value_get_length(refs); for (size_t i = 0; i < count; i++) { sentry_value_t entry = sentry_value_get_by_index(refs, i); - if (tus_upload_ref(state, att_dir, entry, send_envelope)) { - has_refs = true; + + const char *filename = sentry_value_as_string( + sentry_value_get_by_key(entry, "filename")); + if (!filename || *filename == '\0') { + continue; } - if (!state->has_tus) { - break; + sentry_path_t *att_file = sentry__path_join_str(att_dir, filename); + size_t file_size = att_file ? sentry__path_get_size(att_file) : 0; + sentry__path_free(att_file); + + if (file_size >= SENTRY_LARGE_ATTACHMENT_SIZE && state->has_tus) { + if (!tus_envelope) { + tus_envelope = sentry__envelope_new_with_dsn(state->dsn); + if (tus_envelope) { + sentry__envelope_set_event_id(tus_envelope, &event_id); + } + } + if (tus_envelope) { + tus_upload_ref(state, att_dir, entry, tus_envelope); + } + if (!state->has_tus) { + break; + } + } else { + inline_cached_attachment(att_dir, entry, envelope); } } sentry_value_decref(refs); - if (has_refs) { + if (tus_envelope) { sentry_prepared_http_request_t *req = sentry__prepare_http_request( - send_envelope, state->dsn, state->ratelimiter, state->user_agent); + tus_envelope, state->dsn, state->ratelimiter, state->user_agent); if (req) { http_send_request(state, req); sentry__prepared_http_request_free(req); } + sentry_envelope_free(tus_envelope); } - sentry_envelope_free(send_envelope); - sentry__path_remove(refs_path); sentry__path_free(refs_path); - sentry__path_remove(att_dir); sentry__path_free(att_dir); } +static void +cleanup_external_attachments(const sentry_run_t *run, const char *uuid) +{ + if (!uuid || !run) { + return; + } + sentry_path_t *att_dir = sentry__path_join_str(run->cache_path, uuid); + if (att_dir) { + sentry__path_remove_all(att_dir); + sentry__path_free(att_dir); + } +} + static int http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope, const char *uuid) @@ -450,7 +519,7 @@ http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope, } } - tus_resolve_and_send(state, uuid); + resolve_and_send_external_attachments(state, uuid, envelope); sentry_prepared_http_request_t *req = sentry__prepare_http_request( envelope, state->dsn, state->ratelimiter, state->user_agent); @@ -459,6 +528,11 @@ http_send_envelope(http_transport_state_t *state, sentry_envelope_t *envelope, } int status_code = http_send_request(state, req); sentry__prepared_http_request_free(req); + + if (status_code >= 0) { + cleanup_external_attachments(state->run, uuid); + } + return status_code; } diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index ed48cfed5..f648dbe88 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -899,24 +899,23 @@ SENTRY_TEST(attachment_ref_copy) = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); const char *test_file_str - = SENTRY_TEST_PATH_PREFIX "sentry_test_large_attachment"; + = SENTRY_TEST_PATH_PREFIX "sentry_test_minidump.dmp"; sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); - size_t large_size = 100 * 1024 * 1024; FILE *f = fopen(test_file_str, "wb"); TEST_CHECK(!!f); - fseek(f, (long)(large_size - 1), SEEK_SET); - fputc(0, f); + fputs("minidump_data", f); fclose(f); sentry_attachment_t *attachment = sentry__attachment_from_path(sentry__path_clone(test_file_path)); + attachment->type = MINIDUMP; sentry_attachment_set_content_type(attachment, "application/x-dmp"); - // cache_large_attachments copies the file (not under run_path) + // cache_external_attachments copies the file (not under run_path) sentry_path_t *db_path = NULL; SENTRY_WITH_OPTIONS (opts) { db_path = sentry__path_clone(opts->database_path); - sentry__cache_large_attachments( + sentry__cache_external_attachments( opts->run->cache_path, &event_id, attachment, NULL); } @@ -929,7 +928,7 @@ SENTRY_TEST(attachment_ref_copy) cache_dir, "c993afb6-b4ac-48a6-b61b-2558e601d65d"); sentry__path_free(cache_dir); sentry_path_t *att_file - = sentry__path_join_str(event_dir, "sentry_test_large_attachment"); + = sentry__path_join_str(event_dir, "sentry_test_minidump.dmp"); TEST_CHECK(sentry__path_is_file(att_file)); // __sentry-attachments.json exists with correct metadata @@ -948,10 +947,13 @@ SENTRY_TEST(attachment_ref_copy) sentry_value_t ref_entry = sentry_value_get_by_index(refs, 0); TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( ref_entry, "filename")), - "sentry_test_large_attachment"); + "sentry_test_minidump.dmp"); TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( ref_entry, "content_type")), "application/x-dmp"); + TEST_CHECK_STRING_EQUAL(sentry_value_as_string(sentry_value_get_by_key( + ref_entry, "attachment_type")), + "event.minidump"); sentry_value_decref(refs); } @@ -974,7 +976,7 @@ SENTRY_TEST(attachment_ref_move) sentry_uuid_t event_id = sentry_uuid_from_string("c993afb6-b4ac-48a6-b61b-2558e601d65d"); - // create large file inside the run directory (SDK-owned) + // create minidump file inside the run directory (SDK-owned) sentry_path_t *run_path = NULL; sentry_path_t *db_path = NULL; SENTRY_WITH_OPTIONS (opts) { @@ -985,23 +987,22 @@ SENTRY_TEST(attachment_ref_move) sentry_path_t *src_path = sentry__path_join_str(run_path, "test_minidump.dmp"); - size_t large_size = 100 * 1024 * 1024; #ifdef SENTRY_PLATFORM_WINDOWS FILE *f = _wfopen(src_path->path_w, L"wb"); #else FILE *f = fopen(src_path->path, "wb"); #endif TEST_CHECK(!!f); - fseek(f, (long)(large_size - 1), SEEK_SET); - fputc(0, f); + fputs("minidump_data", f); fclose(f); sentry_attachment_t *attachment = sentry__attachment_from_path(sentry__path_clone(src_path)); + attachment->type = MINIDUMP; // cache with run_path → file is renamed (moved) SENTRY_WITH_OPTIONS (opts) { - sentry__cache_large_attachments( + sentry__cache_external_attachments( opts->run->cache_path, &event_id, attachment, run_path); } @@ -1064,11 +1065,11 @@ SENTRY_TEST(attachment_ref_restore) sentry_path_t *att_file = sentry__path_join_str(event_att_dir, "test_minidump.dmp"); - size_t large_size = 100 * 1024 * 1024; + const char *minidump_data = "minidump_data"; + size_t minidump_size = strlen(minidump_data); FILE *f = fopen(att_file->path, "wb"); TEST_ASSERT(!!f); - fseek(f, (long)(large_size - 1), SEEK_SET); - fputc(0, f); + fputs(minidump_data, f); fclose(f); // write __sentry-attachments.json @@ -1079,7 +1080,7 @@ SENTRY_TEST(attachment_ref_restore) sentry_value_set_by_key( ref_obj, "attachment_type", sentry_value_new_string("event.minidump")); sentry_value_set_by_key(ref_obj, "attachment_length", - sentry_value_new_uint64((uint64_t)large_size)); + sentry_value_new_uint64((uint64_t)minidump_size)); sentry_value_append(refs, ref_obj); sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); sentry__jsonwriter_write_value(jw, refs); From 0f27df0f95ba70ca4fa87d8a43a98d09a7bc4c81 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 19 Mar 2026 10:31:03 +0100 Subject: [PATCH 114/121] Reapply "feat(tus): split into separate creation and upload requests" This reverts commit 4f3fa6db9145d03041020c87413128952f4d6d28. --- src/transports/sentry_http_transport.c | 91 +++++++++++++++++++++----- src/transports/sentry_http_transport.h | 7 +- tests/test_integration_tus.py | 58 ++++++++++++---- tests/unit/test_envelopes.c | 51 ++++++++++++--- 4 files changed, 166 insertions(+), 41 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 9924f81f6..c1178273a 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -225,10 +225,6 @@ prepare_tus_request_common( h->key = "x-sentry-auth"; h->value = sentry__dsn_get_auth_header(dsn, user_agent); - h = &req->headers[req->headers_len++]; - h->key = "content-type"; - h->value = sentry__string_clone(TUS_MIME); - h = &req->headers[req->headers_len++]; h->key = "tus-resumable"; h->value = sentry__string_clone("1.0.0"); @@ -241,18 +237,46 @@ prepare_tus_request_common( } static sentry_prepared_http_request_t * -prepare_tus_request(const sentry_path_t *path, size_t file_size, - const sentry_dsn_t *dsn, const char *user_agent) +prepare_tus_upload_request( + const char *location, const sentry_path_t *path, size_t file_size) { - if (!path) { + if (!location || !path) { return NULL; } + sentry_prepared_http_request_t *req - = prepare_tus_request_common(file_size, dsn, user_agent); - if (req) { - req->body_path = sentry__path_clone(path); - req->body_len = file_size; + = SENTRY_MAKE(sentry_prepared_http_request_t); + if (!req) { + return NULL; + } + memset(req, 0, sizeof(*req)); + + req->headers = sentry_malloc( + sizeof(sentry_prepared_http_header_t) * TUS_MAX_HTTP_HEADERS); + if (!req->headers) { + sentry_free(req); + return NULL; } + req->headers_len = 0; + + req->method = "PATCH"; + req->url = sentry__string_clone(location); + req->body_path = sentry__path_clone(path); + req->body_len = file_size; + + sentry_prepared_http_header_t *h; + h = &req->headers[req->headers_len++]; + h->key = "tus-resumable"; + h->value = sentry__string_clone("1.0.0"); + + h = &req->headers[req->headers_len++]; + h->key = "content-type"; + h->value = sentry__string_clone(TUS_MIME); + + h = &req->headers[req->headers_len++]; + h->key = "upload-offset"; + h->value = sentry__string_clone("0"); + return req; } @@ -307,8 +331,9 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, return false; } - sentry_prepared_http_request_t *req = prepare_tus_request( - att_file, file_size, state->dsn, state->user_agent); + // Step 1: TUS creation request (POST, no body) + sentry_prepared_http_request_t *req + = prepare_tus_request_common(file_size, state->dsn, state->user_agent); if (!req) { sentry__path_free(att_file); return false; @@ -339,6 +364,29 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, return false; } + // Step 2: TUS upload request (PATCH with file body) + req = prepare_tus_upload_request(resp.location, att_file, file_size); + char *location = resp.location; + resp.location = NULL; + http_response_cleanup(&resp); + + if (!req) { + sentry__path_free(att_file); + sentry_free(location); + return false; + } + + memset(&resp, 0, sizeof(resp)); + ok = state->send_func(state->client, req, &resp); + sentry__prepared_http_request_free(req); + http_response_cleanup(&resp); + + if (!ok || resp.status_code != 204) { + sentry__path_free(att_file); + sentry_free(location); + return false; + } + const char *ref_ct = sentry_value_as_string( sentry_value_get_by_key(entry, "content_type")); const char *att_type = sentry_value_as_string( @@ -346,12 +394,12 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, sentry_value_t att_length = sentry_value_get_by_key(entry, "attachment_length"); sentry_value_incref(att_length); - sentry__envelope_add_attachment_ref(tus_envelope, resp.location, filename, + sentry__envelope_add_attachment_ref(tus_envelope, location, filename, *ref_ct ? ref_ct : NULL, *att_type ? att_type : NULL, att_length); sentry__path_remove(att_file); sentry__path_free(att_file); - http_response_cleanup(&resp); + sentry_free(location); return true; } @@ -772,9 +820,16 @@ sentry__http_transport_get_bgworker(sentry_transport_t *transport) } sentry_prepared_http_request_t * -sentry__prepare_tus_request(const sentry_path_t *path, size_t file_size, - const sentry_dsn_t *dsn, const char *user_agent) +sentry__prepare_tus_create_request( + size_t file_size, const sentry_dsn_t *dsn, const char *user_agent) +{ + return prepare_tus_request_common(file_size, dsn, user_agent); +} + +sentry_prepared_http_request_t * +sentry__prepare_tus_upload_request( + const char *location, const sentry_path_t *path, size_t file_size) { - return prepare_tus_request(path, file_size, dsn, user_agent); + return prepare_tus_upload_request(location, path, file_size); } #endif diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index c89204800..b8d1f79ae 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -55,9 +55,10 @@ void sentry__http_transport_set_shutdown_client( #ifdef SENTRY_UNITTEST void *sentry__http_transport_get_bgworker(sentry_transport_t *transport); -sentry_prepared_http_request_t *sentry__prepare_tus_request( - const sentry_path_t *path, size_t file_size, const sentry_dsn_t *dsn, - const char *user_agent); +sentry_prepared_http_request_t *sentry__prepare_tus_create_request( + size_t file_size, const sentry_dsn_t *dsn, const char *user_agent); +sentry_prepared_http_request_t *sentry__prepare_tus_upload_request( + const char *location, const sentry_path_t *path, size_t file_size); #endif #endif diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index e1b861870..fa75af32e 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -26,8 +26,11 @@ def test_tus_upload_large_attachment(cmake, httpserver): {"SENTRY_BACKEND": "none", "SENTRY_TRANSPORT_COMPRESSION": "Off"}, ) - location = "/api/123456/upload/abc123def456789/?length=104857600&signature=xyz" + upload_uri = "/api/123456/upload/abc123def456789/" + upload_qs = "length=104857600&signature=xyz" + location = httpserver.url_for(upload_uri) + "?" + upload_qs + # TUS creation request (POST, no body) -> 201 + Location httpserver.expect_oneshot_request( "/api/123456/upload/", headers={"tus-resumable": "1.0.0"}, @@ -37,6 +40,14 @@ def test_tus_upload_large_attachment(cmake, httpserver): headers={"Location": location}, ) + # TUS upload request (PATCH with body) -> 204 + httpserver.expect_oneshot_request( + upload_uri, + method="PATCH", + headers={"tus-resumable": "1.0.0"}, + query_string=upload_qs, + ).respond_with_data("", status=204) + httpserver.expect_request( "/api/123456/envelope/", headers={"x-sentry-auth": auth_header}, @@ -51,27 +62,35 @@ def test_tus_upload_large_attachment(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 3 + assert len(httpserver.log) == 4 - # Find the upload request and envelope requests + # Find the create, upload, and envelope requests + create_req = None upload_req = None envelope_reqs = [] for entry in httpserver.log: req = entry[0] - if "/upload/" in req.path: + if req.path == "/api/123456/upload/" and req.method == "POST": + create_req = req + elif upload_uri in req.path and req.method == "PATCH": upload_req = req elif "/envelope/" in req.path: envelope_reqs.append(req) + assert create_req is not None assert upload_req is not None assert len(envelope_reqs) == 2 + # Verify TUS creation request headers + assert create_req.headers.get("tus-resumable") == "1.0.0" + upload_length = create_req.headers.get("upload-length") + assert upload_length is not None + assert int(upload_length) == 100 * 1024 * 1024 + # Verify TUS upload request headers assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" - upload_length = upload_req.headers.get("upload-length") - assert upload_length is not None - assert int(upload_length) == 100 * 1024 * 1024 + assert upload_req.headers.get("upload-offset") == "0" # One envelope has the resolved attachment-refs, the other is the original attachment_ref = None @@ -183,7 +202,9 @@ def test_tus_crash_restart(cmake, httpserver): att_size = os.path.getsize(os.path.join(att_dir, att_files[0])) assert att_size >= 100 * 1024 * 1024 - location = "/api/123456/upload/abc123def456789/?length=104857600&signature=xyz" + upload_uri = "/api/123456/upload/abc123def456789/" + upload_qs = "length=104857600&signature=xyz" + location = httpserver.url_for(upload_uri) + "?" + upload_qs # Second run: restart picks up crash and uploads via TUS httpserver.expect_oneshot_request( @@ -195,6 +216,13 @@ def test_tus_crash_restart(cmake, httpserver): headers={"Location": location}, ) + httpserver.expect_oneshot_request( + upload_uri, + method="PATCH", + headers={"tus-resumable": "1.0.0"}, + query_string=upload_qs, + ).respond_with_data("", status=204) + httpserver.expect_request( "/api/123456/envelope/", headers={"x-sentry-auth": auth_header}, @@ -207,24 +235,32 @@ def test_tus_crash_restart(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 3 + assert len(httpserver.log) == 4 + create_req = None upload_req = None envelope_reqs = [] for entry in httpserver.log: req = entry[0] - if "/upload/" in req.path: + if req.path == "/api/123456/upload/" and req.method == "POST": + create_req = req + elif upload_uri in req.path and req.method == "PATCH": upload_req = req elif "/envelope/" in req.path: envelope_reqs.append(req) + assert create_req is not None assert upload_req is not None assert len(envelope_reqs) == 2 + # Verify TUS creation request headers + assert create_req.headers.get("tus-resumable") == "1.0.0" + assert int(create_req.headers.get("upload-length")) == 100 * 1024 * 1024 + # Verify TUS upload request headers assert upload_req.headers.get("tus-resumable") == "1.0.0" assert upload_req.headers.get("content-type") == "application/offset+octet-stream" - assert int(upload_req.headers.get("upload-length")) == 100 * 1024 * 1024 + assert upload_req.headers.get("upload-offset") == "0" # One envelope has the resolved attachment-refs, the other is the original attachment_ref = None diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index f648dbe88..e7382d9e2 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -742,19 +742,15 @@ SENTRY_TEST(tus_request_preparation) { SENTRY_TEST_DSN_NEW_DEFAULT(dsn); - const char *test_file_str = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; - sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); - TEST_CHECK_INT_EQUAL( - sentry__path_write_buffer(test_file_path, "test-data", 9), 0); - + // Test creation request (POST, no body) sentry_prepared_http_request_t *req - = sentry__prepare_tus_request(test_file_path, 9, dsn, NULL); + = sentry__prepare_tus_create_request(9, dsn, NULL); TEST_CHECK(!!req); TEST_CHECK_STRING_EQUAL(req->method, "POST"); TEST_CHECK_STRING_EQUAL( req->url, "https://sentry.invalid:443/api/42/upload/"); - TEST_CHECK(!!req->body_path); - TEST_CHECK_INT_EQUAL(req->body_len, 9); + TEST_CHECK(!req->body_path); + TEST_CHECK_INT_EQUAL(req->body_len, 0); TEST_CHECK(!req->body); bool has_tus_resumable = false; @@ -769,15 +765,52 @@ SENTRY_TEST(tus_request_preparation) TEST_CHECK_STRING_EQUAL(req->headers[i].value, "9"); has_upload_length = true; } + if (strcmp(req->headers[i].key, "content-type") == 0) { + has_content_type = true; + } + } + TEST_CHECK(has_tus_resumable); + TEST_CHECK(has_upload_length); + TEST_CHECK(!has_content_type); + + sentry__prepared_http_request_free(req); + + // Test upload request (PATCH with body) + const char *test_file_str = SENTRY_TEST_PATH_PREFIX "sentry_test_tus_file"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + TEST_CHECK_INT_EQUAL( + sentry__path_write_buffer(test_file_path, "test-data", 9), 0); + + const char *location = "https://sentry.invalid/api/42/upload/abc123/"; + req = sentry__prepare_tus_upload_request(location, test_file_path, 9); + TEST_CHECK(!!req); + TEST_CHECK_STRING_EQUAL(req->method, "PATCH"); + TEST_CHECK_STRING_EQUAL(req->url, location); + TEST_CHECK(!!req->body_path); + TEST_CHECK_INT_EQUAL(req->body_len, 9); + TEST_CHECK(!req->body); + + has_tus_resumable = false; + has_content_type = false; + bool has_upload_offset = false; + for (size_t i = 0; i < req->headers_len; i++) { + if (strcmp(req->headers[i].key, "tus-resumable") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "1.0.0"); + has_tus_resumable = true; + } if (strcmp(req->headers[i].key, "content-type") == 0) { TEST_CHECK_STRING_EQUAL( req->headers[i].value, "application/offset+octet-stream"); has_content_type = true; } + if (strcmp(req->headers[i].key, "upload-offset") == 0) { + TEST_CHECK_STRING_EQUAL(req->headers[i].value, "0"); + has_upload_offset = true; + } } TEST_CHECK(has_tus_resumable); - TEST_CHECK(has_upload_length); TEST_CHECK(has_content_type); + TEST_CHECK(has_upload_offset); sentry__prepared_http_request_free(req); sentry__path_remove(test_file_path); From 1a676d861dfc0a2c5a675967cf2bebb38cf52bdc Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 19 Mar 2026 11:26:32 +0100 Subject: [PATCH 115/121] fix(tus): resolve relative Location URL, add auth to PATCH, handle empty POST - Resolve relative Location header against DSN origin (sentry__dsn_resolve_url) - Add x-sentry-auth header to TUS upload PATCH request - Use CURLOPT_CUSTOMREQUEST for bodyless POST to avoid curl's default Content-Type: application/x-www-form-urlencoded Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_utils.c | 19 ++++++++++++++++++ src/sentry_utils.h | 7 +++++++ src/transports/sentry_http_transport.c | 22 +++++++++++++-------- src/transports/sentry_http_transport.h | 3 ++- src/transports/sentry_http_transport_curl.c | 4 +++- tests/unit/test_envelopes.c | 3 ++- tests/unit/test_utils.c | 22 +++++++++++++++++++++ tests/unit/tests.inc | 1 + 8 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/sentry_utils.c b/src/sentry_utils.c index 40786242c..da678d3d4 100644 --- a/src/sentry_utils.c +++ b/src/sentry_utils.c @@ -401,6 +401,25 @@ sentry__dsn_get_upload_url(const sentry_dsn_t *dsn) return sentry__stringbuilder_into_string(&sb); } +char * +sentry__dsn_resolve_url(const sentry_dsn_t *dsn, const char *path) +{ + if (!dsn || !dsn->is_valid || !path) { + return NULL; + } + sentry_stringbuilder_t sb; + sentry__stringbuilder_init(&sb); + if (path[0] == '/') { + sentry__stringbuilder_append(&sb, dsn->is_secure ? "https" : "http"); + sentry__stringbuilder_append(&sb, "://"); + sentry__stringbuilder_append(&sb, dsn->host); + sentry__stringbuilder_append_char(&sb, ':'); + sentry__stringbuilder_append_int64(&sb, (int64_t)dsn->port); + } + sentry__stringbuilder_append(&sb, path); + return sentry__stringbuilder_into_string(&sb); +} + char * sentry__dsn_get_minidump_url(const sentry_dsn_t *dsn, const char *user_agent) { diff --git a/src/sentry_utils.h b/src/sentry_utils.h index 6777ccd4b..95b3ac2f1 100644 --- a/src/sentry_utils.h +++ b/src/sentry_utils.h @@ -115,6 +115,13 @@ char *sentry__dsn_get_envelope_url(const sentry_dsn_t *dsn); */ char *sentry__dsn_get_upload_url(const sentry_dsn_t *dsn); +/** + * Resolves a possibly relative URL against the DSN's origin. + * If the path starts with '/', the DSN's scheme, host, and port are prepended. + * Returns a newly allocated string. + */ +char *sentry__dsn_resolve_url(const sentry_dsn_t *dsn, const char *path); + /** * Returns the minidump endpoint url used for uploads done by the out-of-process * crashpad backend as a newly allocated string. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index c1178273a..18262539d 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -237,8 +237,8 @@ prepare_tus_request_common( } static sentry_prepared_http_request_t * -prepare_tus_upload_request( - const char *location, const sentry_path_t *path, size_t file_size) +prepare_tus_upload_request(const char *location, const sentry_path_t *path, + size_t file_size, const sentry_dsn_t *dsn, const char *user_agent) { if (!location || !path) { return NULL; @@ -265,6 +265,10 @@ prepare_tus_upload_request( req->body_len = file_size; sentry_prepared_http_header_t *h; + h = &req->headers[req->headers_len++]; + h->key = "x-sentry-auth"; + h->value = sentry__dsn_get_auth_header(dsn, user_agent); + h = &req->headers[req->headers_len++]; h->key = "tus-resumable"; h->value = sentry__string_clone("1.0.0"); @@ -365,10 +369,10 @@ tus_upload_ref(http_transport_state_t *state, const sentry_path_t *att_dir, } // Step 2: TUS upload request (PATCH with file body) - req = prepare_tus_upload_request(resp.location, att_file, file_size); - char *location = resp.location; - resp.location = NULL; + char *location = sentry__dsn_resolve_url(state->dsn, resp.location); http_response_cleanup(&resp); + req = prepare_tus_upload_request( + location, att_file, file_size, state->dsn, state->user_agent); if (!req) { sentry__path_free(att_file); @@ -827,9 +831,11 @@ sentry__prepare_tus_create_request( } sentry_prepared_http_request_t * -sentry__prepare_tus_upload_request( - const char *location, const sentry_path_t *path, size_t file_size) +sentry__prepare_tus_upload_request(const char *location, + const sentry_path_t *path, size_t file_size, const sentry_dsn_t *dsn, + const char *user_agent) { - return prepare_tus_upload_request(location, path, file_size); + return prepare_tus_upload_request( + location, path, file_size, dsn, user_agent); } #endif diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index b8d1f79ae..3f81d7dd3 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -58,7 +58,8 @@ void *sentry__http_transport_get_bgworker(sentry_transport_t *transport); sentry_prepared_http_request_t *sentry__prepare_tus_create_request( size_t file_size, const sentry_dsn_t *dsn, const char *user_agent); sentry_prepared_http_request_t *sentry__prepare_tus_upload_request( - const char *location, const sentry_path_t *path, size_t file_size); + const char *location, const sentry_path_t *path, size_t file_size, + const sentry_dsn_t *dsn, const char *user_agent); #endif #endif diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index b9b414f65..06c6bed63 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -287,10 +287,12 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, curl_easy_setopt(curl, CURLOPT_READDATA, body_file); curl_easy_setopt( curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)req->body_len); - } else { + } else if (req->body) { curl_easy_setopt(curl, CURLOPT_POST, (long)1); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req->body); curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, (long)req->body_len); + } else { + curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, req->method); } char error_buf[CURL_ERROR_SIZE]; diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index e7382d9e2..f0afaa35a 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -782,7 +782,8 @@ SENTRY_TEST(tus_request_preparation) sentry__path_write_buffer(test_file_path, "test-data", 9), 0); const char *location = "https://sentry.invalid/api/42/upload/abc123/"; - req = sentry__prepare_tus_upload_request(location, test_file_path, 9); + req = sentry__prepare_tus_upload_request( + location, test_file_path, 9, dsn, NULL); TEST_CHECK(!!req); TEST_CHECK_STRING_EQUAL(req->method, "PATCH"); TEST_CHECK_STRING_EQUAL(req->url, location); diff --git a/tests/unit/test_utils.c b/tests/unit/test_utils.c index 4a95cef4b..406844cde 100644 --- a/tests/unit/test_utils.c +++ b/tests/unit/test_utils.c @@ -275,6 +275,28 @@ SENTRY_TEST(dsn_store_url_custom_agent) sentry__dsn_decref(dsn); } +SENTRY_TEST(dsn_resolve_url) +{ + SENTRY_TEST_DSN_NEW(dsn, "https://key@sentry.io/42"); + char *url; + + // relative path gets origin prepended + url = sentry__dsn_resolve_url(dsn, "/api/42/upload/abc123/"); + TEST_CHECK_STRING_EQUAL(url, "https://sentry.io:443/api/42/upload/abc123/"); + sentry_free(url); + + // absolute URL passes through + url = sentry__dsn_resolve_url(dsn, "https://other.host/path"); + TEST_CHECK_STRING_EQUAL(url, "https://other.host/path"); + sentry_free(url); + + // NULL inputs + TEST_CHECK(!sentry__dsn_resolve_url(NULL, "/path")); + TEST_CHECK(!sentry__dsn_resolve_url(dsn, NULL)); + + sentry__dsn_decref(dsn); +} + SENTRY_TEST(page_allocator) { #if !defined(SENTRY_PLATFORM_UNIX) || defined(SENTRY_PLATFORM_PS) diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index e13634f25..9a688e7d3 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -91,6 +91,7 @@ XX(dsn_parsing_project_id_with_path_prefix) XX(dsn_parsing_project_id_without_path) XX(dsn_parsing_too_long_org_length) XX(dsn_parsing_zero_id_org) +XX(dsn_resolve_url) XX(dsn_store_url_custom_agent) XX(dsn_store_url_with_path) XX(dsn_store_url_without_path) From 14b3248651276a4c80cdf7c3335dd9713b575144 Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 19 Mar 2026 17:28:53 +0100 Subject: [PATCH 116/121] feat(envelope): support attachment-refs in raw envelopes Add append_raw_attachment_ref() to append attachment-ref items to raw (serialized) envelopes by directly manipulating the payload buffer. sentry__envelope_add_attachment_ref() dispatches to this for raw envelopes, so callers don't need to know about the envelope format. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_envelope.c | 89 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index ef4a1004e..f123fb887 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -606,11 +606,100 @@ sentry__envelope_add_session( envelope, payload, payload_len, "session"); } +static bool +append_raw_attachment_ref(sentry_envelope_t *envelope, const char *location, + const char *filename, const char *content_type, const char *attachment_type, + sentry_value_t attachment_length) +{ + if (!envelope || !envelope->is_raw || !location) { + return false; + } + + // Build payload: {"location":"..."} + sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL); + if (!jw) { + return false; + } + sentry_value_t payload_obj = sentry_value_new_object(); + sentry_value_set_by_key( + payload_obj, "location", sentry_value_new_string(location)); + sentry__jsonwriter_write_value(jw, payload_obj); + sentry_value_decref(payload_obj); + size_t payload_len = 0; + char *payload_buf = sentry__jsonwriter_into_string(jw, &payload_len); + if (!payload_buf) { + return false; + } + + // Build item header + jw = sentry__jsonwriter_new_sb(NULL); + if (!jw) { + sentry_free(payload_buf); + return false; + } + sentry_value_t headers = sentry_value_new_object(); + sentry_value_set_by_key( + headers, "type", sentry_value_new_string("attachment")); + sentry_value_set_by_key(headers, "content_type", + sentry_value_new_string("application/vnd.sentry.attachment-ref")); + sentry_value_set_by_key( + headers, "length", sentry_value_new_int32((int32_t)payload_len)); + if (filename) { + sentry_value_set_by_key( + headers, "filename", sentry_value_new_string(filename)); + } + if (attachment_type) { + sentry_value_set_by_key(headers, "attachment_type", + sentry_value_new_string(attachment_type)); + } + sentry_value_set_by_key(headers, "attachment_length", attachment_length); + sentry__jsonwriter_write_value(jw, headers); + sentry_value_decref(headers); + size_t header_len = 0; + char *header_buf = sentry__jsonwriter_into_string(jw, &header_len); + if (!header_buf) { + sentry_free(payload_buf); + return false; + } + + // Append: \n
\n + size_t old_len = envelope->contents.raw.payload_len; + size_t new_len = old_len + 1 + header_len + 1 + payload_len; + char *new_payload = sentry_malloc(new_len); + if (!new_payload) { + sentry_free(header_buf); + sentry_free(payload_buf); + return false; + } + + char *p = new_payload; + memcpy(p, envelope->contents.raw.payload, old_len); + p += old_len; + *p++ = '\n'; + memcpy(p, header_buf, header_len); + p += header_len; + *p++ = '\n'; + memcpy(p, payload_buf, payload_len); + + sentry_free(envelope->contents.raw.payload); + envelope->contents.raw.payload = new_payload; + envelope->contents.raw.payload_len = new_len; + + sentry_free(header_buf); + sentry_free(payload_buf); + return true; +} + sentry_envelope_item_t * sentry__envelope_add_attachment_ref(sentry_envelope_t *envelope, const char *location, const char *filename, const char *content_type, const char *attachment_type, sentry_value_t attachment_length) { + if (envelope && envelope->is_raw) { + append_raw_attachment_ref(envelope, location, filename, content_type, + attachment_type, attachment_length); + return NULL; + } sentry_envelope_item_t *item = envelope_add_item(envelope); if (!item) { return NULL; From 8e3b656156e3fbd38bbd80a14dd28f23edcd2d5d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 19 Mar 2026 17:32:45 +0100 Subject: [PATCH 117/121] refactor(tus): append attachment-refs to event envelope Instead of sending attachment-refs in a separate envelope, append them to the event envelope so the server can associate them in one request. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/transports/sentry_http_transport.c | 23 +---------- tests/test_integration_tus.py | 56 +++++++++++--------------- 2 files changed, 25 insertions(+), 54 deletions(-) diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 18262539d..64407df97 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -495,9 +495,6 @@ resolve_and_send_external_attachments(http_transport_state_t *state, return; } - sentry_uuid_t event_id = sentry_uuid_from_string(uuid); - sentry_envelope_t *tus_envelope = NULL; - size_t count = sentry_value_get_length(refs); for (size_t i = 0; i < count; i++) { sentry_value_t entry = sentry_value_get_by_index(refs, i); @@ -512,15 +509,7 @@ resolve_and_send_external_attachments(http_transport_state_t *state, sentry__path_free(att_file); if (file_size >= SENTRY_LARGE_ATTACHMENT_SIZE && state->has_tus) { - if (!tus_envelope) { - tus_envelope = sentry__envelope_new_with_dsn(state->dsn); - if (tus_envelope) { - sentry__envelope_set_event_id(tus_envelope, &event_id); - } - } - if (tus_envelope) { - tus_upload_ref(state, att_dir, entry, tus_envelope); - } + tus_upload_ref(state, att_dir, entry, envelope); if (!state->has_tus) { break; } @@ -531,16 +520,6 @@ resolve_and_send_external_attachments(http_transport_state_t *state, sentry_value_decref(refs); - if (tus_envelope) { - sentry_prepared_http_request_t *req = sentry__prepare_http_request( - tus_envelope, state->dsn, state->ratelimiter, state->user_agent); - if (req) { - http_send_request(state, req); - sentry__prepared_http_request_free(req); - } - sentry_envelope_free(tus_envelope); - } - sentry__path_free(refs_path); sentry__path_free(att_dir); } diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index fa75af32e..85eab7cfb 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -62,12 +62,12 @@ def test_tus_upload_large_attachment(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 4 + assert len(httpserver.log) == 3 # Find the create, upload, and envelope requests create_req = None upload_req = None - envelope_reqs = [] + envelope_req = None for entry in httpserver.log: req = entry[0] if req.path == "/api/123456/upload/" and req.method == "POST": @@ -75,11 +75,11 @@ def test_tus_upload_large_attachment(cmake, httpserver): elif upload_uri in req.path and req.method == "PATCH": upload_req = req elif "/envelope/" in req.path: - envelope_reqs.append(req) + envelope_req = req assert create_req is not None assert upload_req is not None - assert len(envelope_reqs) == 2 + assert envelope_req is not None # Verify TUS creation request headers assert create_req.headers.get("tus-resumable") == "1.0.0" @@ -92,19 +92,15 @@ def test_tus_upload_large_attachment(cmake, httpserver): assert upload_req.headers.get("content-type") == "application/offset+octet-stream" assert upload_req.headers.get("upload-offset") == "0" - # One envelope has the resolved attachment-refs, the other is the original + # The envelope contains the event and the attachment-ref + body = envelope_req.get_data() + envelope = Envelope.deserialize(body) attachment_ref = None - for envelope_req in envelope_reqs: - body = envelope_req.get_data() - envelope = Envelope.deserialize(body) - for item in envelope: - if ( - item.headers.get("content_type") - == "application/vnd.sentry.attachment-ref" - ): - if hasattr(item.payload, "json") and "location" in item.payload.json: - attachment_ref = item - break + for item in envelope: + if item.headers.get("content_type") == "application/vnd.sentry.attachment-ref": + if hasattr(item.payload, "json") and "location" in item.payload.json: + attachment_ref = item + break assert attachment_ref is not None assert attachment_ref.payload.json["location"] == location @@ -235,11 +231,11 @@ def test_tus_crash_restart(cmake, httpserver): env=env, ) - assert len(httpserver.log) == 4 + assert len(httpserver.log) == 3 create_req = None upload_req = None - envelope_reqs = [] + envelope_req = None for entry in httpserver.log: req = entry[0] if req.path == "/api/123456/upload/" and req.method == "POST": @@ -247,11 +243,11 @@ def test_tus_crash_restart(cmake, httpserver): elif upload_uri in req.path and req.method == "PATCH": upload_req = req elif "/envelope/" in req.path: - envelope_reqs.append(req) + envelope_req = req assert create_req is not None assert upload_req is not None - assert len(envelope_reqs) == 2 + assert envelope_req is not None # Verify TUS creation request headers assert create_req.headers.get("tus-resumable") == "1.0.0" @@ -262,19 +258,15 @@ def test_tus_crash_restart(cmake, httpserver): assert upload_req.headers.get("content-type") == "application/offset+octet-stream" assert upload_req.headers.get("upload-offset") == "0" - # One envelope has the resolved attachment-refs, the other is the original + # The envelope contains the event and the attachment-ref + body = envelope_req.get_data() + envelope = Envelope.deserialize(body) attachment_ref = None - for envelope_req in envelope_reqs: - body = envelope_req.get_data() - envelope = Envelope.deserialize(body) - for item in envelope: - if ( - item.headers.get("content_type") - == "application/vnd.sentry.attachment-ref" - ): - if hasattr(item.payload, "json") and "location" in item.payload.json: - attachment_ref = item - break + for item in envelope: + if item.headers.get("content_type") == "application/vnd.sentry.attachment-ref": + if hasattr(item.payload, "json") and "location" in item.payload.json: + attachment_ref = item + break assert attachment_ref is not None assert attachment_ref.payload.json["location"] == location From a07948dc8dffa40b03029917fdaa3d973ca65e0f Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Thu, 19 Mar 2026 17:33:21 +0100 Subject: [PATCH 118/121] feat(curl): log upload progress and reduce debug noise Show upload progress (MB uploaded, percentage, speed) via the progress callback. Suppress per-chunk "=> Send" noise for non-JSON data since the progress callback covers that. Keep recv logging for all responses. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/transports/sentry_http_transport_curl.c | 37 +++++++++++++++------ 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 06c6bed63..499ae2877 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -130,11 +130,21 @@ curl_client_shutdown(void *_client) static int progress_callback(void *clientp, curl_off_t UNUSED(dltotal), - curl_off_t UNUSED(dlnow), curl_off_t UNUSED(ultotal), - curl_off_t UNUSED(ulnow)) + curl_off_t UNUSED(dlnow), curl_off_t ultotal, curl_off_t ulnow) { curl_client_t *client = clientp; - return sentry__atomic_fetch(&client->shutdown) ? 1 : 0; + if (sentry__atomic_fetch(&client->shutdown)) { + return 1; + } + if (ultotal > 0) { + double speed = 0; + curl_easy_getinfo(client->curl_handle, CURLINFO_SPEED_UPLOAD, &speed); + SENTRY_DEBUGF("upload progress: %.1f / %.1f MB (%.0f%%, %.1f MB/s)", + (double)ulnow / (1024.0 * 1024.0), + (double)ultotal / (1024.0 * 1024.0), + (double)ulnow / (double)ultotal * 100.0, speed / (1024.0 * 1024.0)); + } + return 0; } static size_t @@ -159,20 +169,27 @@ debug_function(CURL *UNUSED(handle), curl_infotype type, char *data, case CURLINFO_HEADER_IN: prefix = "< "; break; - case CURLINFO_DATA_OUT: + case CURLINFO_DATA_OUT: { + size_t len = size; + while (len > 0 && (data[len - 1] == '\n' || data[len - 1] == '\r')) { + len--; + } + if (len >= 2 && data[0] == '{' && data[len - 1] == '}') { + fprintf( + stderr, "=> Send (%zu bytes): %.*s\n", size, (int)len, data); + } + return 0; + } case CURLINFO_DATA_IN: { - const char *dir = type == CURLINFO_DATA_OUT ? "Send" : "Recv"; size_t len = size; while (len > 0 && (data[len - 1] == '\n' || data[len - 1] == '\r')) { len--; } if (len >= 2 && data[0] == '{' && data[len - 1] == '}') { - fprintf(stderr, "%s %s (%zu bytes): %.*s\n", - type == CURLINFO_DATA_OUT ? "=>" : "<=", dir, size, (int)len, - data); + fprintf( + stderr, "<= Recv (%zu bytes): %.*s\n", size, (int)len, data); } else { - fprintf(stderr, "%s %s (%zu bytes)\n", - type == CURLINFO_DATA_OUT ? "=>" : "<=", dir, size); + fprintf(stderr, "<= Recv (%zu bytes)\n", size); } return 0; } From 7560af2ce2bed50684c7da89f2eac04581d3d8ab Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 20 Mar 2026 13:24:38 +0100 Subject: [PATCH 119/121] fix(tus): add +json suffix to attachment-ref content type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per RFC 6838 §4.2.8, use application/vnd.sentry.attachment-ref+json to signal that the payload is syntactically JSON. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_envelope.c | 4 ++-- tests/__init__.py | 27 ++++++++++++++------------- tests/test_integration_tus.py | 13 ++++++++++--- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index f123fb887..2957a52ad 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -641,7 +641,7 @@ append_raw_attachment_ref(sentry_envelope_t *envelope, const char *location, sentry_value_set_by_key( headers, "type", sentry_value_new_string("attachment")); sentry_value_set_by_key(headers, "content_type", - sentry_value_new_string("application/vnd.sentry.attachment-ref")); + sentry_value_new_string("application/vnd.sentry.attachment-ref+json")); sentry_value_set_by_key( headers, "length", sentry_value_new_int32((int32_t)payload_len)); if (filename) { @@ -707,7 +707,7 @@ sentry__envelope_add_attachment_ref(sentry_envelope_t *envelope, sentry__envelope_item_set_header( item, "type", sentry_value_new_string("attachment")); sentry__envelope_item_set_header(item, "content_type", - sentry_value_new_string("application/vnd.sentry.attachment-ref")); + sentry_value_new_string("application/vnd.sentry.attachment-ref+json")); if (filename) { sentry__envelope_item_set_header( item, "filename", sentry_value_new_string(filename)); diff --git a/tests/__init__.py b/tests/__init__.py index 00ae16097..362a30394 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -127,13 +127,13 @@ def run(cwd, exe, args, expect_failure=False, env=None, **kwargs): if not is_pipe: sys.stdout.buffer.write(child.stdout) if expect_failure: - assert ( - child.returncode != 0 - ), f"command unexpectedly successful: {exe} {" ".join(args)}" + assert child.returncode != 0, ( + f"command unexpectedly successful: {exe} {" ".join(args)}" + ) else: - assert ( - child.returncode == 0 - ), f"command failed unexpectedly: {exe} {" ".join(args)}" + assert child.returncode == 0, ( + f"command failed unexpectedly: {exe} {" ".join(args)}" + ) if check and child.returncode: raise subprocess.CalledProcessError( child.returncode, child.args, output=child.stdout, stderr=child.stderr @@ -173,13 +173,13 @@ def run(cwd, exe, args, expect_failure=False, env=None, **kwargs): try: result = subprocess.run([*cmd, *args], cwd=cwd, env=env, check=check, **kwargs) if expect_failure: - assert ( - result.returncode != 0 - ), f"command unexpectedly successful: {cmd} {" ".join(args)}" + assert result.returncode != 0, ( + f"command unexpectedly successful: {cmd} {" ".join(args)}" + ) else: - assert ( - result.returncode == 0 - ), f"command failed unexpectedly: {cmd} {" ".join(args)}" + assert result.returncode == 0, ( + f"command failed unexpectedly: {cmd} {" ".join(args)}" + ) return result except subprocess.CalledProcessError: raise pytest.fail.Exception( @@ -354,7 +354,8 @@ def deserialize_from( "log", "trace_metric", ] - or headers.get("content_type") == "application/vnd.sentry.attachment-ref" + or headers.get("content_type") + == "application/vnd.sentry.attachment-ref+json" ): rv = cls(headers=headers, payload=PayloadRef(json=json.loads(payload))) else: diff --git a/tests/test_integration_tus.py b/tests/test_integration_tus.py index 85eab7cfb..0707aea8b 100644 --- a/tests/test_integration_tus.py +++ b/tests/test_integration_tus.py @@ -97,7 +97,10 @@ def test_tus_upload_large_attachment(cmake, httpserver): envelope = Envelope.deserialize(body) attachment_ref = None for item in envelope: - if item.headers.get("content_type") == "application/vnd.sentry.attachment-ref": + if ( + item.headers.get("content_type") + == "application/vnd.sentry.attachment-ref+json" + ): if hasattr(item.payload, "json") and "location" in item.payload.json: attachment_ref = item break @@ -158,7 +161,8 @@ def test_tus_upload_404_disables(cmake, httpserver): envelope = Envelope.deserialize(body) for item in envelope: assert ( - item.headers.get("content_type") != "application/vnd.sentry.attachment-ref" + item.headers.get("content_type") + != "application/vnd.sentry.attachment-ref+json" ) @@ -263,7 +267,10 @@ def test_tus_crash_restart(cmake, httpserver): envelope = Envelope.deserialize(body) attachment_ref = None for item in envelope: - if item.headers.get("content_type") == "application/vnd.sentry.attachment-ref": + if ( + item.headers.get("content_type") + == "application/vnd.sentry.attachment-ref+json" + ): if hasattr(item.payload, "json") and "location" in item.payload.json: attachment_ref = item break From 312493e33d4382de4d70730f6cf4e06518219d0d Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 20 Mar 2026 13:42:48 +0100 Subject: [PATCH 120/121] fix(tests): adapt crashpad/breakpad tests for external minidump caching Minidumps are now cached as external files instead of being inlined in envelopes. Update test_crashpad_cache_keep to check for the external attachment dir, and add dedicated tests for the external caching flow. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_integration_crashpad.py | 52 +++++++++++++++++++++- tests/test_integration_http.py | 70 ++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 1 deletion(-) diff --git a/tests/test_integration_crashpad.py b/tests/test_integration_crashpad.py index 3db4b4931..da9639790 100644 --- a/tests/test_integration_crashpad.py +++ b/tests/test_integration_crashpad.py @@ -826,7 +826,10 @@ def test_crashpad_cache_keep(cmake, httpserver, cache_keep): envelope = Envelope.deserialize_from(f) assert "dsn" in envelope.headers assert_meta(envelope, integration="crashpad") - assert_minidump(envelope) + # minidump is cached as an external file, not inline in the envelope + att_dirs = [d for d in cache_dir.iterdir() if d.is_dir()] + assert len(att_dirs) > 0, "expected external minidump dir" + assert list(att_dirs[0].glob("__sentry-attachments.json")) def test_crashpad_cache_max_size(cmake, httpserver): @@ -964,3 +967,50 @@ def test_crashpad_cache_max_age(cmake, httpserver): assert len(cache_files) == 3 for f in cache_files: assert time.time() - f.stat().st_mtime <= 5 * 24 * 60 * 60 + + +def test_crashpad_cache_external_minidump(cmake, httpserver): + """ + Crashpad crash report is converted to a cached envelope at startup. + The minidump is cached as an external file in cache// and + resolved into the envelope at send time. + """ + tmp_path = cmake(["sentry_example"], {"SENTRY_BACKEND": "crashpad"}) + cache_dir = tmp_path.joinpath(".sentry-native/cache") + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + httpserver.expect_oneshot_request("/api/123456/minidump/").respond_with_data("OK") + + # First run: crash — crashpad handler uploads minidump directly + with httpserver.wait(timeout=10) as waiting: + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "crash"], + expect_failure=True, + env=env, + ) + assert waiting.result + + # Second run: process_completed_reports converts crashpad report to + # cached envelope + external minidump in cache/ + run( + tmp_path, + "sentry_example", + ["log", "cache-keep", "no-setup"], + env=env, + ) + + # Verify cached envelope exists with external minidump dir + assert cache_dir.exists() + cache_files = list(cache_dir.glob("*.envelope")) + assert len(cache_files) == 1 + + att_dirs = [d for d in cache_dir.iterdir() if d.is_dir()] + assert len(att_dirs) > 0, "expected external attachment dir in cache" + att_files = [ + f for f in att_dirs[0].iterdir() if f.name != "__sentry-attachments.json" + ] + refs_files = list(att_dirs[0].glob("__sentry-attachments.json")) + assert len(refs_files) == 1, "expected __sentry-attachments.json" + assert len(att_files) > 0, "expected minidump file in attachment dir" diff --git a/tests/test_integration_http.py b/tests/test_integration_http.py index 0561f98dc..56040f9cc 100644 --- a/tests/test_integration_http.py +++ b/tests/test_integration_http.py @@ -1181,3 +1181,73 @@ def test_http_retry_session_on_network_error(cmake, httpserver): cache_files = list(cache_dir.glob("*.envelope")) assert len(cache_files) == 0 + + +@pytest.mark.skipif( + not has_breakpad, + reason="test needs breakpad backend", +) +def test_breakpad_cache_external_minidump(cmake, httpserver): + """ + Breakpad crash produces a minidump that is cached externally. + On restart, the minidump is resolved from cache and included in the + sent envelope. + """ + tmp_path = cmake( + ["sentry_example"], + {"SENTRY_BACKEND": "breakpad", "SENTRY_TRANSPORT_COMPRESSION": "Off"}, + ) + + env = dict(os.environ, SENTRY_DSN=make_dsn(httpserver)) + + # First run: crash + run( + tmp_path, + "sentry_example", + ["log", "start-session", "crash"], + expect_failure=True, + env=get_asan_crash_env(env), + ) + + # Verify minidump was cached externally in cache// + cache_dir = os.path.join(tmp_path, ".sentry-native", "cache") + if os.path.isdir(cache_dir): + att_dirs = [ + d + for d in os.listdir(cache_dir) + if os.path.isdir(os.path.join(cache_dir, d)) + ] + if att_dirs: + att_dir = os.path.join(cache_dir, att_dirs[0]) + refs_files = [ + f for f in os.listdir(att_dir) if f == "__sentry-attachments.json" + ] + assert len(refs_files) > 0, "expected __sentry-attachments.json" + + # Second run: restart picks up crash envelope and resolves external attachments + httpserver.expect_request( + "/api/123456/envelope/", + headers={"x-sentry-auth": auth_header}, + ).respond_with_data("OK") + + run( + tmp_path, + "sentry_example", + ["log", "no-setup"], + env=env, + ) + + assert len(httpserver.log) >= 1 + + # Find the crash envelope with a minidump + crash_envelope = None + for entry in httpserver.log: + body = entry[0].get_data() + envelope = Envelope.deserialize(body) + for item in envelope: + if item.headers.get("attachment_type") == "event.minidump": + crash_envelope = envelope + break + + assert crash_envelope is not None, "expected envelope with minidump" + assert_breakpad_crash(crash_envelope) From 07664673141112fc0a1cb3dadca45cd51cd487ae Mon Sep 17 00:00:00 2001 From: J-P Nurmi Date: Fri, 20 Mar 2026 13:53:37 +0100 Subject: [PATCH 121/121] fix(envelope): include content_type in raw attachment-ref payload append_raw_attachment_ref was ignoring the content_type parameter, causing MSVC C4100 (unused parameter) build failures on Windows and missing content_type in raw envelope payloads. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/sentry_envelope.c | 4 ++++ tests/unit/test_envelopes.c | 30 ++++++++++++++++++++++++++++++ tests/unit/tests.inc | 1 + 3 files changed, 35 insertions(+) diff --git a/src/sentry_envelope.c b/src/sentry_envelope.c index 2957a52ad..fa35b88a9 100644 --- a/src/sentry_envelope.c +++ b/src/sentry_envelope.c @@ -623,6 +623,10 @@ append_raw_attachment_ref(sentry_envelope_t *envelope, const char *location, sentry_value_t payload_obj = sentry_value_new_object(); sentry_value_set_by_key( payload_obj, "location", sentry_value_new_string(location)); + if (content_type) { + sentry_value_set_by_key( + payload_obj, "content_type", sentry_value_new_string(content_type)); + } sentry__jsonwriter_write_value(jw, payload_obj); sentry_value_decref(payload_obj); size_t payload_len = 0; diff --git a/tests/unit/test_envelopes.c b/tests/unit/test_envelopes.c index f0afaa35a..7b53cb5d4 100644 --- a/tests/unit/test_envelopes.c +++ b/tests/unit/test_envelopes.c @@ -1167,6 +1167,36 @@ SENTRY_TEST(attachment_ref_restore) sentry_close(); } +SENTRY_TEST(attachment_ref_raw_content_type) +{ + const char *test_file_str = SENTRY_TEST_PATH_PREFIX "sentry_test_raw_ref"; + sentry_path_t *test_file_path = sentry__path_from_str(test_file_str); + + // write a minimal envelope to disk so we can load it as raw + sentry_envelope_t *envelope = sentry__envelope_new(); + sentry_value_t event = sentry_value_new_object(); + sentry__envelope_add_event(envelope, event); + TEST_ASSERT(sentry_envelope_write_to_path(envelope, test_file_path) == 0); + sentry_envelope_free(envelope); + + // load as raw and append an attachment-ref with content_type + sentry_envelope_t *raw = sentry__envelope_from_path(test_file_path); + TEST_ASSERT(!!raw); + sentry__envelope_add_attachment_ref(raw, "https://up.example.com/abc", + "dump.dmp", "application/x-dmp", "event.minidump", + sentry_value_new_int32(42)); + + size_t size = 0; + char *serialized = sentry_envelope_serialize(raw, &size); + TEST_CHECK(!!serialized); + TEST_CHECK(!!strstr(serialized, "\"content_type\":\"application/x-dmp\"")); + sentry_free(serialized); + + sentry_envelope_free(raw); + sentry__path_remove(test_file_path); + sentry__path_free(test_file_path); +} + SENTRY_TEST(deserialize_envelope_invalid) { TEST_CHECK(!sentry_envelope_deserialize("", 0)); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 9a688e7d3..918f18d2f 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -6,6 +6,7 @@ XX(attachment_ref_copy) XX(attachment_ref_creation) XX(attachment_ref_from_path) XX(attachment_ref_move) +XX(attachment_ref_raw_content_type) XX(attachment_ref_restore) XX(attachments_add_dedupe) XX(attachments_add_remove)