Repository: cfug/dio (dart)

Issue:
`Transformer.isJsonMimeType` throws format exceptions without caught
### Package

dio

### Version

5.4.3+1

### Operating-System

Android, iOS, Web, MacOS, Linux, Windows

### Adapter

Default Dio

### Output of `flutter doctor -v`

```shell
[✓] Flutter (Channel stable, 3.19.6, on macOS 14.5 23F79 darwin-arm64 (Rosetta), locale zh-Hans-CN)
    • Flutter version 3.19.6 on channel stable at /Users/alex/dev/flutter-stable
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 54e66469a9 (10 weeks ago), 2024-04-17 13:08:03 -0700
    • Engine revision c4cd48e186
    • Dart version 3.3.4
    • DevTools version 2.31.1
```


### Dart Version

Dart 3.3.4

### Steps to Reproduce

```dart
dio.get('https://nhrl6-qyaaa-aaaao-ae6kq-cai.raw.ic0.app/file/859.png');
```

### Expected Result

Returns response without exceptions.

### Actual Result

```console
Error: Error on line 1, column 5: Invalid media type: expected "/".
  ╷
1 │ .png;charset=utf-8
  │     ^
  ╵
```

Pull request description:
Resolve #2256

### 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
- [x] I have updated the `CHANGELOG.md` in the corresponding package

Patch:
diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md
index ec6f7da73..4243e91a3 100644
--- a/dio/CHANGELOG.md
+++ b/dio/CHANGELOG.md
@@ -15,6 +15,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
 - Improves `InterceptorState.toString()`.
 - If the `CancelToken` got canceled before making requests,
   throws the exception directly rather than cut actual HTTP requests afterward.
+- Catch `MediaType` parse exception in `Transformer.isJsonMimeType`.
 
 ## 5.4.3+1
 
