From a4169f8a23a57a4649fd17261dc8e162d943c9c8 Mon Sep 17 00:00:00 2001 From: Huzaifa Abdul Rehman Date: Thu, 13 Aug 2026 03:26:26 +0500 Subject: [PATCH 1/7] feat(native): add public custom HTTP transport client interface Expose sentry_http_transport_new so applications can plug in their own HTTP client (platform-native ones like Qt, .NET, or Dart) while sentry-native keeps owning request queueing, envelope ordering, retry with exponential backoff, offline caching, rate-limit handling, and client reports. The curl and WinHTTP transports are now implemented against this same interface, deduplicating the Sentry-specific response header parsing that used to be copy-pasted in both. --- CHANGELOG.md | 1 + include/sentry.h | 181 +++++++++++ src/transports/sentry_http_transport.c | 153 +++++++++- src/transports/sentry_http_transport.h | 18 +- src/transports/sentry_http_transport_curl.c | 18 +- .../sentry_http_transport_winhttp.c | 23 +- tests/unit/CMakeLists.txt | 1 + tests/unit/test_http_transport.c | 288 ++++++++++++++++++ tests/unit/test_retry.c | 2 +- tests/unit/test_tus.c | 6 +- tests/unit/tests.inc | 15 + 11 files changed, 676 insertions(+), 30 deletions(-) create mode 100644 tests/unit/test_http_transport.c diff --git a/CHANGELOG.md b/CHANGELOG.md index 918b0c7d84..b9e57445d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Forward `enable_logs` option through `NdkOptions` so the Android SDK can enable native structured logging. ([#1971](https://github.com/getsentry/sentry-native/pull/1971)) - Report enabled Qt and WER integrations in event SDK metadata (`sdk.integrations`) alongside the configured crash backend. ([#1969](https://github.com/ getsentry/sentry-native/pull/1969)) +- Add a public custom HTTP transport client interface (`sentry_http_transport_new`) so applications can plug in their own HTTP client (e.g. platform-native ones) while sentry-native continues to own request queueing, retry/backoff, offline caching, rate-limiting, and client reports. The built-in curl and WinHTTP transports are now implemented against this same interface. ([#XXXX](https://github.com/getsentry/sentry-native/pull/XXXX)) - Add `sentry_scope_set_span` and `sentry_scope_set_transaction_object` to bind a span or transaction to a given scope. ([#1981](https://github.com/getsentry/sentry-native/pull/1981)) **Fixes**: diff --git a/include/sentry.h b/include/sentry.h index 271f2d0006..2a6fe33f6a 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -972,6 +972,187 @@ SENTRY_EXPERIMENTAL_API void sentry_transport_retry( */ SENTRY_API void sentry_transport_free(sentry_transport_t *transport); +/* -- HTTP Transport Client Interface (Experimental) -- */ + +/** + * Represents a single HTTP request prepared by sentry-native, to be + * executed by a custom HTTP client set up via `sentry_http_transport_new`. + * + * The pointer is only valid for the duration of the + * `sentry_http_client_send_func_t` call it was passed to; the client must + * not retain it afterwards. + */ +struct sentry_prepared_http_request_s; +typedef struct sentry_prepared_http_request_s sentry_http_request_t; + +/** + * Returns the HTTP method of `req`, such as `"POST"` or `"PATCH"`. + */ +SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_method( + const sentry_http_request_t *req); + +/** + * Returns the fully-resolved request URL of `req`. + */ +SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_url( + const sentry_http_request_t *req); + +/** + * Returns the number of headers sentry-native has prepared for `req`. Use + * together with `sentry_http_request_get_header` to iterate over them. + */ +SENTRY_EXPERIMENTAL_API size_t sentry_http_request_get_header_count( + const sentry_http_request_t *req); + +/** + * Retrieves the header at `index` (`0 <= index < + * sentry_http_request_get_header_count(req)`) into `*key_out` and + * `*value_out`. Returns `1` on success, `0` if `index` is out of range. The + * returned pointers are only valid for the duration of the current + * `sentry_http_client_send_func_t` call. + */ +SENTRY_EXPERIMENTAL_API int sentry_http_request_get_header( + const sentry_http_request_t *req, size_t index, const char **key_out, + const char **value_out); + +/** + * Returns the in-memory request body of `req` and writes its length to + * `*len_out`. Returns `NULL` if `req` has no in-memory body, which happens + * for large-attachment uploads that stream a file instead -- see + * `sentry_http_request_get_body_file_path`. Exactly one of the two ever + * returns non-`NULL` for a given request. + */ +SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body( + const sentry_http_request_t *req, size_t *len_out); + +/** + * Returns the path to a file that should be streamed as the request body of + * `req`, and writes its size to `*len_out`. Returns `NULL` unless `req` + * requires a file-backed body -- see `sentry_http_request_get_body`. + */ +SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body_file_path( + const sentry_http_request_t *req, size_t *len_out); + +/** + * Represents the response to a single HTTP request, to be filled in by a + * custom HTTP client's `sentry_http_client_send_func_t` before returning. + * + * The pointer is only valid for the duration of that call. + */ +struct sentry_http_response_s; +typedef struct sentry_http_response_s sentry_http_response_t; + +/** + * Sets the HTTP status code of `resp`. + */ +SENTRY_EXPERIMENTAL_API void sentry_http_response_set_status_code( + sentry_http_response_t *resp, int status_code); + +/** + * Records a response header on `resp`. `key` is matched case-insensitively + * against the headers sentry-native cares about (currently `Retry-After`, + * `X-Sentry-Rate-Limits`, and `Location`); anything else is ignored. Pass + * every header the HTTP response actually had -- sentry-native, not the + * client, decides which ones matter, so headers Sentry starts caring about + * later don't require client changes. + */ +SENTRY_EXPERIMENTAL_API void sentry_http_response_set_header( + sentry_http_response_t *resp, const char *key, const char *value); + +/** + * Marks `resp` as having failed because the transport is shutting down, + * rather than because of a regular request error. Only meaningful when the + * client's `sentry_http_client_send_func_t` call is about to return `0` + * (failure); sentry-native uses this to skip retry/caching for requests + * that failed only because shutdown interrupted them. + */ +SENTRY_EXPERIMENTAL_API void sentry_http_response_set_shutdown( + sentry_http_response_t *resp, int is_shutdown); + +/** + * Creates a new HTTP client instance for a transport created with + * `sentry_http_transport_new`. Returns an opaque client pointer, or `NULL` + * on failure. `factory_data` is the pointer passed to + * `sentry_http_transport_new`, unchanged and still owned by the caller. + * + * sentry-native currently calls this exactly once per transport, on the + * thread that calls `sentry_http_transport_new`, to create the single + * client the transport's background thread will use for every request. A + * future multi-threaded transport may call this once per worker thread + * instead, each time from that worker thread; implementations should not + * assume they are called from any particular thread, or exactly once. + */ +typedef void *(*sentry_http_client_factory_func_t)(void *factory_data); + +/** + * Executes a single HTTP request using `client` (as returned by the + * transport's `sentry_http_client_factory_func_t`), and fills in `resp` via + * `sentry_http_response_set_status_code` and + * `sentry_http_response_set_header`. + * + * Returns `1` on success. Returning `0` marks the request as failed for + * sentry-native's retry/caching logic; call + * `sentry_http_response_set_shutdown` first if the failure is because the + * transport is shutting down rather than a request error. + * + * sentry-native calls this from a single background thread, once per + * request, in the order requests were queued, and never invokes it again + * for the same client until the previous call returns. A future + * multi-threaded transport may drive different client instances from + * different threads concurrently, but will still only ever call into one + * client instance from one thread at a time. + */ +typedef int (*sentry_http_client_send_func_t)( + void *client, sentry_http_request_t *req, sentry_http_response_t *resp); + +/** + * Creates a new HTTP transport that executes requests through a custom HTTP + * client, while sentry-native continues to own request preparation and + * serialization, queueing, envelope ordering, HTTP retry with exponential + * backoff, offline caching, rate-limit handling, client reports, and + * flush/shutdown behavior -- the same behavior the built-in curl and + * WinHTTP transports get, since they are implemented against this same + * interface. + * + * `factory` is called to create the client instance passed to `send_func` + * and to the optional hook registered with + * `sentry_http_transport_set_client_start_func` and + * `sentry_http_transport_set_client_shutdown_func`. `factory_data` is + * passed through to `factory` unchanged; sentry-native does not take + * ownership of it. + * + * `client_free_func` frees the client created by `factory`. It is used both + * if transport creation fails after `factory` already produced a client, + * and later when the returned transport itself is freed. Pass `NULL` if + * the client owns no resources that need freeing. + * + * Returns `NULL` if `factory` or `send_func` is `NULL`, or if the client + * factory itself fails. + */ +SENTRY_EXPERIMENTAL_API sentry_transport_t *sentry_http_transport_new( + sentry_http_client_factory_func_t factory, void *factory_data, + sentry_http_client_send_func_t send_func, + void (*client_free_func)(void *client)); + +/** + * Sets the hook that initializes the client once `sentry_options_t` is + * available, mirroring `sentry_transport_set_startup_func` for the + * transport itself. Called once, from within `sentry_init`, before the + * transport's background thread starts sending requests. Should return `0` + * on success; a non-zero return bubbles up to `sentry_init`. + */ +SENTRY_EXPERIMENTAL_API void sentry_http_transport_set_client_start_func( + sentry_transport_t *transport, + int (*start_func)(void *client, const sentry_options_t *options)); + +/** + * Sets the hook that tells the client the transport is shutting down, e.g. + * to unblock or cancel an in-flight request. Called at most once, when the + * transport is shut down. + */ +SENTRY_EXPERIMENTAL_API void sentry_http_transport_set_client_shutdown_func( + sentry_transport_t *transport, void (*shutdown_func)(void *client)); + /** * Create a new function transport. * diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 1fe9d2db4c..a6f71e76ee 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -35,7 +35,7 @@ typedef struct { void *client; void (*free_client)(void *); int (*start_client)(void *, const sentry_options_t *); - sentry_http_send_func_t send_func; + sentry_http_client_send_func_t send_func; void (*shutdown_client)(void *client); sentry_retry_t *retry; sentry_cache_keep_t cache_keep; @@ -330,6 +330,116 @@ http_response_cleanup(sentry_http_response_t *resp) sentry_free(resp->location); } +void +sentry_http_response_set_header( + sentry_http_response_t *resp, const char *key, const char *value) +{ + if (!resp || !key) { + return; + } + char *lower_key = sentry__string_clone(key); + if (!lower_key) { + return; + } + sentry__string_ascii_lower(lower_key); + + if (sentry__string_eq(lower_key, "retry-after")) { + sentry_free(resp->retry_after); + resp->retry_after = sentry__string_clone(value); + } else if (sentry__string_eq(lower_key, "x-sentry-rate-limits")) { + sentry_free(resp->x_sentry_rate_limits); + resp->x_sentry_rate_limits = sentry__string_clone(value); + } else if (sentry__string_eq(lower_key, "location")) { + sentry_free(resp->location); + resp->location = sentry__string_clone(value); + } + sentry_free(lower_key); +} + +void +sentry_http_response_set_status_code( + sentry_http_response_t *resp, int status_code) +{ + if (!resp) { + return; + } + resp->status_code = status_code; +} + +void +sentry_http_response_set_shutdown(sentry_http_response_t *resp, int is_shutdown) +{ + if (!resp) { + return; + } + resp->shutdown = !!is_shutdown; +} + +const char * +sentry_http_request_get_method(const sentry_http_request_t *req) +{ + return req ? req->method : NULL; +} + +const char * +sentry_http_request_get_url(const sentry_http_request_t *req) +{ + return req ? req->url : NULL; +} + +size_t +sentry_http_request_get_header_count(const sentry_http_request_t *req) +{ + return req ? req->headers_len : 0; +} + +int +sentry_http_request_get_header(const sentry_http_request_t *req, size_t index, + const char **key_out, const char **value_out) +{ + if (!req || index >= req->headers_len) { + return 0; + } + if (key_out) { + *key_out = req->headers[index].key; + } + if (value_out) { + *value_out = req->headers[index].value; + } + return 1; +} + +const char * +sentry_http_request_get_body(const sentry_http_request_t *req, size_t *len_out) +{ + if (!req || req->body_path) { + if (len_out) { + *len_out = 0; + } + return NULL; + } + if (len_out) { + *len_out = req->body_len; + } + return req->body; +} + +const char * +sentry_http_request_get_body_file_path( + const sentry_http_request_t *req, size_t *len_out) +{ + if (!req || !req->body_path) { + if (len_out) { + *len_out = 0; + } + return NULL; + } + if (len_out) { + *len_out = req->body_len; + } + return req->body_path->path; +} + enum { RESULT_OK = 0, RESULT_ERROR = -1, @@ -901,7 +1011,8 @@ http_transport_submit_cleanup( } sentry_transport_t * -sentry__http_transport_new(void *client, sentry_http_send_func_t send_func) +sentry__http_transport_new( + void *client, sentry_http_client_send_func_t send_func) { http_transport_state_t *state = SENTRY_MAKE(http_transport_state_t); if (!state) { @@ -960,6 +1071,44 @@ sentry__http_transport_set_shutdown_client( http_transport_get_state(transport)->shutdown_client = shutdown_client; } +sentry_transport_t * +sentry_http_transport_new(sentry_http_client_factory_func_t factory, + void *factory_data, sentry_http_client_send_func_t send_func, + void (*client_free_func)(void *client)) +{ + if (!factory || !send_func) { + return NULL; + } + void *client = factory(factory_data); + if (!client) { + return NULL; + } + sentry_transport_t *transport + = sentry__http_transport_new(client, send_func); + if (!transport) { + if (client_free_func) { + client_free_func(client); + } + return NULL; + } + sentry__http_transport_set_free_client(transport, client_free_func); + return transport; +} + +void +sentry_http_transport_set_client_start_func(sentry_transport_t *transport, + int (*start_func)(void *client, const sentry_options_t *options)) +{ + sentry__http_transport_set_start_client(transport, start_func); +} + +void +sentry_http_transport_set_client_shutdown_func( + sentry_transport_t *transport, void (*shutdown_func)(void *client)) +{ + sentry__http_transport_set_shutdown_client(transport, shutdown_func); +} + #ifdef SENTRY_UNITTEST void * sentry__http_transport_get_bgworker(sentry_transport_t *transport) diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index a1972c978d..ce906d2f17 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -35,23 +35,29 @@ sentry_prepared_http_request_t *sentry__prepare_tus_upload_request( void sentry__prepared_http_request_free(sentry_prepared_http_request_t *req); -typedef struct { +// The public `sentry_http_response_t` (see sentry.h) is a forward-declared +// opaque handle for this same tag; this is the only place its layout is +// defined. Internal code and the public accessors in sentry_http_transport.c +// both operate on this concrete struct via that shared tag. +struct sentry_http_response_s { int status_code; char *retry_after; char *x_sentry_rate_limits; char *location; bool shutdown; -} sentry_http_response_t; - -typedef bool (*sentry_http_send_func_t)(void *client, - sentry_prepared_http_request_t *req, sentry_http_response_t *resp); +}; /** * Creates a new HTTP transport with the given client and send function. * Use the setter functions below to configure optional client callbacks. + * + * `send_func` uses the same `sentry_http_client_send_func_t` signature (see + * sentry.h) that the public `sentry_http_transport_new` accepts, so the + * curl and WinHTTP default clients are themselves reference implementations + * of that public interface. */ sentry_transport_t *sentry__http_transport_new( - void *client, sentry_http_send_func_t send_func); + void *client, sentry_http_client_send_func_t send_func); void sentry__http_transport_set_free_client( sentry_transport_t *transport, void (*free_client)(void *)); diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index ae0c78af8f..1ab8f362d1 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -388,19 +388,11 @@ header_callback(char *buffer, size_t size, size_t nitems, void *userdata) char *sep = strchr(header, ':'); if (sep) { *sep = '\0'; - sentry__string_ascii_lower(header); - sentry_slice_t value + sentry_slice_t trimmed_value = sentry__slice_trim(sentry__slice_from_str(sep + 1)); - if (sentry__string_eq(header, "retry-after")) { - sentry_free(info->retry_after); - info->retry_after = sentry__slice_to_owned(value); - } else if (sentry__string_eq(header, "x-sentry-rate-limits")) { - sentry_free(info->x_sentry_rate_limits); - info->x_sentry_rate_limits = sentry__slice_to_owned(value); - } else if (sentry__string_eq(header, "location")) { - sentry_free(info->location); - info->location = sentry__slice_to_owned(value); - } + char *value = sentry__slice_to_owned(trimmed_value); + sentry_http_response_set_header(info, header, value); + sentry_free(value); } sentry_free(header); @@ -427,7 +419,7 @@ file_read_callback(char *buffer, size_t size, size_t nitems, void *userdata) return CURL_READFUNC_ABORT; } -static bool +static int curl_send_task(void *_client, sentry_prepared_http_request_t *req, sentry_http_response_t *resp) { diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index 2854d6bc7c..a42ffa95c6 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -191,7 +191,7 @@ query_header(HINTERNET request, const wchar_t *header) return NULL; } -static bool +static int winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, sentry_http_response_t *resp) { @@ -376,13 +376,26 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, WINHTTP_NO_HEADER_INDEX); resp->status_code = (int)status_code; - resp->x_sentry_rate_limits + char *rate_limits = query_header(client->request, L"x-sentry-rate-limits"); - if (!resp->x_sentry_rate_limits) { - resp->retry_after = query_header(client->request, L"retry-after"); + if (rate_limits) { + sentry_http_response_set_header( + resp, "x-sentry-rate-limits", rate_limits); + sentry_free(rate_limits); + } else { + char *retry_after = query_header(client->request, L"retry-after"); + if (retry_after) { + sentry_http_response_set_header( + resp, "retry-after", retry_after); + sentry_free(retry_after); + } } - resp->location = query_header(client->request, L"location"); + char *location = query_header(client->request, L"location"); + if (location) { + sentry_http_response_set_header(resp, "location", location); + sentry_free(location); + } } uint64_t now = sentry__monotonic_time(); diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt index b3689ba7d7..2313bb880a 100644 --- a/tests/unit/CMakeLists.txt +++ b/tests/unit/CMakeLists.txt @@ -33,6 +33,7 @@ add_executable(sentry_test_unit test_failures.c test_feedback.c test_fuzzfailures.c + test_http_transport.c test_info.c test_logger.c test_logs.c diff --git a/tests/unit/test_http_transport.c b/tests/unit/test_http_transport.c new file mode 100644 index 0000000000..40cba27408 --- /dev/null +++ b/tests/unit/test_http_transport.c @@ -0,0 +1,288 @@ +#include "sentry_alloc.h" +#include "sentry_path.h" +#include "sentry_string.h" +#include "sentry_testsupport.h" +#include "transports/sentry_http_transport.h" + +#include + +SENTRY_TEST(http_response_set_header_sets_known_headers) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + sentry_http_response_set_header(&resp, "retry-after", "60"); + sentry_http_response_set_header( + &resp, "x-sentry-rate-limits", "60:error:key"); + sentry_http_response_set_header(&resp, "location", "/uploads/1"); + + TEST_CHECK_STRING_EQUAL(resp.retry_after, "60"); + TEST_CHECK_STRING_EQUAL(resp.x_sentry_rate_limits, "60:error:key"); + TEST_CHECK_STRING_EQUAL(resp.location, "/uploads/1"); + + sentry_free(resp.retry_after); + sentry_free(resp.x_sentry_rate_limits); + sentry_free(resp.location); +} + +SENTRY_TEST(http_response_set_header_is_case_insensitive) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + sentry_http_response_set_header(&resp, "Retry-After", "30"); + sentry_http_response_set_header( + &resp, "X-SENTRY-RATE-LIMITS", "30:error:key"); + sentry_http_response_set_header(&resp, "Location", "/uploads/2"); + + TEST_CHECK_STRING_EQUAL(resp.retry_after, "30"); + TEST_CHECK_STRING_EQUAL(resp.x_sentry_rate_limits, "30:error:key"); + TEST_CHECK_STRING_EQUAL(resp.location, "/uploads/2"); + + sentry_free(resp.retry_after); + sentry_free(resp.x_sentry_rate_limits); + sentry_free(resp.location); +} + +SENTRY_TEST(http_response_set_header_ignores_unknown_headers) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + sentry_http_response_set_header(&resp, "content-type", "text/plain"); + sentry_http_response_set_header(&resp, "x-request-id", "abc123"); + + TEST_CHECK(resp.retry_after == NULL); + TEST_CHECK(resp.x_sentry_rate_limits == NULL); + TEST_CHECK(resp.location == NULL); +} + +SENTRY_TEST(http_response_set_header_overwrites_previous_value) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + sentry_http_response_set_header(&resp, "retry-after", "10"); + sentry_http_response_set_header(&resp, "retry-after", "20"); + + TEST_CHECK_STRING_EQUAL(resp.retry_after, "20"); + + sentry_free(resp.retry_after); +} + +SENTRY_TEST(http_response_set_header_null_safety) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + // Must not crash on a NULL response or a NULL key. + sentry_http_response_set_header(NULL, "retry-after", "10"); + sentry_http_response_set_header(&resp, NULL, "10"); + + TEST_CHECK(resp.retry_after == NULL); +} + +SENTRY_TEST(http_response_set_status_code) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + sentry_http_response_set_status_code(&resp, 429); + TEST_CHECK_INT_EQUAL(resp.status_code, 429); + + // Must not crash on a NULL response. + sentry_http_response_set_status_code(NULL, 500); +} + +SENTRY_TEST(http_response_set_shutdown) +{ + sentry_http_response_t resp; + memset(&resp, 0, sizeof(resp)); + + TEST_CHECK(!resp.shutdown); + sentry_http_response_set_shutdown(&resp, 1); + TEST_CHECK(resp.shutdown); + sentry_http_response_set_shutdown(&resp, 0); + TEST_CHECK(!resp.shutdown); + + // Must not crash on a NULL response. + sentry_http_response_set_shutdown(NULL, 1); +} + +static sentry_prepared_http_request_t * +make_test_request(void) +{ + sentry_prepared_http_request_t *req + = SENTRY_MAKE(sentry_prepared_http_request_t); + memset(req, 0, sizeof(*req)); + req->method = "POST"; + req->url = sentry__string_clone("https://sentry.invalid/api/42/envelope/"); + req->headers_len = 2; + req->headers = sentry_malloc( + sizeof(sentry_prepared_http_header_t) * req->headers_len); + req->headers[0].key = "content-type"; + req->headers[0].value + = sentry__string_clone("application/x-sentry-envelope"); + req->headers[1].key = "content-length"; + req->headers[1].value = sentry__string_clone("11"); + req->body = sentry__string_clone("hello world"); + req->body_len = 11; + req->body_owned = true; + return req; +} + +SENTRY_TEST(http_request_accessors_in_memory_body) +{ + sentry_prepared_http_request_t *req = make_test_request(); + + TEST_CHECK_STRING_EQUAL(sentry_http_request_get_method(req), "POST"); + TEST_CHECK_STRING_EQUAL(sentry_http_request_get_url(req), + "https://sentry.invalid/api/42/envelope/"); + TEST_CHECK_INT_EQUAL((int)sentry_http_request_get_header_count(req), 2); + + const char *key = NULL; + const char *value = NULL; + TEST_CHECK(sentry_http_request_get_header(req, 0, &key, &value) == 1); + TEST_CHECK_STRING_EQUAL(key, "content-type"); + TEST_CHECK_STRING_EQUAL(value, "application/x-sentry-envelope"); + + TEST_CHECK(sentry_http_request_get_header(req, 1, &key, &value) == 1); + TEST_CHECK_STRING_EQUAL(key, "content-length"); + TEST_CHECK_STRING_EQUAL(value, "11"); + + // Out of range must fail rather than reading past the array. + TEST_CHECK(sentry_http_request_get_header(req, 2, &key, &value) == 0); + + size_t len = 0; + const char *body = sentry_http_request_get_body(req, &len); + TEST_CHECK(body != NULL); + TEST_CHECK_INT_EQUAL((int)len, 11); + TEST_CHECK(memcmp(body, "hello world", 11) == 0); + + // A request with an in-memory body has no file-backed body. + size_t file_len = 123; + TEST_CHECK(sentry_http_request_get_body_file_path(req, &file_len) == NULL); + TEST_CHECK_INT_EQUAL((int)file_len, 0); + + sentry__prepared_http_request_free(req); +} + +SENTRY_TEST(http_request_accessors_file_backed_body) +{ + sentry_prepared_http_request_t *req + = SENTRY_MAKE(sentry_prepared_http_request_t); + memset(req, 0, sizeof(*req)); + req->method = "PATCH"; + req->url = sentry__string_clone("https://sentry.invalid/upload/abc"); + req->headers_len = 0; + req->body_path = sentry__path_from_str("/tmp/does-not-need-to-exist"); + req->body_len = 100 * 1024 * 1024; + + size_t len = 0; + TEST_CHECK(sentry_http_request_get_body(req, &len) == NULL); + TEST_CHECK_INT_EQUAL((int)len, 0); + + len = 0; + const char *path = sentry_http_request_get_body_file_path(req, &len); + TEST_CHECK_STRING_EQUAL(path, "/tmp/does-not-need-to-exist"); + TEST_CHECK_INT_EQUAL((int)len, 100 * 1024 * 1024); + + sentry__prepared_http_request_free(req); +} + +SENTRY_TEST(http_request_accessors_null_safety) +{ + TEST_CHECK(sentry_http_request_get_method(NULL) == NULL); + TEST_CHECK(sentry_http_request_get_url(NULL) == NULL); + TEST_CHECK_INT_EQUAL((int)sentry_http_request_get_header_count(NULL), 0); + TEST_CHECK(sentry_http_request_get_header(NULL, 0, NULL, NULL) == 0); + size_t len = 0; + TEST_CHECK(sentry_http_request_get_body(NULL, &len) == NULL); + TEST_CHECK(sentry_http_request_get_body_file_path(NULL, &len) == NULL); +} + +static void * +counting_factory(void *factory_data) +{ + int *call_count = factory_data; + (*call_count)++; + return sentry_malloc(sizeof(int)); +} + +static void * +failing_factory(void *factory_data) +{ + (void)factory_data; + return NULL; +} + +static int +noop_send_func( + void *client, sentry_http_request_t *req, sentry_http_response_t *resp) +{ + (void)client; + (void)req; + (void)resp; + return 1; +} + +static bool g_client_freed = false; + +static void +counting_client_free(void *client) +{ + g_client_freed = true; + sentry_free(client); +} + +SENTRY_TEST(http_transport_new_creates_transport_via_factory) +{ + int call_count = 0; + g_client_freed = false; + + sentry_transport_t *transport = sentry_http_transport_new( + counting_factory, &call_count, noop_send_func, counting_client_free); + + TEST_CHECK(!!transport); + TEST_CHECK_INT_EQUAL(call_count, 1); + TEST_CHECK(!g_client_freed); + + sentry_transport_free(transport); + TEST_CHECK(g_client_freed); +} + +SENTRY_TEST(http_transport_new_null_factory_fails) +{ + int call_count = 0; + sentry_transport_t *transport = sentry_http_transport_new( + NULL, &call_count, noop_send_func, counting_client_free); + TEST_CHECK(!transport); +} + +SENTRY_TEST(http_transport_new_null_send_func_fails) +{ + int call_count = 0; + sentry_transport_t *transport = sentry_http_transport_new( + counting_factory, &call_count, NULL, counting_client_free); + TEST_CHECK(!transport); + // The factory must not have been called if `send_func` was already + // known to be invalid. + TEST_CHECK_INT_EQUAL(call_count, 0); +} + +SENTRY_TEST(http_transport_new_factory_failure_returns_null) +{ + sentry_transport_t *transport = sentry_http_transport_new( + failing_factory, NULL, noop_send_func, counting_client_free); + TEST_CHECK(!transport); +} + +SENTRY_TEST(http_transport_new_null_client_free_func_is_safe) +{ + int call_count = 0; + sentry_transport_t *transport = sentry_http_transport_new( + counting_factory, &call_count, noop_send_func, NULL); + TEST_CHECK(!!transport); + // Must not crash freeing a transport with no client-free hook. + sentry_transport_free(transport); +} diff --git a/tests/unit/test_retry.c b/tests/unit/test_retry.c index f2e564faa4..98c21e3d5f 100644 --- a/tests/unit/test_retry.c +++ b/tests/unit/test_retry.c @@ -85,7 +85,7 @@ test_send_cb(sentry_envelope_t *envelope, void *_ctx) return ctx->status_code; } -static bool +static int test_http_send_fails(void *client, sentry_prepared_http_request_t *req, sentry_http_response_t *resp) { diff --git a/tests/unit/test_tus.c b/tests/unit/test_tus.c index 0ed3397a39..27b89cbde0 100644 --- a/tests/unit/test_tus.c +++ b/tests/unit/test_tus.c @@ -132,7 +132,7 @@ SENTRY_TEST(tus_request_preparation) sentry__dsn_decref(dsn); } -static bool +static int tus_mock_send(void *client, sentry_prepared_http_request_t *req, sentry_http_response_t *resp) { @@ -189,7 +189,7 @@ typedef struct { int envelope_count; } tus_create_failure_state_t; -static bool +static int tus_create_failure_send(void *client, sentry_prepared_http_request_t *req, sentry_http_response_t *resp) { @@ -261,7 +261,7 @@ typedef struct { static const char *TUS_RELATIVE_LOCATION = "/api/42/upload/019db3e0/?length=104857600&signature=test"; -static bool +static int tus_capture_send(void *client, sentry_prepared_http_request_t *req, sentry_http_response_t *resp) { diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index 12d5dd5b9e..fa8323bbf0 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -182,6 +182,21 @@ XX(find_mem_range) XX(formatted_log_messages) XX(fuzz_json) XX(getenv_double) +XX(http_request_accessors_file_backed_body) +XX(http_request_accessors_in_memory_body) +XX(http_request_accessors_null_safety) +XX(http_response_set_header_ignores_unknown_headers) +XX(http_response_set_header_is_case_insensitive) +XX(http_response_set_header_null_safety) +XX(http_response_set_header_overwrites_previous_value) +XX(http_response_set_header_sets_known_headers) +XX(http_response_set_shutdown) +XX(http_response_set_status_code) +XX(http_transport_new_creates_transport_via_factory) +XX(http_transport_new_factory_failure_returns_null) +XX(http_transport_new_null_client_free_func_is_safe) +XX(http_transport_new_null_factory_fails) +XX(http_transport_new_null_send_func_fails) XX(init_failure) XX(installation_id) XX(internal_uuid_api) From 1a23cebbeb836ad14f7db3eac595450ddd548a69 Mon Sep 17 00:00:00 2001 From: Huzaifa Abdul Rehman Date: Thu, 13 Aug 2026 03:27:22 +0500 Subject: [PATCH 2/7] docs: fill in PR number for HTTP transport client interface changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9e57445d8..183aa3d4d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,7 @@ - Forward `enable_logs` option through `NdkOptions` so the Android SDK can enable native structured logging. ([#1971](https://github.com/getsentry/sentry-native/pull/1971)) - Report enabled Qt and WER integrations in event SDK metadata (`sdk.integrations`) alongside the configured crash backend. ([#1969](https://github.com/ getsentry/sentry-native/pull/1969)) -- Add a public custom HTTP transport client interface (`sentry_http_transport_new`) so applications can plug in their own HTTP client (e.g. platform-native ones) while sentry-native continues to own request queueing, retry/backoff, offline caching, rate-limiting, and client reports. The built-in curl and WinHTTP transports are now implemented against this same interface. ([#XXXX](https://github.com/getsentry/sentry-native/pull/XXXX)) +- Add a public custom HTTP transport client interface (`sentry_http_transport_new`) so applications can plug in their own HTTP client (e.g. platform-native ones) while sentry-native continues to own request queueing, retry/backoff, offline caching, rate-limiting, and client reports. The built-in curl and WinHTTP transports are now implemented against this same interface. ([#1987](https://github.com/getsentry/sentry-native/pull/1987)) - Add `sentry_scope_set_span` and `sentry_scope_set_transaction_object` to bind a span or transaction to a given scope. ([#1981](https://github.com/getsentry/sentry-native/pull/1981)) **Fixes**: From f82bd52e2d7402c12e0a3ba2cc89f602e87f66fb Mon Sep 17 00:00:00 2001 From: Huzaifa Abdul Rehman Date: Thu, 13 Aug 2026 14:27:34 +0500 Subject: [PATCH 3/7] fix(native): address review feedback on HTTP transport client interface - Document that both body accessors return NULL for bodyless requests (e.g. the TUS creation POST), and cover that case in the accessor tests - Add a sentry_http_client_t typedef to distinguish client pointers from factory/user-data pointers in the public API - Rename the sentry_prepared_http_request_s tag to sentry_http_request_s so the public opaque type no longer leaks the old internal name - Document that the client shutdown hook runs on the sentry_close thread, concurrently with an in-flight send_func call on the transport thread - Fix a test that leaked its client object by allocating one instead of using a non-owning fake pointer when exercising the null-free-func path --- include/sentry.h | 53 +++++++++++++++++++------- src/transports/sentry_http_transport.c | 11 +++--- src/transports/sentry_http_transport.h | 6 ++- tests/unit/test_http_transport.c | 34 ++++++++++++++++- tests/unit/tests.inc | 1 + 5 files changed, 84 insertions(+), 21 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index 2a6fe33f6a..b36969ea0e 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -982,8 +982,8 @@ SENTRY_API void sentry_transport_free(sentry_transport_t *transport); * `sentry_http_client_send_func_t` call it was passed to; the client must * not retain it afterwards. */ -struct sentry_prepared_http_request_s; -typedef struct sentry_prepared_http_request_s sentry_http_request_t; +struct sentry_http_request_s; +typedef struct sentry_http_request_s sentry_http_request_t; /** * Returns the HTTP method of `req`, such as `"POST"` or `"PATCH"`. @@ -1018,9 +1018,11 @@ SENTRY_EXPERIMENTAL_API int sentry_http_request_get_header( /** * Returns the in-memory request body of `req` and writes its length to * `*len_out`. Returns `NULL` if `req` has no in-memory body, which happens - * for large-attachment uploads that stream a file instead -- see - * `sentry_http_request_get_body_file_path`. Exactly one of the two ever - * returns non-`NULL` for a given request. + * both for large-attachment uploads that stream a file instead -- see + * `sentry_http_request_get_body_file_path` -- and for bodyless requests such + * as the initial TUS creation `POST`, for which both accessors return + * `NULL`. At most one of the two ever returns non-`NULL` for a given + * request. */ SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body( const sentry_http_request_t *req, size_t *len_out); @@ -1028,7 +1030,8 @@ SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body( /** * Returns the path to a file that should be streamed as the request body of * `req`, and writes its size to `*len_out`. Returns `NULL` unless `req` - * requires a file-backed body -- see `sentry_http_request_get_body`. + * requires a file-backed body -- see `sentry_http_request_get_body`, which + * also covers requests with no body at all. */ SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body_file_path( const sentry_http_request_t *req, size_t *len_out); @@ -1069,6 +1072,15 @@ SENTRY_EXPERIMENTAL_API void sentry_http_response_set_header( SENTRY_EXPERIMENTAL_API void sentry_http_response_set_shutdown( sentry_http_response_t *resp, int is_shutdown); +/** + * Opaque handle to a custom HTTP client instance, as created by a + * `sentry_http_client_factory_func_t` and passed back into + * `sentry_http_client_send_func_t` and the client start/shutdown hooks. + * Purely cosmetic -- it exists to distinguish client pointers from the + * unrelated `void *factory_data` / user-data pointers used alongside them. + */ +typedef void sentry_http_client_t; + /** * Creates a new HTTP client instance for a transport created with * `sentry_http_transport_new`. Returns an opaque client pointer, or `NULL` @@ -1082,7 +1094,8 @@ SENTRY_EXPERIMENTAL_API void sentry_http_response_set_shutdown( * instead, each time from that worker thread; implementations should not * assume they are called from any particular thread, or exactly once. */ -typedef void *(*sentry_http_client_factory_func_t)(void *factory_data); +typedef sentry_http_client_t *(*sentry_http_client_factory_func_t)( + void *factory_data); /** * Executes a single HTTP request using `client` (as returned by the @@ -1100,10 +1113,16 @@ typedef void *(*sentry_http_client_factory_func_t)(void *factory_data); * for the same client until the previous call returns. A future * multi-threaded transport may drive different client instances from * different threads concurrently, but will still only ever call into one - * client instance from one thread at a time. + * client instance from one thread at a time for `send_func` calls + * specifically. + * + * The one exception is `sentry_http_transport_set_client_shutdown_func`'s + * hook, which sentry-native may call from a different thread while a + * `send_func` call for the same client is still in flight -- see its + * documentation. */ -typedef int (*sentry_http_client_send_func_t)( - void *client, sentry_http_request_t *req, sentry_http_response_t *resp); +typedef int (*sentry_http_client_send_func_t)(sentry_http_client_t *client, + sentry_http_request_t *req, sentry_http_response_t *resp); /** * Creates a new HTTP transport that executes requests through a custom HTTP @@ -1132,7 +1151,7 @@ typedef int (*sentry_http_client_send_func_t)( SENTRY_EXPERIMENTAL_API sentry_transport_t *sentry_http_transport_new( sentry_http_client_factory_func_t factory, void *factory_data, sentry_http_client_send_func_t send_func, - void (*client_free_func)(void *client)); + void (*client_free_func)(sentry_http_client_t *client)); /** * Sets the hook that initializes the client once `sentry_options_t` is @@ -1143,15 +1162,23 @@ SENTRY_EXPERIMENTAL_API sentry_transport_t *sentry_http_transport_new( */ SENTRY_EXPERIMENTAL_API void sentry_http_transport_set_client_start_func( sentry_transport_t *transport, - int (*start_func)(void *client, const sentry_options_t *options)); + int (*start_func)( + sentry_http_client_t *client, const sentry_options_t *options)); /** * Sets the hook that tells the client the transport is shutting down, e.g. * to unblock or cancel an in-flight request. Called at most once, when the * transport is shut down. + * + * This hook runs on the thread that calls `sentry_close`, which is not the + * background thread that runs `send_func` -- if a `send_func` call is still + * in flight when shutdown starts, this hook may be invoked concurrently + * with it, on a different thread, for the same client. The client + * implementation must be able to tolerate that. */ SENTRY_EXPERIMENTAL_API void sentry_http_transport_set_client_shutdown_func( - sentry_transport_t *transport, void (*shutdown_func)(void *client)); + sentry_transport_t *transport, + void (*shutdown_func)(sentry_http_client_t *client)); /** * Create a new function transport. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index a6f71e76ee..ed51b85620 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -1074,12 +1074,12 @@ sentry__http_transport_set_shutdown_client( sentry_transport_t * sentry_http_transport_new(sentry_http_client_factory_func_t factory, void *factory_data, sentry_http_client_send_func_t send_func, - void (*client_free_func)(void *client)) + void (*client_free_func)(sentry_http_client_t *client)) { if (!factory || !send_func) { return NULL; } - void *client = factory(factory_data); + sentry_http_client_t *client = factory(factory_data); if (!client) { return NULL; } @@ -1097,14 +1097,15 @@ sentry_http_transport_new(sentry_http_client_factory_func_t factory, void sentry_http_transport_set_client_start_func(sentry_transport_t *transport, - int (*start_func)(void *client, const sentry_options_t *options)) + int (*start_func)( + sentry_http_client_t *client, const sentry_options_t *options)) { sentry__http_transport_set_start_client(transport, start_func); } void -sentry_http_transport_set_client_shutdown_func( - sentry_transport_t *transport, void (*shutdown_func)(void *client)) +sentry_http_transport_set_client_shutdown_func(sentry_transport_t *transport, + void (*shutdown_func)(sentry_http_client_t *client)) { sentry__http_transport_set_shutdown_client(transport, shutdown_func); } diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index ce906d2f17..3a8da2c16c 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -12,7 +12,11 @@ typedef struct sentry_prepared_http_header_s { char *value; } sentry_prepared_http_header_t; -typedef struct sentry_prepared_http_request_s { +// The public `sentry_http_request_t` (see sentry.h) is a forward-declared +// opaque handle for this same tag; this is the only place its layout is +// defined. Internal code and the public accessors in sentry_http_transport.c +// both operate on this concrete struct via that shared tag. +typedef struct sentry_http_request_s { const char *method; char *url; sentry_prepared_http_header_t *headers; diff --git a/tests/unit/test_http_transport.c b/tests/unit/test_http_transport.c index 40cba27408..707705a2a3 100644 --- a/tests/unit/test_http_transport.c +++ b/tests/unit/test_http_transport.c @@ -190,6 +190,28 @@ SENTRY_TEST(http_request_accessors_file_backed_body) sentry__prepared_http_request_free(req); } +SENTRY_TEST(http_request_accessors_bodyless_request) +{ + // Mirrors the TUS creation `POST`, which has neither an in-memory nor a + // file-backed body. + sentry_prepared_http_request_t *req + = SENTRY_MAKE(sentry_prepared_http_request_t); + memset(req, 0, sizeof(*req)); + req->method = "POST"; + req->url = sentry__string_clone("https://sentry.invalid/upload/"); + req->headers_len = 0; + + size_t len = 123; + TEST_CHECK(sentry_http_request_get_body(req, &len) == NULL); + TEST_CHECK_INT_EQUAL((int)len, 0); + + len = 123; + TEST_CHECK(sentry_http_request_get_body_file_path(req, &len) == NULL); + TEST_CHECK_INT_EQUAL((int)len, 0); + + sentry__prepared_http_request_free(req); +} + SENTRY_TEST(http_request_accessors_null_safety) { TEST_CHECK(sentry_http_request_get_method(NULL) == NULL); @@ -216,6 +238,15 @@ failing_factory(void *factory_data) return NULL; } +// Returns a non-`NULL` client without heap-allocating, for tests that +// exercise a code path with no client-free hook and must not leak. +static void * +non_owning_factory(void *factory_data) +{ + (void)factory_data; + return (void *)1; +} + static int noop_send_func( void *client, sentry_http_request_t *req, sentry_http_response_t *resp) @@ -279,9 +310,8 @@ SENTRY_TEST(http_transport_new_factory_failure_returns_null) SENTRY_TEST(http_transport_new_null_client_free_func_is_safe) { - int call_count = 0; sentry_transport_t *transport = sentry_http_transport_new( - counting_factory, &call_count, noop_send_func, NULL); + non_owning_factory, NULL, noop_send_func, NULL); TEST_CHECK(!!transport); // Must not crash freeing a transport with no client-free hook. sentry_transport_free(transport); diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index fa8323bbf0..b77b355fc6 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -182,6 +182,7 @@ XX(find_mem_range) XX(formatted_log_messages) XX(fuzz_json) XX(getenv_double) +XX(http_request_accessors_bodyless_request) XX(http_request_accessors_file_backed_body) XX(http_request_accessors_in_memory_body) XX(http_request_accessors_null_safety) From 36a3eb8704431bf862f71a5ece7a7cbbc14b2cba Mon Sep 17 00:00:00 2001 From: Huzaifa Abdul Rehman Date: Thu, 13 Aug 2026 16:25:16 +0500 Subject: [PATCH 4/7] ref(native): drop redundant opaque-handle comments in http transport header The forward-declared-tag pattern is used throughout the codebase (envelope, options, transport, scope, ...), so spelling it out here adds no information. --- src/transports/sentry_http_transport.h | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index 3a8da2c16c..2fcc671cd7 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -12,10 +12,6 @@ typedef struct sentry_prepared_http_header_s { char *value; } sentry_prepared_http_header_t; -// The public `sentry_http_request_t` (see sentry.h) is a forward-declared -// opaque handle for this same tag; this is the only place its layout is -// defined. Internal code and the public accessors in sentry_http_transport.c -// both operate on this concrete struct via that shared tag. typedef struct sentry_http_request_s { const char *method; char *url; @@ -39,10 +35,6 @@ sentry_prepared_http_request_t *sentry__prepare_tus_upload_request( void sentry__prepared_http_request_free(sentry_prepared_http_request_t *req); -// The public `sentry_http_response_t` (see sentry.h) is a forward-declared -// opaque handle for this same tag; this is the only place its layout is -// defined. Internal code and the public accessors in sentry_http_transport.c -// both operate on this concrete struct via that shared tag. struct sentry_http_response_s { int status_code; char *retry_after; From 108e96080a105345ae4392bd7ba1b8fa7733b794 Mon Sep 17 00:00:00 2001 From: Huzaifa Abdul Rehman Date: Thu, 13 Aug 2026 16:26:29 +0500 Subject: [PATCH 5/7] docs: move HTTP transport changelog entry back under Unreleased The 0.16.3 release cut the Unreleased heading while this branch was open, so the rebase left this entry inside the released section. --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 183aa3d4d8..f874c0855e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**Features**: + +- Add a public custom HTTP transport client interface (`sentry_http_transport_new`) so applications can plug in their own HTTP client (e.g. platform-native ones) while sentry-native continues to own request queueing, retry/backoff, offline caching, rate-limiting, and client reports. The built-in curl and WinHTTP transports are now implemented against this same interface. ([#1987](https://github.com/getsentry/sentry-native/pull/1987)) + **Fixes**: - Linux/ARM32: prevent recursive crashes when libunwind receives an unmapped initial instruction pointer during crash handling. ([#1977](https://github.com/getsentry/sentry-native/pull/1977)) @@ -13,7 +17,6 @@ - Forward `enable_logs` option through `NdkOptions` so the Android SDK can enable native structured logging. ([#1971](https://github.com/getsentry/sentry-native/pull/1971)) - Report enabled Qt and WER integrations in event SDK metadata (`sdk.integrations`) alongside the configured crash backend. ([#1969](https://github.com/ getsentry/sentry-native/pull/1969)) -- Add a public custom HTTP transport client interface (`sentry_http_transport_new`) so applications can plug in their own HTTP client (e.g. platform-native ones) while sentry-native continues to own request queueing, retry/backoff, offline caching, rate-limiting, and client reports. The built-in curl and WinHTTP transports are now implemented against this same interface. ([#1987](https://github.com/getsentry/sentry-native/pull/1987)) - Add `sentry_scope_set_span` and `sentry_scope_set_transaction_object` to bind a span or transaction to a given scope. ([#1981](https://github.com/getsentry/sentry-native/pull/1981)) **Fixes**: From f7258e01ed049c5fbad511d20b472176dba359ce Mon Sep 17 00:00:00 2001 From: Huzaifa Abdul Rehman Date: Thu, 13 Aug 2026 17:50:57 +0500 Subject: [PATCH 6/7] ref(native): infer shutdown failures in the transport, not the client Custom HTTP clients had to call `sentry_http_response_set_shutdown` to say that a request failed only because the transport was shutting down. That put the burden on every client implementation, and required them to be thread safe even in the current single-instance case, since the shutdown hook runs on the `sentry_close` thread. The transport already knows when it is shutting down, so it can classify the failure itself. Adds an atomic `shutting_down` flag set at the start of `http_transport_shutdown`, before the client is ever asked to stop, and reads it in `http_send_request` instead of the client-reported value. Removes `sentry_http_response_set_shutdown` from the public API. --- include/sentry.h | 16 +++----------- src/transports/sentry_http_transport.c | 22 ++++++++++--------- src/transports/sentry_http_transport.h | 1 - src/transports/sentry_http_transport_curl.c | 1 - .../sentry_http_transport_winhttp.c | 3 --- tests/unit/test_http_transport.c | 15 ------------- tests/unit/tests.inc | 1 - 7 files changed, 15 insertions(+), 44 deletions(-) diff --git a/include/sentry.h b/include/sentry.h index b36969ea0e..f5f51e50ca 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1062,16 +1062,6 @@ SENTRY_EXPERIMENTAL_API void sentry_http_response_set_status_code( SENTRY_EXPERIMENTAL_API void sentry_http_response_set_header( sentry_http_response_t *resp, const char *key, const char *value); -/** - * Marks `resp` as having failed because the transport is shutting down, - * rather than because of a regular request error. Only meaningful when the - * client's `sentry_http_client_send_func_t` call is about to return `0` - * (failure); sentry-native uses this to skip retry/caching for requests - * that failed only because shutdown interrupted them. - */ -SENTRY_EXPERIMENTAL_API void sentry_http_response_set_shutdown( - sentry_http_response_t *resp, int is_shutdown); - /** * Opaque handle to a custom HTTP client instance, as created by a * `sentry_http_client_factory_func_t` and passed back into @@ -1104,9 +1094,9 @@ typedef sentry_http_client_t *(*sentry_http_client_factory_func_t)( * `sentry_http_response_set_header`. * * Returns `1` on success. Returning `0` marks the request as failed for - * sentry-native's retry/caching logic; call - * `sentry_http_response_set_shutdown` first if the failure is because the - * transport is shutting down rather than a request error. + * sentry-native's retry/caching logic. A request that fails only because + * shutdown interrupted it does not need any special handling: sentry-native + * already knows it is shutting down and classifies the failure accordingly. * * sentry-native calls this from a single background thread, once per * request, in the order requests were queued, and never invokes it again diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index ed51b85620..802f49ab1e 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -37,6 +37,10 @@ typedef struct { int (*start_client)(void *, const sentry_options_t *); sentry_http_client_send_func_t send_func; void (*shutdown_client)(void *client); + // Set before the client is asked to stop, so a `send_func` failure that + // only happened because shutdown interrupted it can be told apart from a + // regular request error without the client having to report it back. + volatile long shutting_down; sentry_retry_t *retry; sentry_cache_keep_t cache_keep; sentry_run_t *run; @@ -366,15 +370,6 @@ sentry_http_response_set_status_code( resp->status_code = status_code; } -void -sentry_http_response_set_shutdown(sentry_http_response_t *resp, int is_shutdown) -{ - if (!resp) { - return; - } - resp->shutdown = !!is_shutdown; -} - const char * sentry_http_request_get_method(const sentry_http_request_t *req) { @@ -452,7 +447,9 @@ http_send_request(http_transport_state_t *state, { memset(resp, 0, sizeof(*resp)); if (!state->send_func(state->client, req, resp)) { - int result = resp->shutdown ? RESULT_SHUTDOWN : RESULT_ERROR; + int result = sentry__atomic_fetch(&state->shutting_down) + ? RESULT_SHUTDOWN + : RESULT_ERROR; http_response_cleanup(resp); return result; } @@ -947,6 +944,11 @@ 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); + // Must be set before the client is asked to stop (which only happens from + // `http_transport_shutdown_timeout` below), so that any `send_func` failure + // caused by that interruption is already seen as a shutdown failure. + sentry__atomic_store(&state->shutting_down, 1); + sentry__retry_shutdown(state->retry); int rv = sentry__bgworker_shutdown_cb( diff --git a/src/transports/sentry_http_transport.h b/src/transports/sentry_http_transport.h index 2fcc671cd7..135a7a584f 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -40,7 +40,6 @@ struct sentry_http_response_s { char *retry_after; char *x_sentry_rate_limits; char *location; - bool shutdown; }; /** diff --git a/src/transports/sentry_http_transport_curl.c b/src/transports/sentry_http_transport_curl.c index 1ab8f362d1..83378f5494 100644 --- a/src/transports/sentry_http_transport_curl.c +++ b/src/transports/sentry_http_transport_curl.c @@ -522,7 +522,6 @@ curl_send_task(void *_client, sentry_prepared_http_request_t *req, g_curl.easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); resp->status_code = (int)response_code; } else { - resp->shutdown = sentry__atomic_fetch(&client->shutdown) != 0; size_t len = strlen(error_buf); if (len) { if (error_buf[len - 1] == '\n') { diff --git a/src/transports/sentry_http_transport_winhttp.c b/src/transports/sentry_http_transport_winhttp.c index a42ffa95c6..5f44a3400d 100644 --- a/src/transports/sentry_http_transport_winhttp.c +++ b/src/transports/sentry_http_transport_winhttp.c @@ -402,9 +402,6 @@ winhttp_send_task(void *_client, sentry_prepared_http_request_t *req, SENTRY_DEBUGF("request handled in %llums", now - started); exit:; - if (!result && sentry__atomic_fetch(&client->shutdown)) { - resp->shutdown = true; - } HINTERNET request = InterlockedExchangePointer(&client->request, NULL); if (request) { WinHttpCloseHandle(request); diff --git a/tests/unit/test_http_transport.c b/tests/unit/test_http_transport.c index 707705a2a3..c61baa0066 100644 --- a/tests/unit/test_http_transport.c +++ b/tests/unit/test_http_transport.c @@ -94,21 +94,6 @@ SENTRY_TEST(http_response_set_status_code) sentry_http_response_set_status_code(NULL, 500); } -SENTRY_TEST(http_response_set_shutdown) -{ - sentry_http_response_t resp; - memset(&resp, 0, sizeof(resp)); - - TEST_CHECK(!resp.shutdown); - sentry_http_response_set_shutdown(&resp, 1); - TEST_CHECK(resp.shutdown); - sentry_http_response_set_shutdown(&resp, 0); - TEST_CHECK(!resp.shutdown); - - // Must not crash on a NULL response. - sentry_http_response_set_shutdown(NULL, 1); -} - static sentry_prepared_http_request_t * make_test_request(void) { diff --git a/tests/unit/tests.inc b/tests/unit/tests.inc index b77b355fc6..cca608f969 100644 --- a/tests/unit/tests.inc +++ b/tests/unit/tests.inc @@ -191,7 +191,6 @@ XX(http_response_set_header_is_case_insensitive) XX(http_response_set_header_null_safety) XX(http_response_set_header_overwrites_previous_value) XX(http_response_set_header_sets_known_headers) -XX(http_response_set_shutdown) XX(http_response_set_status_code) XX(http_transport_new_creates_transport_via_factory) XX(http_transport_new_factory_failure_returns_null) From cc413a9ec891f09d6e104a7546c66a6bd04b90dd Mon Sep 17 00:00:00 2001 From: Huzaifa Abdul Rehman Date: Thu, 13 Aug 2026 18:29:08 +0500 Subject: [PATCH 7/7] feat(native): add wide-char accessor for the request body file path On Windows `sentry_path_t` keeps the path both as canonical UTF-8 (`path`) and as wide chars (`path_w`), and the SDK's rule is to use the wide Win32 APIs when leaving the SDK boundary, since the narrow ones interpret `char *` according to the ANSI code page rather than UTF-8. `sentry_http_request_get_body_file_path` only exposed the narrow variant, so a custom client passing it to `fopen`/`CreateFileA` would fail to open any path containing non-ASCII characters. Both built-in transports avoid this by reading `path_w` directly (`CreateFileW` in WinHTTP, `_wfopen` in curl), which a custom client had no way to do. Adds `sentry_http_request_get_body_file_pathw`, following the same `w` suffix convention already used by `sentry_options_set_database_pathw` and friends, and documents the encoding on the narrow accessor. --- include/sentry.h | 14 ++++++++++++++ src/transports/sentry_http_transport.c | 18 ++++++++++++++++++ tests/unit/test_http_transport.c | 17 +++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/include/sentry.h b/include/sentry.h index f5f51e50ca..da6ae11e12 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -1032,10 +1032,24 @@ SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body( * `req`, and writes its size to `*len_out`. Returns `NULL` unless `req` * requires a file-backed body -- see `sentry_http_request_get_body`, which * also covers requests with no body at all. + * + * The path is in a platform-specific filesystem path encoding, which on + * Windows is UTF-8 rather than the ANSI code page the narrow Win32 APIs + * assume. API users on Windows are encouraged to use + * `sentry_http_request_get_body_file_pathw` instead, which is what the + * built-in curl and WinHTTP transports use. */ SENTRY_EXPERIMENTAL_API const char *sentry_http_request_get_body_file_path( const sentry_http_request_t *req, size_t *len_out); +#ifdef SENTRY_PLATFORM_WINDOWS +/** + * Wide char version of `sentry_http_request_get_body_file_path`. + */ +SENTRY_EXPERIMENTAL_API const wchar_t *sentry_http_request_get_body_file_pathw( + const sentry_http_request_t *req, size_t *len_out); +#endif + /** * Represents the response to a single HTTP request, to be filled in by a * custom HTTP client's `sentry_http_client_send_func_t` before returning. diff --git a/src/transports/sentry_http_transport.c b/src/transports/sentry_http_transport.c index 802f49ab1e..69ee8182cb 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -435,6 +435,24 @@ sentry_http_request_get_body_file_path( return req->body_path->path; } +#ifdef SENTRY_PLATFORM_WINDOWS +const wchar_t * +sentry_http_request_get_body_file_pathw( + const sentry_http_request_t *req, size_t *len_out) +{ + if (!req || !req->body_path) { + if (len_out) { + *len_out = 0; + } + return NULL; + } + if (len_out) { + *len_out = req->body_len; + } + return req->body_path->path_w; +} +#endif + enum { RESULT_OK = 0, RESULT_ERROR = -1, diff --git a/tests/unit/test_http_transport.c b/tests/unit/test_http_transport.c index c61baa0066..b3a3552b32 100644 --- a/tests/unit/test_http_transport.c +++ b/tests/unit/test_http_transport.c @@ -5,6 +5,9 @@ #include "transports/sentry_http_transport.h" #include +#ifdef SENTRY_PLATFORM_WINDOWS +# include +#endif SENTRY_TEST(http_response_set_header_sets_known_headers) { @@ -172,6 +175,20 @@ SENTRY_TEST(http_request_accessors_file_backed_body) TEST_CHECK_STRING_EQUAL(path, "/tmp/does-not-need-to-exist"); TEST_CHECK_INT_EQUAL((int)len, 100 * 1024 * 1024); +#ifdef SENTRY_PLATFORM_WINDOWS + // The wide variant must stay in sync with the narrow one, since that is + // what a Windows client needs to hand to the `W` Win32 file APIs. + len = 0; + const wchar_t *path_w = sentry_http_request_get_body_file_pathw(req, &len); + TEST_CHECK(path_w != NULL); + TEST_CHECK(wcscmp(path_w, L"/tmp/does-not-need-to-exist") == 0); + TEST_CHECK_INT_EQUAL((int)len, 100 * 1024 * 1024); + + len = 123; + TEST_CHECK(sentry_http_request_get_body_file_pathw(NULL, &len) == NULL); + TEST_CHECK_INT_EQUAL((int)len, 0); +#endif + sentry__prepared_http_request_free(req); }