Skip to content

Commit 72e1d42

Browse files
jpnurmiclaudecursoragent
authored
feat(transport): add HTTP retry with exponential backoff (#1520)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent bcd4633 commit 72e1d42

21 files changed

+1646
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
**Features**:
66

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

910
**Fixes**:

examples/example.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,12 @@ main(int argc, char **argv)
676676
sentry_options_set_cache_max_age(options, 5 * 24 * 60 * 60); // 5 days
677677
sentry_options_set_cache_max_items(options, 5);
678678
}
679+
if (has_arg(argc, argv, "http-retry")) {
680+
sentry_options_set_http_retry(options, true);
681+
}
682+
if (has_arg(argc, argv, "no-http-retry")) {
683+
sentry_options_set_http_retry(options, false);
684+
}
679685

680686
if (has_arg(argc, argv, "enable-metrics")) {
681687
sentry_options_set_enable_metrics(options, true);

include/sentry.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,24 @@ SENTRY_API void sentry_transport_set_shutdown_func(
946946
sentry_transport_t *transport,
947947
int (*shutdown_func)(uint64_t timeout, void *state));
948948

949+
/**
950+
* Retries sending all pending envelopes in the transport's retry queue,
951+
* e.g. when coming back online. Only applicable for HTTP transports.
952+
*
953+
* Note: The SDK automatically retries failed envelopes on next application
954+
* startup. This function allows manual triggering of pending retries at
955+
* runtime. Each envelope is retried up to 6 times. If all attempts are
956+
* exhausted during intermittent connectivity, events will be discarded
957+
* (or moved to cache if enabled via sentry_options_set_cache_keep).
958+
*
959+
* Warning: This function has no rate limiting - it will immediately
960+
* attempt to send all pending envelopes. Calling this repeatedly during
961+
* extended network outages may exhaust retry attempts that might have
962+
* succeeded with the SDK's built-in exponential backoff.
963+
*/
964+
SENTRY_EXPERIMENTAL_API void sentry_transport_retry(
965+
sentry_transport_t *transport);
966+
949967
/**
950968
* Generic way to free transport.
951969
*/
@@ -2260,6 +2278,18 @@ SENTRY_EXPERIMENTAL_API void sentry_options_set_enable_logs(
22602278
SENTRY_EXPERIMENTAL_API int sentry_options_get_enable_logs(
22612279
const sentry_options_t *opts);
22622280

2281+
/**
2282+
* Enables or disables HTTP retry with exponential backoff for network failures.
2283+
*
2284+
* Only applicable for HTTP transports.
2285+
*
2286+
* Disabled by default.
2287+
*/
2288+
SENTRY_EXPERIMENTAL_API void sentry_options_set_http_retry(
2289+
sentry_options_t *opts, int enabled);
2290+
SENTRY_EXPERIMENTAL_API int sentry_options_get_http_retry(
2291+
const sentry_options_t *opts);
2292+
22632293
/**
22642294
* Enables or disables custom attributes parsing for structured logging.
22652295
*

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ sentry_target_sources_cwd(sentry
3434
sentry_process.h
3535
sentry_ratelimiter.c
3636
sentry_ratelimiter.h
37+
sentry_retry.c
38+
sentry_retry.h
3739
sentry_ringbuffer.c
3840
sentry_ringbuffer.h
3941
sentry_sampling_context.h

src/backends/native/sentry_crash_daemon.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3152,6 +3152,7 @@ sentry__crash_daemon_main(pid_t app_pid, uint64_t app_tid, HANDLE event_handle,
31523152
sentry_options_set_debug(options, ipc->shmem->debug_enabled);
31533153
options->attach_screenshot = ipc->shmem->attach_screenshot;
31543154
options->cache_keep = ipc->shmem->cache_keep;
3155+
options->http_retry = false;
31553156

31563157
// Set custom logger that writes to file
31573158
if (log_file) {

src/sentry_core.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ sentry_init(sentry_options_t *options)
292292
backend->prune_database_func(backend);
293293
}
294294

295-
if (options->cache_keep) {
295+
if (options->cache_keep || options->http_retry) {
296296
if (!sentry__transport_submit_cleanup(options->transport, options)) {
297297
sentry__cleanup_cache(options);
298298
}

src/sentry_database.c

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,14 +179,120 @@ sentry__run_write_external(
179179

180180
bool
181181
sentry__run_write_cache(
182-
const sentry_run_t *run, const sentry_envelope_t *envelope)
182+
const sentry_run_t *run, const sentry_envelope_t *envelope, int retry_count)
183+
{
184+
if (sentry__path_create_dir_all(run->cache_path) != 0) {
185+
SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path);
186+
return false;
187+
}
188+
189+
if (retry_count < 0) {
190+
return sentry__envelope_write_to_cache(envelope, run->cache_path) == 0;
191+
}
192+
193+
sentry_uuid_t event_id = sentry__envelope_get_event_id(envelope);
194+
if (sentry_uuid_is_nil(&event_id)) {
195+
event_id = sentry_uuid_new_v4();
196+
}
197+
198+
char uuid[37];
199+
sentry_uuid_as_string(&event_id, uuid);
200+
201+
sentry_path_t *path = sentry__run_make_cache_path(
202+
run, sentry__usec_time() / 1000, retry_count, uuid);
203+
if (!path) {
204+
return false;
205+
}
206+
207+
int rv = sentry_envelope_write_to_path(envelope, path);
208+
sentry__path_free(path);
209+
if (rv) {
210+
SENTRY_WARN("writing envelope to file failed");
211+
}
212+
return rv == 0;
213+
}
214+
215+
bool
216+
sentry__parse_cache_filename(const char *filename, uint64_t *ts_out,
217+
int *count_out, const char **uuid_out)
218+
{
219+
// Minimum retry filename: <ts>-<count>-<uuid>.envelope (49+ chars).
220+
// Cache filenames are exactly 45 chars (<uuid>.envelope).
221+
if (strlen(filename) <= 45) {
222+
return false;
223+
}
224+
225+
char *end;
226+
uint64_t ts = strtoull(filename, &end, 10);
227+
if (*end != '-') {
228+
return false;
229+
}
230+
231+
const char *count_str = end + 1;
232+
long count = strtol(count_str, &end, 10);
233+
if (*end != '-' || count < 0) {
234+
return false;
235+
}
236+
237+
const char *uuid = end + 1;
238+
size_t tail_len = strlen(uuid);
239+
// 36 chars UUID (with dashes) + ".envelope"
240+
if (tail_len != 36 + 9 || strcmp(uuid + 36, ".envelope") != 0) {
241+
return false;
242+
}
243+
244+
*ts_out = ts;
245+
*count_out = (int)count;
246+
*uuid_out = uuid;
247+
return true;
248+
}
249+
250+
sentry_path_t *
251+
sentry__run_make_cache_path(
252+
const sentry_run_t *run, uint64_t ts, int count, const char *uuid)
253+
{
254+
char filename[128];
255+
if (count >= 0) {
256+
snprintf(filename, sizeof(filename), "%" PRIu64 "-%02d-%.36s.envelope",
257+
ts, count, uuid);
258+
} else {
259+
snprintf(filename, sizeof(filename), "%.36s.envelope", uuid);
260+
}
261+
return sentry__path_join_str(run->cache_path, filename);
262+
}
263+
264+
bool
265+
sentry__run_move_cache(
266+
const sentry_run_t *run, const sentry_path_t *src, int retry_count)
183267
{
184268
if (sentry__path_create_dir_all(run->cache_path) != 0) {
185269
SENTRY_ERRORF("mkdir failed: \"%s\"", run->cache_path->path);
186270
return false;
187271
}
188272

189-
return sentry__envelope_write_to_cache(envelope, run->cache_path) == 0;
273+
const char *src_name = sentry__path_filename(src);
274+
uint64_t parsed_ts;
275+
int parsed_count;
276+
const char *parsed_uuid;
277+
const char *cache_name = sentry__parse_cache_filename(src_name, &parsed_ts,
278+
&parsed_count, &parsed_uuid)
279+
? parsed_uuid
280+
: src_name;
281+
282+
sentry_path_t *dst_path = sentry__run_make_cache_path(
283+
run, sentry__usec_time() / 1000, retry_count, cache_name);
284+
if (!dst_path) {
285+
return false;
286+
}
287+
288+
int rv = sentry__path_rename(src, dst_path);
289+
sentry__path_free(dst_path);
290+
if (rv != 0) {
291+
SENTRY_WARNF("failed to cache envelope \"%s\"", src_name);
292+
return false;
293+
}
294+
295+
return true;
190296
}
191297

192298
bool

src/sentry_database.h

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,27 @@ bool sentry__run_clear_session(const sentry_run_t *run);
7272

7373
/**
7474
* This will serialize and write the given envelope to disk into the cache
75-
* directory: `<database>/cache/<event-uuid>.envelope`
75+
* directory. When retry_count >= 0 the filename uses retry format
76+
* `<ts>-<count>-<uuid>.envelope`, otherwise `<uuid>.envelope`.
7677
*/
77-
bool sentry__run_write_cache(
78-
const sentry_run_t *run, const sentry_envelope_t *envelope);
78+
bool sentry__run_write_cache(const sentry_run_t *run,
79+
const sentry_envelope_t *envelope, int retry_count);
80+
81+
/**
82+
* Moves a file into the cache directory. When retry_count >= 0 the
83+
* destination uses retry format `<ts>-<count>-<uuid>.envelope`,
84+
* otherwise the original filename is preserved.
85+
*/
86+
bool sentry__run_move_cache(
87+
const sentry_run_t *run, const sentry_path_t *src, int retry_count);
88+
89+
/**
90+
* Builds a cache path. When count >= 0 the result is
91+
* `<db>/cache/<ts>-<count>-<uuid>.envelope`, otherwise
92+
* `<db>/cache/<uuid>.envelope`.
93+
*/
94+
sentry_path_t *sentry__run_make_cache_path(
95+
const sentry_run_t *run, uint64_t ts, int count, const char *uuid);
7996

8097
/**
8198
* This function is essential to send crash reports from previous runs of the
@@ -92,6 +109,13 @@ bool sentry__run_write_cache(
92109
void sentry__process_old_runs(
93110
const sentry_options_t *options, uint64_t last_crash);
94111

112+
/**
113+
* Parses a retry cache filename: `<ts>-<count>-<uuid>.envelope`.
114+
* Returns false for plain cache filenames (`<uuid>.envelope`).
115+
*/
116+
bool sentry__parse_cache_filename(const char *filename, uint64_t *ts_out,
117+
int *count_out, const char **uuid_out);
118+
95119
/**
96120
* Cleans up the cache based on options.cache_max_items,
97121
* options.cache_max_size and options.cache_max_age.

src/sentry_options.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ sentry_options_new(void)
8282
opts->crash_reporting_mode
8383
= SENTRY_CRASH_REPORTING_MODE_NATIVE_WITH_MINIDUMP; // Default: best of
8484
// both worlds
85+
opts->http_retry = false;
8586

8687
return opts;
8788
}
@@ -876,6 +877,18 @@ sentry_options_set_handler_strategy(
876877

877878
#endif // SENTRY_PLATFORM_LINUX
878879

880+
void
881+
sentry_options_set_http_retry(sentry_options_t *opts, int enabled)
882+
{
883+
opts->http_retry = !!enabled;
884+
}
885+
886+
int
887+
sentry_options_get_http_retry(const sentry_options_t *opts)
888+
{
889+
return opts->http_retry;
890+
}
891+
879892
void
880893
sentry_options_set_propagate_traceparent(
881894
sentry_options_t *opts, int propagate_traceparent)

src/sentry_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ struct sentry_options_s {
7878
bool enable_metrics;
7979
sentry_before_send_metric_function_t before_send_metric_func;
8080
void *before_send_metric_data;
81+
bool http_retry;
8182

8283
/* everything from here on down are options which are stored here but
8384
not exposed through the options API */

0 commit comments

Comments
 (0)