diff --git a/dio/lib/src/transformer.dart b/dio/lib/src/transformer.dart
index 0750146c0..b167c12d0 100644
--- a/dio/lib/src/transformer.dart
+++ b/dio/lib/src/transformer.dart
@@ -78,10 +78,19 @@ abstract class Transformer {
     if (contentType == null) {
       return false;
     }
-    final mediaType = MediaType.parse(contentType);
-    return mediaType.mimeType == 'application/json' ||
-        mediaType.mimeType == 'text/json' ||
-        mediaType.subtype.endsWith('+json');
+    try {
+      final mediaType = MediaType.parse(contentType);
+      return mediaType.mimeType == 'application/json' ||
+          mediaType.mimeType == 'text/json' ||
+          mediaType.subtype.endsWith('+json');
+    } catch (e, s) {
+      debugLog(
+        'Failed to parse the media type: $contentType, '
+        'thus it is not a JSON MIME type.',
+        s,
+      );
+      return false;
+    }
   }
 
   static FutureOr<String> defaultTransformRequest(


Test patch:
diff --git a/dio/test/transformer_test.dart b/dio/test/transformer_test.dart
index bafb7ef5c..0175531f7 100644
--- a/dio/test/transformer_test.dart
+++ b/dio/test/transformer_test.dart
@@ -6,6 +6,17 @@ import 'package:dio/src/transformers/util/consolidate_bytes.dart';
 import 'package:test/test.dart';
 
 void main() {
+  // Regression: https://github.com/cfug/dio/issues/2256
+  test('Transformer.isJsonMimeType', () {
+    expect(Transformer.isJsonMimeType('application/json'), isTrue);
+    expect(Transformer.isJsonMimeType('application/json;charset=utf8'), isTrue);
+    expect(Transformer.isJsonMimeType('text/json'), isTrue);
+    expect(Transformer.isJsonMimeType('image/jpg'), isFalse);
+    expect(Transformer.isJsonMimeType('image/png'), isFalse);
+    expect(Transformer.isJsonMimeType('.png'), isFalse);
+    expect(Transformer.isJsonMimeType('.png;charset=utf-8'), isFalse);
+  });
+
   group(BackgroundTransformer(), () {
     test('transformResponse transforms the request', () async {
       final transformer = BackgroundTransformer();
@@ -59,240 +70,236 @@ void main() {
     expect(jsonResponse, null);
   });
 
-  group(
-    FusedTransformer(),
-    () {
-      test(
-          'transformResponse transforms json without content-length set in response',
-          () async {
-        final transformer = FusedTransformer();
-        final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.json),
-          ResponseBody.fromString(
-            '{"foo": "bar"}',
-            200,
-            headers: {
-              Headers.contentTypeHeader: ['application/json'],
-            },
-          ),
-        );
-        expect(response, {'foo': 'bar'});
-      });
+  group(FusedTransformer, () {
+    test(
+        'transformResponse transforms json without content-length set in response',
+        () async {
+      final transformer = FusedTransformer();
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.json),
+        ResponseBody.fromString(
+          '{"foo": "bar"}',
+          200,
+          headers: {
+            Headers.contentTypeHeader: ['application/json'],
+          },
+        ),
+      );
+      expect(response, {'foo': 'bar'});
+    });
 
-      test('transformResponse transforms json with content-length', () async {
-        final transformer = FusedTransformer();
-        const jsonString = '{"foo": "bar"}';
-        final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.json),
-          ResponseBody.fromString(
-            jsonString,
-            200,
-            headers: {
-              Headers.contentTypeHeader: ['application/json'],
-              Headers.contentLengthHeader: [
-                utf8.encode(jsonString).length.toString(),
-              ],
-            },
-          ),
-        );
-        expect(response, {'foo': 'bar'});
-      });
+    test('transformResponse transforms json with content-length', () async {
+      final transformer = FusedTransformer();
+      const jsonString = '{"foo": "bar"}';
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.json),
+        ResponseBody.fromString(
+          jsonString,
+          200,
+          headers: {
+            Headers.contentTypeHeader: ['application/json'],
+            Headers.contentLengthHeader: [
+              utf8.encode(jsonString).length.toString(),
+            ],
+          },
+        ),
+      );
+      expect(response, {'foo': 'bar'});
+    });
 
-      test('transforms json in background isolate', () async {
-        final transformer = FusedTransformer(contentLengthIsolateThreshold: 0);
-        final jsonString = '{"foo": "bar"}';
-        final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.json),
-          ResponseBody.fromString(
-            jsonString,
-            200,
-            headers: {
-              Headers.contentTypeHeader: ['application/json'],
-              Headers.contentLengthHeader: [
-                utf8.encode(jsonString).length.toString(),
-              ],
-            },
-          ),
-        );
-        expect(response, {'foo': 'bar'});
-      });
+    test('transforms json in background isolate', () async {
+      final transformer = FusedTransformer(contentLengthIsolateThreshold: 0);
+      final jsonString = '{"foo": "bar"}';
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.json),
+        ResponseBody.fromString(
+          jsonString,
+          200,
+          headers: {
+            Headers.contentTypeHeader: ['application/json'],
+            Headers.contentLengthHeader: [
+              utf8.encode(jsonString).length.toString(),
+            ],
+          },
+        ),
+      );
+      expect(response, {'foo': 'bar'});
+    });
 
-      test('transformResponse transforms that arrives in many chunks',
-          () async {
-        final transformer = FusedTransformer();
-        final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.json),
-          ResponseBody(
-            Stream.fromIterable(
-                /* utf-8 encoding of {"foo": "bar"} */
-                [
-                  Uint8List.fromList([123]),
-                  Uint8List.fromList([34]),
-                  Uint8List.fromList([102, 111, 111]),
-                  Uint8List.fromList([34]),
-                  Uint8List.fromList([58]),
-                  Uint8List.fromList([34]),
-                  Uint8List.fromList([98, 97, 114]),
-                  Uint8List.fromList([34]),
-                  Uint8List.fromList([125]),
-                ]),
-            200,
-            headers: {
-              Headers.contentTypeHeader: ['application/json'],
-            },
-          ),
-        );
-        expect(response, {'foo': 'bar'});
-      });
+    test('transformResponse transforms that arrives in many chunks', () async {
+      final transformer = FusedTransformer();
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.json),
+        ResponseBody(
+          Stream.fromIterable(
+              /* utf-8 encoding of {"foo": "bar"} */
+              [
+                Uint8List.fromList([123]),
+                Uint8List.fromList([34]),
+                Uint8List.fromList([102, 111, 111]),
+                Uint8List.fromList([34]),
+                Uint8List.fromList([58]),
+                Uint8List.fromList([34]),
+                Uint8List.fromList([98, 97, 114]),
+                Uint8List.fromList([34]),
+                Uint8List.fromList([125]),
+              ]),
+          200,
+          headers: {
+            Headers.contentTypeHeader: ['application/json'],
+          },
+        ),
+      );
+      expect(response, {'foo': 'bar'});
+    });
 
