Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
663f3bb
feat(transport): add HTTP retry logic with exponential backoff
jpnurmi Feb 12, 2026
52a273e
fix(retry): filter startup scan by timestamp, create cache dir
jpnurmi Feb 12, 2026
e230f8b
fix(retry): use wall clock time instead of monotonic time
jpnurmi Feb 12, 2026
3810f2b
ref(retry): define backoff base in seconds
jpnurmi Feb 12, 2026
a910f32
ref(retry): replace scan+free_paths with foreach callback API
jpnurmi Feb 12, 2026
8b8921b
feat(transport): set 15s request timeout for curl and winhttp
jpnurmi Feb 12, 2026
46d2a9e
fix(retry): avoid duplicate delayed retry tasks on startup
jpnurmi Feb 12, 2026
4f3d625
ref(retry): take options in sentry__retry_new, own path construction
jpnurmi Feb 12, 2026
199bd30
ref(retry): set startup_time at creation, remove setter
jpnurmi Feb 12, 2026
94a7ca7
fix(retry): return total file count so polling continues during backoff
jpnurmi Feb 12, 2026
011fbb5
fix(retry): use callback return value to track remaining retry files
jpnurmi Feb 12, 2026
f673193
ref(retry): rename constants to SENTRY_RETRY_INTERVAL and SENTRY_RETR…
jpnurmi Feb 12, 2026
caef6c1
ref(retry): encapsulate retry scheduling into the retry module
jpnurmi Feb 12, 2026
9c7c99d
ref(transport): remove unnecessary includes, restore blank line
jpnurmi Feb 12, 2026
3813feb
ref(retry): move precondition checks to callers
jpnurmi Feb 12, 2026
ea5233f
ref(transport): extract http_send_envelope helper
jpnurmi Feb 12, 2026
f17f8f9
test(retry): remove redundant retry_no_duplicate_rescan test
jpnurmi Feb 12, 2026
e7886a4
ref(curl): use CURLOPT_TIMEOUT_MS for consistency with winhttp and cr…
jpnurmi Feb 12, 2026
ce4ac80
ref(retry): unify startup and poll into a single task
jpnurmi Feb 12, 2026
34d5131
ref(retry): extract sentry__retry_make_path helper
jpnurmi Feb 12, 2026
8015d2c
fix(retry): prevent envelope duplication between retry and cache
jpnurmi Feb 12, 2026
8fcdb4f
ref(database): derive can_cache flag to skip cache dir creation early
jpnurmi Feb 12, 2026
551ad74
ref(retry): change send callback to envelope-based API
jpnurmi Feb 12, 2026
d2f1a07
test(retry): verify cache_keep preserves envelopes on successful send
jpnurmi Feb 12, 2026
3750856
fix(retry): use PRIu64 format specifier for uint64_t
jpnurmi Feb 12, 2026
0cbf407
fix(retry): guard against unsigned underflow in backoff check
jpnurmi Feb 12, 2026
3b2a5d6
fix(retry): prevent startup poll from re-processing same-session enve…
jpnurmi Feb 13, 2026
6608084
fix(retry): flush pending retries on shutdown
jpnurmi Feb 14, 2026
961ec95
ref(retry): use millisecond timestamps for retry filenames
jpnurmi Feb 14, 2026
558c32e
fix(retry): flush pending retries synchronously before shutdown
jpnurmi Feb 14, 2026
be1d7cf
fix(retry): stop retrying on network failure
jpnurmi Feb 14, 2026
fccc565
fix(retry): dump unsent envelopes to retry dir on shutdown timeout
jpnurmi Feb 14, 2026
d831987
test(retry): update expectations for stop-on-failure behavior
jpnurmi Feb 14, 2026
69f4f68
style(retry): fix line length in unit tests
jpnurmi Feb 14, 2026
5562feb
fix(retry): prevent duplicate envelope writes from detached worker
jpnurmi Feb 14, 2026
0664fb3
docs: add changelog entry for HTTP retry feature
jpnurmi Feb 14, 2026
c241adf
test(retry): use sentry__retry_send instead of duplicated eligibility…
jpnurmi Feb 14, 2026
a053ccb
fix(retry): raise backoff cap from 2h to 8h to match crashpad
jpnurmi Feb 14, 2026
71ee17a
refactor(retry): introduce retry_item_t to avoid re-parsing filenames
jpnurmi Feb 15, 2026
dfe7c1f
feat(retry): add debug and warning output for HTTP retries
jpnurmi Feb 15, 2026
5d80da6
refactor(cache): add cache_path to sentry_run_t and centralize cache …
jpnurmi Feb 15, 2026
06cca66
fix(transport): use connect-only timeouts for curl and winhttp
jpnurmi Feb 15, 2026
fd6c577
fix(retry): decrement total count when removing corrupt envelope files
jpnurmi Feb 15, 2026
173de80
fix(retry): only warn about exhausted retries on network failure
jpnurmi Feb 15, 2026
0482bf4
docs(retry): add doc comments to sentry_retry.h declarations
jpnurmi Feb 15, 2026
39f36ec
feat(transport): add sentry_transport_retry()
jpnurmi Feb 15, 2026
7b30688
refactor(retry): store retry envelopes in cache/ directory
jpnurmi Feb 15, 2026
e6c8db4
fix(retry): own cache_path to prevent use-after-free on detached thread
jpnurmi Feb 16, 2026
c520575
fix(retry): don't consume shutdown timeout with bgworker flush
jpnurmi Feb 16, 2026
79d1732
fix(retry): flush in-flight retries before shutdown
jpnurmi Feb 16, 2026
344dcc8
refactor(retry): replace http_retries count with boolean http_retry
jpnurmi Feb 16, 2026
5cdcc47
fix(transport): use explicit WinHTTP send/receive timeouts
jpnurmi Feb 16, 2026
9136500
fix(retry): deduplicate poll tasks on concurrent envelope failures
jpnurmi Feb 16, 2026
9b14dc2
fix(retry): set sealed flag before dumping queued envelopes
jpnurmi Feb 16, 2026
e75f595
fix(retry): prevent retry flush from consuming shutdown timeout
jpnurmi Feb 16, 2026
136fabf
fix(retry): zero-initialize retry struct after malloc
jpnurmi Feb 16, 2026
1a0d99b
fix(retry): skip flush task after seal to prevent duplicate sends
jpnurmi Feb 16, 2026
ed28b85
refactor(database): remove unused sentry__run_write_cache
jpnurmi Feb 16, 2026
62999c3
fix(retry): make trigger one-shot to prevent rapid retry exhaustion
jpnurmi Feb 17, 2026
70b1040
fix(core): check http_retry option instead of transport capability
jpnurmi Feb 17, 2026
794df4b
fix(retry): prevent UB from negative count in backoff shift
jpnurmi Feb 17, 2026
83475e4
fix(options): normalize http_retry with !! to match other boolean set…
jpnurmi Feb 17, 2026
8aae746
revert(database): restore original variable names and whitespace in w…
jpnurmi Feb 23, 2026
791d598
docs: clarify sentry_transport_retry behavior and limitations
jpnurmi Feb 24, 2026
a2efe25
docs(retry): document retry behavior for network failures vs HTTP res…
jpnurmi Feb 24, 2026
4b1e7d7
fix(retry): only clear startup_time when envelope is written
jpnurmi Feb 24, 2026
6064f1d
fix(retry): check for NULL from sentry__path_clone
jpnurmi Feb 24, 2026
45ee886
fix(retry): apply backoff when system clock moves backward
jpnurmi Feb 24, 2026
ec59d5e
fix(retry): increase SENTRY_RETRY_ATTEMPTS to 6 to match Crashpad
jpnurmi Feb 24, 2026
00bef01
fix(retry): avoid retry flush consuming entire shutdown timeout
jpnurmi Feb 24, 2026
d96beb1
fix(retry): warn on failed retry envelope rename
jpnurmi Feb 24, 2026
985b3da
fix(retry): check for NULL from sentry__path_clone in retry send
jpnurmi Feb 24, 2026
87532a3
fix(retry): fix data race on startup_time between threads
jpnurmi Feb 24, 2026
754a65d
fix(retry): clear retry_func when retry fails to initialize
jpnurmi Feb 24, 2026
0e64115
fix(retry): persist non-event envelopes to the retry cache
jpnurmi Feb 25, 2026
9ab3e11
fix(retry): close race between poll task and enqueue on scheduled flag
jpnurmi Feb 25, 2026
5a10044
refactor(database): add retry_count to write_envelope, add sentry__ru…
jpnurmi Feb 25, 2026
f35817d
refactor(database): make sentry_run_t refcounted
jpnurmi Feb 25, 2026
123a7be
refactor(cache): strip retry prefix in move_cache and simplify handle…
jpnurmi Feb 25, 2026
acfd148
fix(cache): use cache_name instead of src_name for UUID in move_cache
jpnurmi Feb 25, 2026
019e0e9
fix(retry): prevent duplicate cache writes during shutdown race
jpnurmi Feb 26, 2026
0e83c47
fix(cache): replace length heuristic with proper filename parsing in …
jpnurmi Feb 26, 2026
5f8e452
docs: fix retry count 5 → 6 in sentry_transport_retry docs
jpnurmi Feb 26, 2026
69ffc09
fix(retry): prevent poll task from re-arming after shutdown
jpnurmi Feb 26, 2026
e50dcf5
fix(winhttp): cancel in-flight request before shutdown to unblock worker
jpnurmi Feb 26, 2026
db19679
fix(winhttp): fix double-close race on client->request between cancel…
jpnurmi Feb 26, 2026
898308d
fix(winhttp): use on_timeout callback to unblock worker instead of ca…
jpnurmi Feb 26, 2026
7db3274
fix(winhttp): remove unnecessary local request snapshot in winhttp_se…
jpnurmi Feb 26, 2026
5b32779
fix(sync): don't force running=0 before on_timeout callback
jpnurmi Feb 26, 2026
9c8d803
fix(test): disable transport retry in unit tests to fix valgrind flak…
jpnurmi Feb 26, 2026
a91842a
fix(retry): clear sealed_envelope after match to prevent address-reus…
jpnurmi Feb 26, 2026
c17329a
ref: consolidate filename formatting into make_cache_path
jpnurmi Mar 4, 2026
3461f52
Adapt to sentry__session_new() change
jpnurmi Mar 9, 2026
36568c1
ref: cache envelopes only on failed HTTP send
jpnurmi Mar 10, 2026
1d85dd8
Clarify bgworker shutdown timeout comment
jpnurmi Mar 10, 2026
168a807
Cache envelopes only on failed HTTP send
jpnurmi Mar 11, 2026
c0f920d
Remove unused sentry__transport_can_retry
jpnurmi Mar 11, 2026
bb06d6f
fix: incref options in cleanup task to prevent use-after-free
jpnurmi Mar 11, 2026
8840444
Update sentry docs URL for network failure handling
jpnurmi Mar 17, 2026
93d884a
Replace pointer comparison with envelope tag in retry seal check
jpnurmi Mar 17, 2026
93db900
Add TODO for retry jitter and shorter poll interval
jpnurmi Mar 17, 2026
8f104d2
Initialize tag for raw envelopes loaded from disk
cursoragent Mar 17, 2026
878ace5
Implement on_timeout for curl transport
jpnurmi Mar 18, 2026
003e9f3
Merge upstream/master into http-retry branch
jpnurmi Mar 30, 2026
381b476
Make http_retry off by default
jpnurmi Mar 30, 2026
a485974
Remove dead envelope tag mechanism
jpnurmi Mar 30, 2026
9672e6e
Remove dead retry_func fallback branch
jpnurmi Mar 30, 2026
e914431
Fix handle_result caching successfully delivered envelopes
jpnurmi Mar 30, 2026
2c12e50
Clean up unnecessary env variables in integration tests
jpnurmi Mar 30, 2026
c483e42
Fix dump_queue writing retry envelopes to run dir instead of cache
jpnurmi Mar 30, 2026
323cc5e
Fix multiple-envelope tests failing on Windows
jpnurmi Mar 30, 2026
a38f78b
Drop retry-count assertions from multiple_network_error test
jpnurmi Mar 30, 2026
5d4dbff
Remove premature STARTUP→RUNNING transition from retry_enqueue
jpnurmi Mar 30, 2026
556ecc7
Drop flaky retry-count assertions from multiple_network_error test
jpnurmi Mar 30, 2026
5599fb9
Make retry_flush_task unconditional
jpnurmi Mar 30, 2026
8447dc9
Fix retry_flush_task re-processing same-session envelopes
jpnurmi Mar 30, 2026
382084d
Remove retry_send_cb wrapper
jpnurmi Mar 30, 2026
899b385
Remove retry filename formatting from write_envelope
jpnurmi Mar 30, 2026
8371796
Remove stale sentry_sync.h include from sentry_envelope.c
jpnurmi Mar 30, 2026
14a0aef
Merge remote-tracking branch 'upstream/master' into jpnurmi/feat/http…
jpnurmi Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

