Skip to content

Commit 065d610

Browse files
authored
feat(tus): add Upload-Metadata header to TUS requests (#1795)
* feat(tus): add Upload-Metadata header to TUS requests Add the `Upload-Metadata` header to TUS creation (POST) and upload (PATCH) requests in the `sentry <base64({"attachment_type":"<type>"})>` format that the relay server expects for preliminary quota checks. * Update CHANGELOG.md
1 parent de12398 commit 065d610

12 files changed

Lines changed: 203 additions & 40 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- Add a `transfer_timeout` option for SDK-managed HTTP transports. ([#1741](https://github.com/getsentry/sentry-native/pull/1741))
1414
- Apple: use `os_sync_wait_on_address` for the level-triggered waitable flag in the batcher on modern macOS(14.4+) and iOS(17.4+). ([#1765](https://github.com/getsentry/sentry-native/pull/1765))
1515
- Native/macOS: add thread names. ([#1766](https://github.com/getsentry/sentry-native/pull/1766))
16+
- Add Upload-Metadata header to TUS requests. ([#1795](https://github.com/getsentry/sentry-native/pull/1795))
1617

1718
**Fixes**:
1819

examples/example.c

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -658,22 +658,6 @@ main(int argc, char **argv)
658658

659659
if (has_arg(argc, argv, "large-attachment")) {
660660
sentry_options_set_enable_large_attachments(options, 1);
661-
const char *large_file = ".sentry-large-attachment";
662-
FILE *f = fopen(large_file, "wb");
663-
if (f) {
664-
// 100 MB = TUS upload threshold
665-
char zeros[4096];
666-
memset(zeros, 0, sizeof(zeros));
667-
size_t remaining = 100 * 1024 * 1024;
668-
while (remaining > 0) {
669-
size_t chunk
670-
= remaining < sizeof(zeros) ? remaining : sizeof(zeros);
671-
fwrite(zeros, 1, chunk, f);
672-
remaining -= chunk;
673-
}
674-
fclose(f);
675-
sentry_options_add_attachment(options, large_file);
676-
}
677661
}
678662

679663
if (has_arg(argc, argv, "stdout")) {
@@ -968,6 +952,29 @@ main(int argc, char **argv)
968952
view_hierarchy, SENTRY_ATTACHMENT_TYPE_VIEW_HIERARCHY);
969953
}
970954

955+
if (has_arg(argc, argv, "large-attachment")) {
956+
const char *large_file = ".sentry-large-attachment";
957+
FILE *f = fopen(large_file, "wb");
958+
if (f) {
959+
// 100 MB = TUS upload threshold
960+
char zeros[4096];
961+
memset(zeros, 0, sizeof(zeros));
962+
size_t remaining = 100 * 1024 * 1024;
963+
while (remaining > 0) {
964+
size_t chunk
965+
= remaining < sizeof(zeros) ? remaining : sizeof(zeros);
966+
fwrite(zeros, 1, chunk, f);
967+
remaining -= chunk;
968+
}
969+
fclose(f);
970+
sentry_attachment_t *attachment = sentry_attach_file(large_file);
971+
if (attachment) {
972+
sentry_attachment_set_type(
973+
attachment, SENTRY_ATTACHMENT_TYPE_MINIDUMP);
974+
}
975+
}
976+
}
977+
971978
if (sentry_options_get_enable_logs(options)) {
972979
if (has_arg(argc, argv, "capture-log")) {
973980
sentry_log_debug("I'm a log message!");

src/sentry_envelope.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1551,6 +1551,8 @@ sentry__envelope_item_get_attachment_ref(
15511551
sentry_value_get_by_key(ref->_owner, "location"));
15521552
ref->content_type = sentry_value_as_string(
15531553
sentry_value_get_by_key(ref->_owner, "content_type"));
1554+
ref->attachment_type = sentry_value_as_string(
1555+
sentry_value_get_by_key(item->headers, "attachment_type"));
15541556
return true;
15551557
}
15561558

@@ -1599,6 +1601,9 @@ sentry__envelope_item_resolve_attachment_ref(
15991601
if (sentry__guarded_strlen(ref.path)) {
16001602
resolved.path = ref.path;
16011603
}
1604+
if (sentry__guarded_strlen(ref.attachment_type)) {
1605+
resolved.attachment_type = ref.attachment_type;
1606+
}
16021607
resolved.location = location;
16031608
if (sentry__guarded_strlen(ref.content_type)) {
16041609
resolved.content_type = ref.content_type;

src/sentry_envelope.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ typedef struct {
2222
const char *path;
2323
const char *location;
2424
const char *content_type;
25+
const char *attachment_type;
2526
sentry_value_t _owner;
2627
} sentry_attachment_ref_t;
2728

src/sentry_utils.c

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#include <locale.h>
1515
#include <math.h>
1616
#include <stdarg.h>
17+
#include <stdint.h>
1718
#include <stdio.h>
1819
#include <stdlib.h>
1920
#include <string.h>
@@ -435,6 +436,37 @@ sentry__dsn_get_minidump_url(const sentry_dsn_t *dsn, const char *user_agent)
435436
return sentry__stringbuilder_into_string(&sb);
436437
}
437438

439+
char *
440+
sentry__base64_encode(const char *data, size_t len)
441+
{
442+
static const char b64[]
443+
= "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
444+
size_t output_len = 4 * ((len + 2) / 3);
445+
char *out = sentry_malloc(output_len + 1);
446+
if (!out) {
447+
return NULL;
448+
}
449+
450+
size_t i, j;
451+
for (i = 0, j = 0; i < len; i += 3) {
452+
uint32_t triple = (uint32_t)(unsigned char)data[i] << 16;
453+
if (i + 1 < len) {
454+
triple |= (uint32_t)(unsigned char)data[i + 1] << 8;
455+
}
456+
if (i + 2 < len) {
457+
triple |= (uint32_t)(unsigned char)data[i + 2];
458+
}
459+
460+
out[j++] = b64[(triple >> 18) & 0x3F];
461+
out[j++] = b64[(triple >> 12) & 0x3F];
462+
out[j++] = i + 1 < len ? b64[(triple >> 6) & 0x3F] : '=';
463+
out[j++] = i + 2 < len ? b64[triple & 0x3F] : '=';
464+
}
465+
466+
out[j] = '\0';
467+
return out;
468+
}
469+
438470
char *
439471
sentry__usec_time_to_iso8601(uint64_t time)
440472
{

src/sentry_utils.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,12 @@ char *sentry__dsn_resolve_url(const sentry_dsn_t *dsn, const char *path);
158158
char *sentry__dsn_get_minidump_url(
159159
const sentry_dsn_t *dsn, const char *user_agent);
160160

161+
/**
162+
* RFC 4648 base64-encodes `len` bytes from `data` and returns a newly
163+
* allocated NUL-terminated string (caller frees).
164+
*/
165+
char *sentry__base64_encode(const char *data, size_t len);
166+
161167
/**
162168
* Returns the number of microseconds since the unix epoch.
163169
*/

src/transports/sentry_http_transport.c

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "sentry_client_report.h"
55
#include "sentry_database.h"
66
#include "sentry_envelope.h"
7+
#include "sentry_json.h"
78
#include "sentry_options.h"
89
#include "sentry_ratelimiter.h"
910
#include "sentry_retry.h"
@@ -20,7 +21,7 @@
2021

2122
#define ENVELOPE_MIME "application/x-sentry-envelope"
2223
#define TUS_MIME "application/offset+octet-stream"
23-
#define TUS_MAX_HTTP_HEADERS 4
24+
#define TUS_MAX_HTTP_HEADERS 5
2425
#ifdef SENTRY_TRANSPORT_COMPRESSION
2526
# define MAX_HTTP_HEADERS 4
2627
#else
@@ -195,9 +196,39 @@ sentry__prepared_http_request_free(sentry_prepared_http_request_t *req)
195196
sentry_free(req);
196197
}
197198

199+
static char *
200+
tus_build_upload_metadata(const char *attachment_type)
201+
{
202+
sentry_jsonwriter_t *jw = sentry__jsonwriter_new_sb(NULL);
203+
sentry__jsonwriter_write_object_start(jw);
204+
sentry__jsonwriter_write_key(jw, "attachment_type");
205+
sentry__jsonwriter_write_str(jw, attachment_type);
206+
sentry__jsonwriter_write_object_end(jw);
207+
208+
size_t json_len;
209+
char *json = sentry__jsonwriter_into_string(jw, &json_len);
210+
if (!json) {
211+
return NULL;
212+
}
213+
214+
char *encoded = sentry__base64_encode(json, json_len);
215+
sentry_free(json);
216+
if (!encoded) {
217+
return NULL;
218+
}
219+
220+
sentry_stringbuilder_t sb;
221+
sentry__stringbuilder_init(&sb);
222+
sentry__stringbuilder_append(&sb, "sentry ");
223+
sentry__stringbuilder_append(&sb, encoded);
224+
sentry_free(encoded);
225+
226+
return sentry__stringbuilder_into_string(&sb);
227+
}
228+
198229
static sentry_prepared_http_request_t *
199-
prepare_tus_request_common(
200-
size_t upload_size, const sentry_dsn_t *dsn, const char *user_agent)
230+
prepare_tus_request_common(size_t upload_size, const char *attachment_type,
231+
const sentry_dsn_t *dsn, const char *user_agent)
201232
{
202233
if (!dsn || !dsn->is_valid) {
203234
return NULL;
@@ -234,12 +265,19 @@ prepare_tus_request_common(
234265
h->key = "upload-length";
235266
h->value = sentry__uint64_to_string((uint64_t)upload_size);
236267

268+
if (!sentry__string_empty(attachment_type)) {
269+
h = &req->headers[req->headers_len++];
270+
h->key = "upload-metadata";
271+
h->value = tus_build_upload_metadata(attachment_type);
272+
}
273+
237274
return req;
238275
}
239276

240277
static sentry_prepared_http_request_t *
241278
prepare_tus_upload_request(const char *location, const sentry_path_t *path,
242-
size_t file_size, const sentry_dsn_t *dsn, const char *user_agent)
279+
size_t file_size, const char *attachment_type, const sentry_dsn_t *dsn,
280+
const char *user_agent)
243281
{
244282
if (!location || !path) {
245283
return NULL;
@@ -282,6 +320,12 @@ prepare_tus_upload_request(const char *location, const sentry_path_t *path,
282320
h->key = "upload-offset";
283321
h->value = sentry__string_clone("0");
284322

323+
if (!sentry__string_empty(attachment_type)) {
324+
h = &req->headers[req->headers_len++];
325+
h->key = "upload-metadata";
326+
h->value = tus_build_upload_metadata(attachment_type);
327+
}
328+
285329
return req;
286330
}
287331

@@ -346,22 +390,22 @@ http_update_ratelimiter(
346390
// resulting remote location URL (caller frees) in `location_out`.
347391
static int
348392
tus_upload_file(http_transport_state_t *state, const sentry_path_t *cache_path,
349-
const char *basename, char **location_out)
393+
const sentry_attachment_ref_t *ref, char **location_out)
350394
{
351-
if (!basename || *basename == '\0') {
395+
if (sentry__string_empty(ref->path)) {
352396
return RESULT_ERROR;
353397
}
354398
*location_out = NULL;
355-
sentry_path_t *att_file = sentry__path_join_str(cache_path, basename);
399+
sentry_path_t *att_file = sentry__path_join_str(cache_path, ref->path);
356400
size_t file_size = att_file ? sentry__path_get_size(att_file) : 0;
357401
if (!att_file || file_size == 0) {
358402
sentry__path_free(att_file);
359403
return RESULT_ERROR;
360404
}
361405

362406
// Step 1: TUS creation (POST, no body)
363-
sentry_prepared_http_request_t *req
364-
= prepare_tus_request_common(file_size, state->dsn, state->user_agent);
407+
sentry_prepared_http_request_t *req = prepare_tus_request_common(
408+
file_size, ref->attachment_type, state->dsn, state->user_agent);
365409
if (!req) {
366410
sentry__path_free(att_file);
367411
return RESULT_ERROR;
@@ -393,8 +437,8 @@ tus_upload_file(http_transport_state_t *state, const sentry_path_t *cache_path,
393437
sentry__path_free(att_file);
394438
return RESULT_ERROR;
395439
}
396-
req = prepare_tus_upload_request(
397-
patch_url, att_file, file_size, state->dsn, state->user_agent);
440+
req = prepare_tus_upload_request(patch_url, att_file, file_size,
441+
ref->attachment_type, state->dsn, state->user_agent);
398442
sentry_free(patch_url);
399443
sentry__path_free(att_file);
400444
if (!req) {
@@ -553,8 +597,7 @@ resolve_attachment_refs(
553597
}
554598

555599
char *new_location = NULL;
556-
int result
557-
= tus_upload_file(state, cache_path, ref.path, &new_location);
600+
int result = tus_upload_file(state, cache_path, &ref, &new_location);
558601
if (new_location) {
559602
bool resolved = sentry__envelope_item_resolve_attachment_ref(
560603
item, new_location);
@@ -933,17 +976,19 @@ sentry__http_transport_get_bgworker(sentry_transport_t *transport)
933976
#endif
934977

935978
sentry_prepared_http_request_t *
936-
sentry__prepare_tus_create_request(
937-
size_t file_size, const sentry_dsn_t *dsn, const char *user_agent)
979+
sentry__prepare_tus_create_request(size_t file_size,
980+
const char *attachment_type, const sentry_dsn_t *dsn,
981+
const char *user_agent)
938982
{
939-
return prepare_tus_request_common(file_size, dsn, user_agent);
983+
return prepare_tus_request_common(
984+
file_size, attachment_type, dsn, user_agent);
940985
}
941986

942987
sentry_prepared_http_request_t *
943988
sentry__prepare_tus_upload_request(const char *location,
944-
const sentry_path_t *path, size_t file_size, const sentry_dsn_t *dsn,
945-
const char *user_agent)
989+
const sentry_path_t *path, size_t file_size, const char *attachment_type,
990+
const sentry_dsn_t *dsn, const char *user_agent)
946991
{
947992
return prepare_tus_upload_request(
948-
location, path, file_size, dsn, user_agent);
993+
location, path, file_size, attachment_type, dsn, user_agent);
949994
}

src/transports/sentry_http_transport.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ sentry_prepared_http_request_t *sentry__prepare_http_request(
2727
sentry_envelope_t *envelope, const sentry_dsn_t *dsn,
2828
const sentry_rate_limiter_t *rl, const char *user_agent);
2929
sentry_prepared_http_request_t *sentry__prepare_tus_create_request(
30-
size_t file_size, const sentry_dsn_t *dsn, const char *user_agent);
30+
size_t file_size, const char *attachment_type, const sentry_dsn_t *dsn,
31+
const char *user_agent);
3132
sentry_prepared_http_request_t *sentry__prepare_tus_upload_request(
3233
const char *location, const sentry_path_t *path, size_t file_size,
33-
const sentry_dsn_t *dsn, const char *user_agent);
34+
const char *attachment_type, const sentry_dsn_t *dsn,
35+
const char *user_agent);
3436

3537
void sentry__prepared_http_request_free(sentry_prepared_http_request_t *req);
3638

tests/test_integration_tus.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
auth_header = (
2424
f"Sentry sentry_key=uiaeosnrtdy, sentry_version=7, sentry_client=sentry.native/{SENTRY_VERSION}"
2525
)
26+
# sentry <base64({"attachment_type":"event.minidump"})>
27+
upload_metadata = "sentry eyJhdHRhY2htZW50X3R5cGUiOiJldmVudC5taW5pZHVtcCJ9"
2628
# fmt: on
2729

2830

@@ -39,6 +41,7 @@ def test_tus_upload(cmake, httpserver):
3941
# TUS creation request (POST, no body) -> 201 + Location
4042
httpserver.expect_oneshot_request(
4143
"/api/123456/upload/",
44+
method="POST",
4245
headers={"tus-resumable": "1.0.0"},
4346
).respond_with_data(
4447
"OK",
@@ -92,6 +95,7 @@ def test_tus_upload(cmake, httpserver):
9295
upload_length = create_req.headers.get("upload-length")
9396
assert upload_length is not None
9497
assert int(upload_length) == 100 * 1024 * 1024
98+
assert create_req.headers.get("upload-metadata") == upload_metadata
9599

96100
# Verify TUS upload request headers
97101
assert upload_req.headers.get("tus-resumable") == "1.0.0"
@@ -118,6 +122,7 @@ def test_tus_upload(cmake, httpserver):
118122
assert attachment_ref.payload.json["location"] == location
119123
assert not os.path.isabs(attachment_ref.payload.json["path"])
120124
assert attachment_ref.headers.get("attachment_length") == 100 * 1024 * 1024
125+
assert attachment_ref.headers.get("attachment_type") == "event.minidump"
121126

122127
# large attachment files should be cleaned up after send
123128
cache_dir = os.path.join(tmp_path, ".sentry-native", "cache")
@@ -461,6 +466,7 @@ def test_tus_crash_restart(cmake, httpserver, backend):
461466
# Verify TUS creation request headers
462467
assert create_req.headers.get("tus-resumable") == "1.0.0"
463468
assert int(create_req.headers.get("upload-length")) == 100 * 1024 * 1024
469+
assert create_req.headers.get("upload-metadata") == upload_metadata
464470

465471
# Verify TUS upload request headers
466472
assert upload_req.headers.get("tus-resumable") == "1.0.0"
@@ -550,6 +556,7 @@ def test_tus_crash_native(cmake, httpserver):
550556
assert upload_req is not None
551557
assert envelope_req is not None
552558
assert int(create_req.headers.get("upload-length")) == 100 * 1024 * 1024
559+
assert create_req.headers.get("upload-metadata") == upload_metadata
553560

554561
body = envelope_req.get_data()
555562
envelope = Envelope.deserialize(body)

0 commit comments

Comments
 (0)