-      test('transformResponse handles bytes', () async {
-        final transformer = FusedTransformer();
-        final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.bytes),
-          ResponseBody.fromBytes(
-            [1, 2, 3],
-            200,
-          ),
-        );
-        expect(response, [1, 2, 3]);
-      });
+    test('transformResponse handles bytes', () async {
+      final transformer = FusedTransformer();
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.bytes),
+        ResponseBody.fromBytes(
+          [1, 2, 3],
+          200,
+        ),
+      );
+      expect(response, [1, 2, 3]);
+    });
 
-      test('transformResponse handles when response stream has multiple chunks',
-          () async {
-        final transformer = FusedTransformer();
-        final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.bytes),
-          ResponseBody(
-            Stream.fromIterable([
-              Uint8List.fromList([1, 2, 3]),
-              Uint8List.fromList([4, 5, 6]),
-              Uint8List.fromList([7, 8, 9]),
-            ]),
-            200,
-          ),
-        );
-        expect(response, [1, 2, 3, 4, 5, 6, 7, 8, 9]);
-      });
+    test('transformResponse handles when response stream has multiple chunks',
+        () async {
+      final transformer = FusedTransformer();
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.bytes),
+        ResponseBody(
+          Stream.fromIterable([
+            Uint8List.fromList([1, 2, 3]),
+            Uint8List.fromList([4, 5, 6]),
+            Uint8List.fromList([7, 8, 9]),
+          ]),
+          200,
+        ),
+      );
+      expect(response, [1, 2, 3, 4, 5, 6, 7, 8, 9]);
+    });
 
-      test('transformResponse handles plain text', () async {
-        final transformer = FusedTransformer();
-        final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.plain),
-          ResponseBody.fromString(
-            'plain text',
-            200,
-            headers: {
-              Headers.contentTypeHeader: ['text/plain'],
-            },
-          ),
-        );
-        expect(response, 'plain text');
-      });
+    test('transformResponse handles plain text', () async {
+      final transformer = FusedTransformer();
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.plain),
+        ResponseBody.fromString(
+          'plain text',
+          200,
+          headers: {
+            Headers.contentTypeHeader: ['text/plain'],
+          },
+        ),
+      );
+      expect(response, 'plain text');
+    });
 
-      test('ResponseType.plain takes precedence over content-type', () async {
-        final transformer = FusedTransformer();
-        final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.plain),
-          ResponseBody.fromString(
-            '{"text": "plain text"}',
-            200,
-            headers: {
-              Headers.contentTypeHeader: ['application/json'],
-            },
-          ),
-        );
-        expect(response, '{"text": "plain text"}');
-      });
+    test('ResponseType.plain takes precedence over content-type', () async {
+      final transformer = FusedTransformer();
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.plain),
+        ResponseBody.fromString(
+          '{"text": "plain text"}',
+          200,
+          headers: {
+            Headers.contentTypeHeader: ['application/json'],
+          },
+        ),
+      );
+      expect(response, '{"text": "plain text"}');
+    });
 
