Repository: cfug/dio (dart)

Issue:
v5.0.0 manually setting the content-type
### Request Statement

I was just wondering why did you remove the default content-type?

I think this would be a problem for people like me who had a huge set of apis that bunch of them is sending a multi-part form data.

the problem is I was only specifying the content type when I'm about to send a multi-part form. Now to migrate all the project I have to edit every endpoint that doesn't send a multi part form. And I think this would be tedious.


And thanks for keeping this Great library up-to-date!

### Solution Brainstorm


The first thing that comes to mind is set up an interceptor and check for the data type if it is a `FormData` or not. I don't know yet if this is a valid solution or not. But why all the hustle?

Pull request description:
Reverts the functionality of cfug/diox#69.

Resolves #1667 #1661 #1653 #1650 #1648 #1646

### New Pull Request Checklist

- [x] I have read the [Documentation](https://pub.dev/documentation/dio/latest/)
- [x] I have searched for a similar pull request in the [project](https://github.com/cfug/dio/pulls) and found none
- [x] I have updated this branch with the latest `main` branch to avoid conflicts (via merge from master or rebase)
- [x] I have added the required tests to prove the fix/feature I'm adding
- [x] I have updated the documentation (if necessary)
- [x] I have run the tests without failures

### Additional context and info (if any)

- The interceptor can be removed by calling `Interceptor.removeImplyContentTypeInterceptor`.
- The interceptor was intended not to be exposed to outside usages.

Patch:
diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md
index d96173e9c..7113e60bd 100644
--- a/dio/CHANGELOG.md
+++ b/dio/CHANGELOG.md
@@ -2,6 +2,7 @@
 
 ## Unreleased
 
+- Add `ImplyContentTypeInterceptor` as a default interceptor.
 - Add `Headers.multipartFormDataContentType` for headers usage.
 
 ## 5.0.0
@@ -18,8 +19,6 @@
 
 ### Breaking Changes
 
-- Content type with `application/json` and `application/x-www-form-urlencoded`
-  will not be implied anymore in the transformer and the request option.
 - The default charset `utf-8` in `Headers` content type constants has been removed.
 - `BaseOptions.setRequestContentTypeWhenNoPayload` has been removed.
 - Improve `DioError`s. There are now more cases in which the inner original stacktrace is supplied.
diff --git a/dio/README-ZH.md b/dio/README-ZH.md
index ba8770978..681b9b891 100644
--- a/dio/README-ZH.md
+++ b/dio/README-ZH.md
@@ -311,6 +311,10 @@ dynamic data;
 String path = '';
 
 /// 请求的 Content-Type。
+///
+/// 默认值会由 [ImplyContentTypeInterceptor] 根据请求载荷类型进行推导。
+/// 可以调用 [Interceptors.removeImplyContentTypeInterceptor] 进行移除。
+///
 /// 如果你想以 `application/x-www-form-urlencoded` 格式编码请求数据,
 /// 可以设置此选项为 `Headers.formUrlEncodedContentType`,
 /// [Dio] 会自动编码请求体。
diff --git a/dio/README.md b/dio/README.md
index 19258e862..e5364e919 100644
--- a/dio/README.md
+++ b/dio/README.md
@@ -313,6 +313,12 @@ dynamic data;
 String path;
 
 /// The request Content-Type.
+///
+/// The default `content-type` for requests will be implied by the
+/// [ImplyContentTypeInterceptor] according to the type of the request payload.
+/// The interceptor can be removed by
+/// [Interceptors.removeImplyContentTypeInterceptor].
+///
 /// If you want to encode request body with 'application/x-www-form-urlencoded',
 /// you can set [Headers.formUrlEncodedContentType], and [Dio]
 /// will automatically encode the request body.
diff --git a/dio/lib/src/dio_mixin.dart b/dio/lib/src/dio_mixin.dart
index f488e1a55..8998a3cb4 100644
--- a/dio/lib/src/dio_mixin.dart
+++ b/dio/lib/src/dio_mixin.dart
@@ -4,17 +4,17 @@ import 'dart:convert';
 import 'dart:math' as math;
 import 'dart:typed_data';
 
-import 'package:dio/src/transformers/background_transformer.dart';
-
 import 'adapter.dart';
 import 'cancel_token.dart';
 import 'dio.dart';
 import 'dio_error.dart';
 import 'form_data.dart';
 import 'headers.dart';
+import 'interceptors/imply_content_type.dart';
 import 'options.dart';
 import 'response.dart';
 import 'transformer.dart';
+import 'transformers/background_transformer.dart';
 
 import 'progress_stream/io_progress_stream.dart'
     if (dart.library.html) 'progress_stream/browser_progress_stream.dart';
@@ -523,7 +523,7 @@ abstract class DioMixin implements Dio {
     }).catchError((dynamic e, StackTrace s) {
       final isState = e is InterceptorState;
       if (isState) {
-        if ((e as InterceptorState).type == InterceptorResultType.resolve) {
+        if (e.type == InterceptorResultType.resolve) {
           return assureResponse<T>(e.data, requestOptions);
         }
       }
diff --git a/dio/lib/src/interceptor.dart b/dio/lib/src/interceptor.dart
index 3b3538464..785106062 100644
--- a/dio/lib/src/interceptor.dart
+++ b/dio/lib/src/interceptor.dart
@@ -302,13 +302,19 @@ class InterceptorsWrapper extends Interceptor with _InterceptorWrapperMixin {
 /// Interceptors are a queue, and you can add any number of interceptors,
 /// All interceptors will be executed in first in first out order.
 class Interceptors extends ListMixin<Interceptor> {
-  final _list = <Interceptor>[];
+  /// Define a nullable list to be capable with growable elements.
+  final List<Interceptor?> _list = [const ImplyContentTypeInterceptor()];
 
   @override
-  int length = 0;
+  int get length => _list.length;
 
   @override
-  Interceptor operator [](int index) => _list[index];
+  set length(int newLength) {
+    _list.length = newLength;
+  }
+
+  @override
+  Interceptor operator [](int index) => _list[index]!;
 
   @override
   void operator []=(int index, Interceptor value) {
@@ -318,6 +324,11 @@ class Interceptors extends ListMixin<Interceptor> {
       _list[index] = value;
     }
   }
+
+  /// Remove the default imply content type interceptor.
+  void removeImplyContentTypeInterceptor() {
+    _list.removeWhere((e) => e is ImplyContentTypeInterceptor);
+  }
 }
 
 class _InterceptorParams<T, V> {
diff --git a/dio/lib/src/interceptors/imply_content_type.dart b/dio/lib/src/interceptors/imply_content_type.dart
new file mode 100644
index 000000000..a0731e8ae
--- /dev/null
+++ b/dio/lib/src/interceptors/imply_content_type.dart
@@ -0,0 +1,47 @@
+import 'dart:developer' as dev;
+
+import '../dio_mixin.dart';
+import '../form_data.dart';
+import '../headers.dart';
+import '../options.dart';
+
+/// {@template dio.interceptors.ImplyContentTypeInterceptor}
+/// The default `content-type` for requests will be implied by the
+/// [ImplyContentTypeInterceptor] according to the type of the request payload.
+/// The interceptor can be removed by
+/// [Interceptors.removeImplyContentTypeInterceptor].
+/// {@endtemplate}
+class ImplyContentTypeInterceptor extends Interceptor {
+  const ImplyContentTypeInterceptor();
+
+  @override
+  void onRequest(
+    RequestOptions options,
+    RequestInterceptorHandler handler,
+  ) {
+    final Object? data = options.data;
+    if (data != null && options.contentType == null) {
+      final String? contentType;
+      if (data is FormData) {
+        contentType = Headers.multipartFormDataContentType;
+      } else if (data is Map || data is String) {
+        contentType = Headers.jsonContentType;
+      } else {
+        // Do not log in the release mode.
+        if (!bool.fromEnvironment('dart.vm.product')) {
+          dev.log(
+            '${data.runtimeType} cannot be used'
+            'to imply a default content-type, '
+            'please set a proper content-type in the request.',
+            level: 900,
+            name: '🔔 Dio',
+            stackTrace: StackTrace.current,
+          );
+        }
+        contentType = null;
+      }
+      options.contentType = contentType;
+    }
+    handler.next(options);
+  }
+}
diff --git a/dio/lib/src/options.dart b/dio/lib/src/options.dart
index 815296df7..667334efe 100644
--- a/dio/lib/src/options.dart
+++ b/dio/lib/src/options.dart
@@ -377,6 +377,8 @@ class Options {
   /// The request Content-Type.
   ///
   /// [Dio] will automatically encode the request body accordingly.
+  ///
+  /// {@macro dio.interceptors.ImplyContentTypeInterceptor}
   String? contentType;
 
   /// [responseType] indicates the type of data that the server will respond with
@@ -703,6 +705,8 @@ class _RequestConfig {
   /// The request Content-Type.
   ///
   /// [Dio] will automatically encode the request body accordingly.
+  ///
+  /// {@macro dio.interceptors.ImplyContentTypeInterceptor}
   String? get contentType => _headers[Headers.contentTypeHeader] as String?;
 
   set contentType(String? contentType) {
diff --git a/dio/migration_guide.md b/dio/migration_guide.md
index 665ffe9d1..481594899 100644
--- a/dio/migration_guide.md
+++ b/dio/migration_guide.md
@@ -17,7 +17,6 @@ When new content need to be added to the migration guide, make sure they're foll
 
 ### Summary
 
-- All request will not imply the default content type by default, leave to users to handle.
 - `get` and `getUri` in `Dio` has different signature.
 - `DefaultHttpClientAdapter` is now named `IOHttpClientAdapter`,
   and the platform independent adapter can be initiated by `HttpClientAdapter()` which is a factory method.
@@ -31,46 +30,6 @@ When new content need to be added to the migration guide, make sure they're foll
 
 ### Details
 
-#### Set `content-type` manually
-
-Now you'll need to specify the content type in the `BaseOptions`
-or in every requests' `Options` or headers. To do so:
-
-- In `BaseOptions`:
-  ```dart
-  dio.options.contentType = Headers.jsonContentType;
-  ```
-- In `Options`:
-  ```dart
-  dio.get('some/path', options: Options(contentType: Headers.jsonContentType));
-  ```
-- In headers:
-  ```dart
-  dio.get(
-    'some/path',
-    options: Options(
-      headers: {Headers.contentTypeHeader: Headers.jsonContentType},
-    ),
-  );
-  ```
-
-If you have your own request method that wraps `dio`:
-```dart
-void request(
-  Uri uri, {
-  Map<String, String?>? queryParameters,
-  Object? body,
-  Map<String, dynamic>? headers,
-  Options? options,
-  ResponseType? responseType = ResponseType.json,
-}) {
-  if (body != null) {
-    // If you have `content-type` in `headers`, it will be use first.
-    options.contentType ??= Headers.jsonContentType;
-  }
-}
-```
-
 #### `get` and `getUri`
 
 ```diff


Test patch:
diff --git a/dio/test/interceptor_test.dart b/dio/test/interceptor_test.dart
index 2f18826ff..549db784f 100644
--- a/dio/test/interceptor_test.dart
+++ b/dio/test/interceptor_test.dart
@@ -1,6 +1,7 @@
 import 'dart:async';
 
 import 'package:dio/dio.dart';
+import 'package:dio/src/interceptors/imply_content_type.dart';
 import 'package:test/test.dart';
 
 import 'mock/adapters.dart';
@@ -300,6 +301,14 @@ void main() {
       response = await dio.get('/test?tag=1');
       expect(response.data['errCode'], 0);
     });
+
+    test('ImplyContentTypeInterceptor', () async {
+      final dio = Dio();
+      expect(dio.interceptors, isNotEmpty);
+      expect(dio.interceptors.first, isA<ImplyContentTypeInterceptor>());
+      dio.interceptors.removeImplyContentTypeInterceptor();
+      expect(dio.interceptors, isEmpty);
+    });
   });
 
   group('response interceptor', () {
@@ -505,4 +514,16 @@ void main() {
       expect(result, 3);
     });
   });
+
+  test('Size of Interceptors', () {
+    final interceptors = Dio().interceptors;
+    expect(interceptors.length, equals(1));
+    expect(interceptors, isNotEmpty);
+    interceptors.add(InterceptorsWrapper());
+    expect(interceptors.length, equals(2));
+    expect(interceptors, isNotEmpty);
+    interceptors.clear();
+    expect(interceptors.length, equals(0));
+    expect(interceptors, isEmpty);
+  });
 }
diff --git a/dio/test/options_test.dart b/dio/test/options_test.dart
index 621d64d48..60610e2be 100644
--- a/dio/test/options_test.dart
+++ b/dio/test/options_test.dart
@@ -251,7 +251,7 @@ void main() {
     final r4 = await dio.post('', data: '');
     expect(
       r4.requestOptions.headers[Headers.contentTypeHeader],
-      null,
+      Headers.jsonContentType,
     );
 
     final r5 = await dio.get(