diff --git a/CHANGELOG.md b/CHANGELOG.md index 918b0c7d84..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)) diff --git a/include/sentry.h b/include/sentry.h index 271f2d0006..da6ae11e12 100644 --- a/include/sentry.h +++ b/include/sentry.h @@ -972,6 +972,218 @@ 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_http_request_s; +typedef struct sentry_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 + * 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); + +/** + * 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`, 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. + * + * 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); + +/** + * 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` + * 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 sentry_http_client_t *(*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. 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 + * 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 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)(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 + * 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)(sentry_http_client_t *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)( + 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)(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 1fe9d2db4c..69ee8182cb 100644 --- a/src/transports/sentry_http_transport.c +++ b/src/transports/sentry_http_transport.c @@ -35,8 +35,12 @@ 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); + // 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; @@ -330,6 +334,125 @@ 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; +} + +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; +} + +#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, @@ -342,7 +465,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; } @@ -837,6 +962,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( @@ -901,7 +1031,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 +1091,45 @@ 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)(sentry_http_client_t *client)) +{ + if (!factory || !send_func) { + return NULL; + } + sentry_http_client_t *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)( + 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)(sentry_http_client_t *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..135a7a584f 100644 --- a/src/transports/sentry_http_transport.h +++ b/src/transports/sentry_http_transport.h @@ -12,7 +12,7 @@ typedef struct sentry_prepared_http_header_s { char *value; } sentry_prepared_http_header_t; -typedef struct sentry_prepared_http_request_s { +typedef struct sentry_http_request_s { const char *method; char *url; sentry_prepared_http_header_t *headers; @@ -35,23 +35,24 @@ sentry_prepared_http_request_t *sentry__prepare_tus_upload_request( void sentry__prepared_http_request_free(sentry_prepared_http_request_t *req); -typedef struct { +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..83378f5494 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) { @@ -530,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 2854d6bc7c..5f44a3400d 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,22 +376,32 @@ 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(); 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/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..b3a3552b32 --- /dev/null +++ b/tests/unit/test_http_transport.c @@ -0,0 +1,320 @@ +#include "sentry_alloc.h" +#include "sentry_path.h" +#include "sentry_string.h" +#include "sentry_testsupport.h" +#include "transports/sentry_http_transport.h" + +#include +#ifdef SENTRY_PLATFORM_WINDOWS +# include +#endif + +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); +} + +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); + +#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); +} + +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); + 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; +} + +// 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) +{ + (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) +{ + sentry_transport_t *transport = sentry_http_transport_new( + 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/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..cca608f969 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_bodyless_request) +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_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)