-      test('transformResponse handles streams', () async {
-        final transformer = FusedTransformer();
+    test('transformResponse handles streams', () async {
+      final transformer = FusedTransformer();
+      final response = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.stream),
+        ResponseBody.fromBytes(
+          [1, 2, 3],
+          200,
+        ),
+      );
+      expect(response, isA<ResponseBody>());
+    });
+
+    test('null response body only when the response is JSON', () async {
+      final transformer = FusedTransformer();
+      for (final responseType in ResponseType.values) {
         final response = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.stream),
-          ResponseBody.fromBytes(
-            [1, 2, 3],
-            200,
-          ),
+          RequestOptions(responseType: responseType),
+          ResponseBody.fromBytes([], 200),
         );
-        expect(response, isA<ResponseBody>());
-      });
-
-      test('null response body only when the response is JSON', () async {
-        final transformer = FusedTransformer();
-        for (final responseType in ResponseType.values) {
-          final response = await transformer.transformResponse(
-            RequestOptions(responseType: responseType),
-            ResponseBody.fromBytes([], 200),
-          );
-          switch (responseType) {
-            case ResponseType.json:
-            case ResponseType.plain:
-              expect(response, '');
-              break;
-            case ResponseType.stream:
-              expect(response, isA<ResponseBody>());
-              break;
-            case ResponseType.bytes:
-              expect(response, []);
-              break;
-            default:
-              throw AssertionError('Unknown response type: $responseType');
-          }
+        switch (responseType) {
+          case ResponseType.json:
+          case ResponseType.plain:
+            expect(response, '');
+            break;
+          case ResponseType.stream:
+            expect(response, isA<ResponseBody>());
+            break;
+          case ResponseType.bytes:
+            expect(response, []);
+            break;
+          default:
+            throw AssertionError('Unknown response type: $responseType');
         }
-        final jsonResponse = await transformer.transformResponse(
-          RequestOptions(responseType: ResponseType.json),
-          ResponseBody.fromBytes(
-            [],
-            200,
-            headers: {
-              Headers.contentTypeHeader: [Headers.jsonContentType],
-            },
-          ),
-        );
-        expect(jsonResponse, null);
-      });
+      }
+      final jsonResponse = await transformer.transformResponse(
+        RequestOptions(responseType: ResponseType.json),
+        ResponseBody.fromBytes(
+          [],
+          200,
+          headers: {
+            Headers.contentTypeHeader: [Headers.jsonContentType],
+          },
+        ),
+      );
+      expect(jsonResponse, null);
+    });
 
-      test('transform the request using urlencode', () async {
-        final transformer = FusedTransformer();
+    test('transform the request using urlencode', () async {
+      final transformer = FusedTransformer();
 
-        final request = await transformer.transformRequest(
-          RequestOptions(responseType: ResponseType.json, data: {'foo': 'bar'}),
-        );
-        expect(request, 'foo=bar');
-      });
+      final request = await transformer.transformRequest(
+        RequestOptions(responseType: ResponseType.json, data: {'foo': 'bar'}),
+      );
+      expect(request, 'foo=bar');
+    });
 
-      test('transform the request using json', () async {
-        final transformer = FusedTransformer();
+    test('transform the request using json', () async {
+      final transformer = FusedTransformer();
 
-        final request = await transformer.transformRequest(
-          RequestOptions(
-            responseType: ResponseType.json,
-            data: {'foo': 'bar'},
-            headers: {'Content-Type': 'application/json'},
+      final request = await transformer.transformRequest(
+        RequestOptions(
+          responseType: ResponseType.json,
+          data: {'foo': 'bar'},
+          headers: {'Content-Type': 'application/json'},
+        ),
+      );
+      expect(request, '{"foo":"bar"}');
+    });
+
+    test(
+      'HEAD request with content-length but empty body should not return null',
+      () async {
+        final transformer = FusedTransformer();
+        final response = await transformer.transformResponse(
+          RequestOptions(responseType: ResponseType.json, method: 'HEAD'),
+          ResponseBody(
+            Stream.value(Uint8List(0)),
+            200,
+            headers: {
+              Headers.contentTypeHeader: ['application/json'],
+              Headers.contentLengthHeader: ['123'],
+            },
           ),
         );
-        expect(request, '{"foo":"bar"}');
-      });
-
-      test(
-        'HEAD request with content-length but empty body should not return null',
-        () async {
-          final transformer = FusedTransformer();
-          final response = await transformer.transformResponse(
-            RequestOptions(responseType: ResponseType.json, method: 'HEAD'),
-            ResponseBody(
-              Stream.value(Uint8List(0)),
-              200,
-              headers: {
-                Headers.contentTypeHeader: ['application/json'],
-                Headers.contentLengthHeader: ['123'],
-              },
-            ),
-          );
-          expect(response, null);
-        },
-      );
-    },
-  );
+        expect(response, null);
+      },
+    );
+  });
 
   group('consolidate bytes', () {
     test('consolidates bytes from a stream', () async {