diff --git a/lib/src/network_request.dart b/lib/src/network_request.dart index 01d90a1..8afc1b8 100644 --- a/lib/src/network_request.dart +++ b/lib/src/network_request.dart @@ -169,17 +169,25 @@ class RawRequest extends NetworkRequest { ); } -/// The body of a [NetworkRequest]. +/// The body of a [NetworkRequest]. Note that this type is aimed at providing +/// readability and type safety and does not dictate behavior of [SturdyHttp] +/// with regards to the content-type header. If you want [SturdyHttp] to infer +/// the content-type header, configure this via the `inferContentType` parameter +/// when constructing the instance. @Freezed(copyWith: false) class NetworkRequestBody with _$NetworkRequestBody { /// An empty body. Results in `null` being passed to `data` of the request. const factory NetworkRequestBody.empty() = _Empty; - /// A JSON body. Passed directly to `data` of the request. + /// A JSON body. Passed directly to `data` of the request. If `inferContentType` + /// has been provided as `true` to the [SturdyHttp] instance, will result in + /// an `application-json` `content-type`. const factory NetworkRequestBody.json(Json data) = _Json; /// A raw body. Allows for nullable untyped data that is passed directly /// to `data` of the request, useful for instances where the data type - /// is not known until runtime. + /// is not known until runtime. If `inferContentType` has been provided as + /// `true` to the [SturdyHttp] instance *and* the [data] can be used to infer + /// the `content-type` header, it will be inferred. const factory NetworkRequestBody.raw(Object? data) = _Raw; } diff --git a/lib/src/sturdy_http.dart b/lib/src/sturdy_http.dart index 03e100d..2700a96 100644 --- a/lib/src/sturdy_http.dart +++ b/lib/src/sturdy_http.dart @@ -47,6 +47,7 @@ class SturdyHttp { SturdyHttpEventListener? eventListener, HttpClientAdapter? customAdapter, Map? proxy, + bool inferContentType = true, }) : this._( dio: _configureDio( baseUrl: baseUrl, @@ -54,6 +55,7 @@ class SturdyHttp { interceptors: interceptors, customAdapter: customAdapter, proxy: proxy, + inferContentType: inferContentType, ), deserializer: deserializer, eventListener: eventListener, @@ -227,6 +229,7 @@ Dio _configureDio({ required Deserializer deserializer, required HttpClientAdapter? customAdapter, required Map? proxy, + required bool inferContentType, }) { return Dio() // Instruct Dio to use the same Isolate approach as requested of SturdyHttp @@ -236,10 +239,12 @@ Dio _configureDio({ ..options.baseUrl = baseUrl ..options.listFormat = ListFormat.multiCompatible ..interceptors.addAll(interceptors) - ..interceptors.removeImplyContentTypeInterceptor() ..map((dio) { if (customAdapter != null) { - return dio..httpClientAdapter = customAdapter; + dio.httpClientAdapter = customAdapter; + } + if (!inferContentType) { + dio.interceptors.removeImplyContentTypeInterceptor(); } return dio; }); diff --git a/test/src/sturdy_http_test.dart b/test/src/sturdy_http_test.dart index d4ba4b3..221477e 100644 --- a/test/src/sturdy_http_test.dart +++ b/test/src/sturdy_http_test.dart @@ -39,6 +39,7 @@ void main() { String baseUrl = 'http://example.com', List interceptors = const [], Map? proxy, + bool inferContentType = false, }) { return SturdyHttp( baseUrl: baseUrl, @@ -46,18 +47,72 @@ void main() { eventListener: eventListener, interceptors: interceptors, proxy: proxy, + inferContentType: inferContentType, ); } group('interceptors', () { - test('it returns the correct interceptors', () { + test('it returns the provided interceptors', () { final interceptors = [_FakeInterceptor()]; - final subject = buildSubject(interceptors: interceptors); + final subject = buildSubject( + interceptors: interceptors, + inferContentType: false, + ); expect(subject.interceptors, interceptors); }); }); + group('inferContentType', () { + test('it does not infer content-type when false', () async { + String? contentType; + final subject = buildSubject( + inferContentType: false, + interceptors: [ + _FakeInterceptor( + onRequestInvoked: (options) { + contentType = + options.headers[Headers.contentTypeHeader] as String?; + }, + ) + ], + ); + + charlatan.whenPost('/infer', (request) => null); + + await subject.execute( + PostRequest('/infer', data: NetworkRequestBody.json({'foo': 'bar'})), + onResponse: (r) {}, + ); + + expect(contentType, isNull); + }); + + test('it infers content-type when true', () async { + String? contentType; + final subject = buildSubject( + inferContentType: true, + interceptors: [ + _FakeInterceptor( + onRequestInvoked: (options) { + contentType = + options.headers[Headers.contentTypeHeader] as String?; + }, + ) + ], + ); + + charlatan.whenPost('/infer', (request) => null); + + await subject.execute( + PostRequest('/infer', data: NetworkRequestBody.json({'foo': 'bar'})), + onResponse: (r) {}, + ); + + expect(contentType, 'application/json'); + }); + }); + group('baseUrl', () { test('it returns the correct baseUrl', () { const baseUrl = 'https://foo.com'; @@ -756,7 +811,17 @@ class Result with _$Result { const factory Result.failure(F failure) = _Failure; } -class _FakeInterceptor extends Interceptor {} +class _FakeInterceptor extends Interceptor { + final Function(RequestOptions options)? onRequestInvoked; + + _FakeInterceptor({this.onRequestInvoked}); + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + onRequestInvoked?.call(options); + super.onRequest(options, handler); + } +} class _SturdyHttpEventListener extends SturdyHttpEventListener { final void Function(RequestOptions) onAuthFailure;