Skip to content

Commit fef6078

Browse files
authored
Support additional request data types in dio package (#3170)
1 parent 75284dc commit fef6078

File tree

5 files changed

+826
-23
lines changed

5 files changed

+826
-23
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
- Add `DioException` response data to error breadcrumb ([#3164](https://github.com/getsentry/sentry-dart/pull/3164))
2020
- Bumped `dio` min verion to `5.2.0`
21+
- Support additional request data types in `dio` package ([#3170](https://github.com/getsentry/sentry-dart/pull/3170))
22+
- Add support for `json`, `UInt8List`, `num`, `bool`, `FormData`, `Multipart` request data.
2123
- Use FFI/JNI for `captureEnvelope` on iOS and Android ([#3115](https://github.com/getsentry/sentry-dart/pull/3115))
2224
- Log a warning when dropping envelope items ([#3165](https://github.com/getsentry/sentry-dart/pull/3165))
2325
- Call options.log for structured logs ([#3187](https://github.com/getsentry/sentry-dart/pull/3187))

packages/dart/lib/src/protocol/max_body_size.dart

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
// See https://docs.sentry.io/platforms/dotnet/guides/aspnetcore/configuration/options/#max-request-body-size
22

3+
import 'package:meta/meta.dart';
4+
35
const _mediumSize = 10000;
46
const _smallSize = 4000;
57

@@ -21,19 +23,30 @@ enum MaxRequestBodySize {
2123
}
2224

2325
extension MaxRequestBodySizeX on MaxRequestBodySize {
24-
bool shouldAddBody(int contentLength) {
26+
/// Returns the size limit in bytes for this setting, or null if no limit.
27+
@internal
28+
int? getSizeLimit() {
2529
switch (this) {
2630
case MaxRequestBodySize.never:
27-
break;
31+
return 0;
2832
case MaxRequestBodySize.small:
29-
return contentLength <= _smallSize;
33+
return _smallSize;
3034
case MaxRequestBodySize.medium:
31-
return contentLength <= _mediumSize;
35+
return _mediumSize;
3236
case MaxRequestBodySize.always:
33-
return true;
34-
// No default here to get a warning when a new enum value is added.
37+
return null; // No limit
38+
}
39+
}
40+
41+
bool shouldAddBody(int contentLength) {
42+
if (this == MaxRequestBodySize.never) {
43+
return false; // Never add body regardless of size
44+
}
45+
final limit = getSizeLimit();
46+
if (limit == null) {
47+
return true; // No limit means always allow
3548
}
36-
return false;
49+
return contentLength <= limit;
3750
}
3851
}
3952

@@ -56,18 +69,29 @@ enum MaxResponseBodySize {
5669
}
5770

5871
extension MaxResponseBodySizeX on MaxResponseBodySize {
59-
bool shouldAddBody(int contentLength) {
72+
/// Returns the size limit in bytes for this setting, or null if no limit.
73+
@internal
74+
int? getSizeLimit() {
6075
switch (this) {
6176
case MaxResponseBodySize.never:
62-
break;
77+
return 0;
6378
case MaxResponseBodySize.small:
64-
return contentLength <= _smallSize;
79+
return _smallSize;
6580
case MaxResponseBodySize.medium:
66-
return contentLength <= _mediumSize;
81+
return _mediumSize;
6782
case MaxResponseBodySize.always:
68-
return true;
69-
// No default here to get a warning when a new enum value is added.
83+
return null; // No limit
84+
}
85+
}
86+
87+
bool shouldAddBody(int contentLength) {
88+
if (this == MaxResponseBodySize.never) {
89+
return false; // Never add body regardless of size
90+
}
91+
final limit = getSizeLimit();
92+
if (limit == null) {
93+
return true; // No limit means always allow
7094
}
71-
return false;
95+
return contentLength <= limit;
7296
}
7397
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import 'package:test/test.dart';
2+
import 'package:sentry/sentry.dart';
3+
4+
void main() {
5+
group('MaxRequestBodySize', () {
6+
test('getSizeLimit returns correct values', () {
7+
expect(MaxRequestBodySize.never.getSizeLimit(), equals(0));
8+
expect(MaxRequestBodySize.small.getSizeLimit(), equals(4000));
9+
expect(MaxRequestBodySize.medium.getSizeLimit(), equals(10000));
10+
expect(MaxRequestBodySize.always.getSizeLimit(), isNull);
11+
});
12+
13+
test('shouldAddBody works correctly with getSizeLimit', () {
14+
// never - should never add body
15+
expect(MaxRequestBodySize.never.shouldAddBody(0), isFalse);
16+
expect(MaxRequestBodySize.never.shouldAddBody(1000), isFalse);
17+
expect(MaxRequestBodySize.never.shouldAddBody(10000), isFalse);
18+
19+
// small - should add body up to 4000 bytes
20+
expect(MaxRequestBodySize.small.shouldAddBody(0), isTrue);
21+
expect(MaxRequestBodySize.small.shouldAddBody(4000), isTrue);
22+
expect(MaxRequestBodySize.small.shouldAddBody(4001), isFalse);
23+
expect(MaxRequestBodySize.small.shouldAddBody(10000), isFalse);
24+
25+
// medium - should add body up to 10000 bytes
26+
expect(MaxRequestBodySize.medium.shouldAddBody(0), isTrue);
27+
expect(MaxRequestBodySize.medium.shouldAddBody(4000), isTrue);
28+
expect(MaxRequestBodySize.medium.shouldAddBody(10000), isTrue);
29+
expect(MaxRequestBodySize.medium.shouldAddBody(10001), isFalse);
30+
31+
// always - should always add body
32+
expect(MaxRequestBodySize.always.shouldAddBody(0), isTrue);
33+
expect(MaxRequestBodySize.always.shouldAddBody(1000), isTrue);
34+
expect(MaxRequestBodySize.always.shouldAddBody(10000), isTrue);
35+
expect(MaxRequestBodySize.always.shouldAddBody(100000), isTrue);
36+
});
37+
});
38+
39+
group('MaxResponseBodySize', () {
40+
test('getSizeLimit returns correct values', () {
41+
expect(MaxResponseBodySize.never.getSizeLimit(), equals(0));
42+
expect(MaxResponseBodySize.small.getSizeLimit(), equals(4000));
43+
expect(MaxResponseBodySize.medium.getSizeLimit(), equals(10000));
44+
expect(MaxResponseBodySize.always.getSizeLimit(), isNull);
45+
});
46+
47+
test('shouldAddBody works correctly with getSizeLimit', () {
48+
// never - should never add body
49+
expect(MaxResponseBodySize.never.shouldAddBody(0), isFalse);
50+
expect(MaxResponseBodySize.never.shouldAddBody(1000), isFalse);
51+
expect(MaxResponseBodySize.never.shouldAddBody(10000), isFalse);
52+
53+
// small - should add body up to 4000 bytes
54+
expect(MaxResponseBodySize.small.shouldAddBody(0), isTrue);
55+
expect(MaxResponseBodySize.small.shouldAddBody(4000), isTrue);
56+
expect(MaxResponseBodySize.small.shouldAddBody(4001), isFalse);
57+
expect(MaxResponseBodySize.small.shouldAddBody(10000), isFalse);
58+
59+
// medium - should add body up to 10000 bytes
60+
expect(MaxResponseBodySize.medium.shouldAddBody(0), isTrue);
61+
expect(MaxResponseBodySize.medium.shouldAddBody(4000), isTrue);
62+
expect(MaxResponseBodySize.medium.shouldAddBody(10000), isTrue);
63+
expect(MaxResponseBodySize.medium.shouldAddBody(10001), isFalse);
64+
65+
// always - should always add body
66+
expect(MaxResponseBodySize.always.shouldAddBody(0), isTrue);
67+
expect(MaxResponseBodySize.always.shouldAddBody(1000), isTrue);
68+
expect(MaxResponseBodySize.always.shouldAddBody(10000), isTrue);
69+
expect(MaxResponseBodySize.always.shouldAddBody(100000), isTrue);
70+
});
71+
});
72+
}

packages/dio/lib/src/dio_event_processor.dart

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import 'package:dio/dio.dart';
44
import 'package:sentry/sentry.dart';
5+
import 'dart:convert';
56

67
/// This is an [EventProcessor], which improves crash reports of [DioError]s.
78
/// It adds information about [DioError.requestOptions] if present and also about
@@ -49,27 +50,98 @@ class DioEventProcessor implements EventProcessor {
4950
uri: options.uri,
5051
method: options.method,
5152
headers: _options.sendDefaultPii ? headers : null,
52-
data: _getRequestData(dioError.requestOptions.data),
53+
data: _getRequestData(dioError.requestOptions.data, options),
5354
);
5455
}
5556

5657
/// Returns the request data, if possible according to the users settings.
57-
Object? _getRequestData(Object? data) {
58-
if (!_options.sendDefaultPii) {
58+
/// Takes into account the content type to determine proper encoding.
59+
///
60+
Object? _getRequestData(Object? data, RequestOptions requestOptions) {
61+
if (!_options.sendDefaultPii || data == null) {
5962
return null;
6063
}
64+
65+
// Handle different data types based on Dio's encoding behavior and content type
6166
if (data is String) {
62-
if (_options.maxRequestBodySize.shouldAddBody(data.codeUnits.length)) {
67+
// For all strings, use UTF-8 encoding for accurate size validation
68+
if (_canEncodeStringWithinLimit(
69+
data,
70+
// ignore: invalid_use_of_internal_member
71+
hardLimit: _options.maxRequestBodySize.getSizeLimit(),
72+
)) {
6373
return data;
6474
}
65-
} else if (data is List<int>) {
75+
}
76+
// For List<int> data (including Uint8List), we have exact size information
77+
else if (data is List<int>) {
6678
if (_options.maxRequestBodySize.shouldAddBody(data.length)) {
6779
return data;
6880
}
81+
} else if (data is num || data is bool) {
82+
if (_options.maxRequestBodySize != MaxRequestBodySize.never) {
83+
return data;
84+
}
85+
} else if (Transformer.isJsonMimeType(requestOptions.contentType)) {
86+
if (_canEncodeJsonWithinLimit(
87+
data,
88+
// ignore: invalid_use_of_internal_member
89+
hardLimit: _options.maxRequestBodySize.getSizeLimit(),
90+
)) {
91+
return data;
92+
}
93+
} else if (data is FormData) {
94+
// FormData has a built-in length property for size checking
95+
if (_options.maxRequestBodySize.shouldAddBody(data.length)) {
96+
return _convertFormDataToMap(data);
97+
}
98+
} else if (data is MultipartFile) {
99+
if (_options.maxRequestBodySize.shouldAddBody(data.length)) {
100+
return _convertMultipartFileToMap(data);
101+
}
69102
}
103+
70104
return null;
71105
}
72106

107+
/// Converts FormData to a map representation that SentryRequest can handle
108+
Map<String, dynamic> _convertFormDataToMap(FormData formData) {
109+
final result = <String, dynamic>{};
110+
111+
// Add form fields - ensure proper typing
112+
for (final field in formData.fields) {
113+
result[field.key] = field.value;
114+
}
115+
116+
// Add file information (metadata only, not the actual file content)
117+
for (final file in formData.files) {
118+
result['${file.key}_file'] = _convertMultipartFileToMap(file.value);
119+
}
120+
121+
return result;
122+
}
123+
124+
/// Converts a MultipartFile to a map representation that SentryRequest can handle
125+
Map<String, dynamic> _convertMultipartFileToMap(MultipartFile file) {
126+
final result = <String, dynamic>{
127+
'filename': file.filename,
128+
'contentType': file.contentType?.toString(),
129+
'length': file.length,
130+
};
131+
132+
// Only add headers if they exist and are not empty
133+
if (file.headers != null && file.headers!.isNotEmpty) {
134+
// Convert headers to a proper Map<String, dynamic>
135+
final headersMap = <String, dynamic>{};
136+
for (final entry in file.headers!.entries) {
137+
headersMap[entry.key] = entry.value;
138+
}
139+
result['headers'] = headersMap;
140+
}
141+
142+
return result;
143+
}
144+
73145
SentryResponse _responseFrom(DioError dioError) {
74146
final response = dioError.response;
75147

@@ -104,3 +176,65 @@ class DioEventProcessor implements EventProcessor {
104176
return data;
105177
}
106178
}
179+
180+
/// Returns true if the data can be encoded as JSON within the given byte limit.
181+
bool _canEncodeJsonWithinLimit(Object? data, {int? hardLimit}) {
182+
if (hardLimit == null) {
183+
// No limit means always allow
184+
return true;
185+
}
186+
if (hardLimit == 0) {
187+
// Zero limit means never allow
188+
return false;
189+
}
190+
191+
// Only proceed with encoding if we have a positive limit
192+
final sink = _CountingByteSink(hardLimit);
193+
final conv = JsonUtf8Encoder().startChunkedConversion(sink);
194+
try {
195+
conv.add(data);
196+
conv.close();
197+
return true;
198+
} on _SizeLimitExceeded {
199+
return false;
200+
} catch (_) {
201+
return false;
202+
}
203+
}
204+
205+
/// Returns true if the string can be encoded as UTF-8 within the given byte limit.
206+
bool _canEncodeStringWithinLimit(String data, {int? hardLimit}) {
207+
if (hardLimit == null) {
208+
// No limit means always allow
209+
return true;
210+
}
211+
if (hardLimit == 0) {
212+
// Zero limit means never allow
213+
return false;
214+
}
215+
216+
// Only proceed with encoding if we have a positive limit
217+
final utf8Bytes = utf8.encode(data);
218+
return utf8Bytes.length <= hardLimit;
219+
}
220+
221+
/// Exception thrown when the hard limit is exceeded during counting.
222+
class _SizeLimitExceeded implements Exception {}
223+
224+
/// A sink that counts bytes without storing them, with an optional hard limit.
225+
class _CountingByteSink implements Sink<List<int>> {
226+
int count = 0;
227+
final int? hardLimit;
228+
_CountingByteSink([this.hardLimit]);
229+
230+
@override
231+
void add(List<int> chunk) {
232+
count += chunk.length;
233+
if (hardLimit != null && count > hardLimit!) {
234+
throw _SizeLimitExceeded();
235+
}
236+
}
237+
238+
@override
239+
void close() {}
240+
}

0 commit comments

Comments
 (0)