**Features**:

- Add HTTP retry with exponential backoff, opt-in via `sentry_options_set_http_retry`. ([#1520](https://github.com/getsentry/sentry-native/pull/1520))
- Store minidump attachments as separate `.dmp` files in the offline cache for direct debugger access. ([#1607](https://github.com/getsentry/sentry-native/pull/1607))

**Fixes**:
Expand Down
6 changes: 6 additions & 0 deletions examples/example.c
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,12 @@ 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_retry(options, true);
}
if (has_arg(argc, argv, "no-http-retry")) {
sentry_options_set_http_retry(options, false);
}

if (has_arg(argc, argv, "enable-metrics")) {
sentry_options_set_enable_metrics(options, true);
Expand Down
30 changes: 30 additions & 0 deletions include/sentry.h
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,24 @@ 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.
*
* 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 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).
*
* 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);

/**
* Generic way to free transport.
*/
Expand Down Expand Up @@ -2260,6 +2278,18 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs(
SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs(
const sentry_options_t *opts);

/**
* Enables or disables HTTP retry with exponential backoff for network failures.
*
* Only applicable for HTTP transports.
*
* Disabled by default.
*/
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);

/**
* Enables or disables custom attributes parsing for structured logging.
*
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/backends/native/sentry_crash_daemon.c
Original file line number Diff line number Diff line change
Expand Up @@ -3152,6 +3152,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle,
sentry_options_set_debug(options, ipc->shmem->debug_enabled);
options->attach_screenshot = ipc->shmem->attach_screenshot;
options->cache_keep = ipc->shmem->cache_keep;
options->http_retry = false;

// Set custom logger that writes to file
if (log_file) {
Expand Down
2 changes: 1 addition & 1 deletion src/sentry_core.c
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ sentry_init(sentry_options_t *options)
backend->prune_database_func(backend);
}

if (options->cache_keep) {
if (options->cache_keep || options->http_retry) {
if (!sentry__transport_submit_cleanup(options->transport, options)) {
sentry__cleanup_cache(options);
}
Expand Down
110 changes: 108 additions & 2 deletions src/sentry_database.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,120 @@ sentry__run_write_external(

bool
sentry__run_write_cache(
const sentry_run_t *run, const sentry_envelope_t *envelope)
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;
}

if (retry_count < 0) {
return sentry__envelope_write_to_cache(envelope, run->cache_path) == 0;
}

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__run_make_cache_path(
run, sentry__usec_time() / 1000, retry_count, uuid);
if (!path) {
return false;
}

int rv = sentry_envelope_write_to_path(envelope, path);
sentry__path_free(path);
if (rv) {
SENTRY_WARN("writing envelope to file failed");
}
return rv == 0;
}

bool
sentry__parse_cache_filename(const char *filename, uint64_t *ts_out,
int *count_out, const char **uuid_out)
{
// Minimum retry filename: <ts>-<count>-<uuid>.envelope (49+ chars).
// Cache filenames are exactly 45 chars (<uuid>.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];
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);
}

bool
sentry__run_move_cache(
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;
}

return sentry__envelope_write_to_cache(envelope, run->cache_path) == 0;
const char *src_name = sentry__path_filename(src);
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;

sentry_path_t *dst_path = sentry__run_make_cache_path(
run, sentry__usec_time() / 1000, retry_count, cache_name);
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\"", src_name);
return false;
}

return true;
}

bool
Expand Down
30 changes: 27 additions & 3 deletions src/sentry_database.h
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,27 @@ bool sentry__run_clear_session(const sentry_run_t *run);

/**
* This will serialize and write the given envelope to disk into the cache
* directory: `<database>/cache/<event-uuid>.envelope`
* directory. When retry_count >= 0 the filename uses retry format
* `<ts>-<count>-<uuid>.envelope`, otherwise `<uuid>.envelope`.
*/
bool sentry__run_write_cache(
const sentry_run_t *run, const sentry_envelope_t *envelope);
bool sentry__run_write_cache(const sentry_run_t *run,
const sentry_envelope_t *envelope, int retry_count);

/**
* Moves a file into the cache directory. When retry_count >= 0 the
* destination uses retry format `<ts>-<count>-<uuid>.envelope`,
* otherwise the original filename is preserved.
*/
bool sentry__run_move_cache(
const sentry_run_t *run, const sentry_path_t *src, int retry_count);

/**
* Builds a cache path. When count >= 0 the result is
* `<db>/cache/<ts>-<count>-<uuid>.envelope`, otherwise
* `<db>/cache/<uuid>.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
Expand All @@ -92,6 +109,13 @@ bool sentry__run_write_cache(
void sentry__process_old_runs(
const sentry_options_t *options, uint64_t last_crash);

/**
* Parses a retry cache filename: `<ts>-<count>-<uuid>.envelope`.
* Returns false for plain cache filenames (`<uuid>.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.
Expand Down
13 changes: 13 additions & 0 deletions src/sentry_options.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 = false;

return opts;
}
Expand Down Expand Up @@ -876,6 +877,18 @@ sentry_options_set_handler_strategy(

#endif // SENTRY_PLATFORM_LINUX

void
sentry_options_set_http_retry(sentry_options_t *opts, int enabled)
{
opts->http_retry = !!enabled;
}

int
sentry_options_get_http_retry(const sentry_options_t *opts)
{
return opts->http_retry;
}

void
sentry_options_set_propagate_traceparent(
sentry_options_t *opts, int propagate_traceparent)
Expand Down
1 change: 1 addition & 0 deletions src/sentry_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ struct sentry_options_s {
bool enable_metrics;
sentry_before_send_metric_function_t before_send_metric_func;
void *before_send_metric_data;
bool http_retry;

/* everything from here on down are options which are stored here but
not exposed through the options API */
Expand Down
Loading
Loading