diff --git a/dio/CHANGELOG.md b/dio/CHANGELOG.md index f245beca8..bf791d6fe 100644 --- a/dio/CHANGELOG.md +++ b/dio/CHANGELOG.md @@ -6,6 +6,7 @@ See the [Migration Guide][] for the complete breaking changes list.** ## Unreleased - Remove `http` from `dev_dependencies`. +- Only produce null response body when `ResponseType.json`. ## 5.2.1+1 diff --git a/dio/lib/src/dio_mixin.dart b/dio/lib/src/dio_mixin.dart index 93966a199..fdc270127 100644 --- a/dio/lib/src/dio_mixin.dart +++ b/dio/lib/src/dio_mixin.dart @@ -537,10 +537,9 @@ abstract class DioMixin implements Dio { // Initiate Http requests Future> _dispatchRequest(RequestOptions reqOpt) async { final cancelToken = reqOpt.cancelToken; - ResponseBody responseBody; try { final stream = await _transformData(reqOpt); - responseBody = await httpClientAdapter.fetch( + final responseBody = await httpClientAdapter.fetch( reqOpt, stream, cancelToken?.whenCancel, @@ -559,7 +558,19 @@ abstract class DioMixin implements Dio { ); final statusOk = reqOpt.validateStatus(responseBody.statusCode); if (statusOk || reqOpt.receiveDataWhenStatusError == true) { - ret.data = await transformer.transformResponse(reqOpt, responseBody); + Object? data = await transformer.transformResponse( + reqOpt, + responseBody, + ); + // Make the response as null before returned as JSON. + if (data is String && + data.isEmpty && + T != dynamic && + T != String && + reqOpt.responseType == ResponseType.json) { + data = null; + } + ret.data = data; } else { await responseBody.stream.listen(null).cancel(); } diff --git a/dio/lib/src/transformer.dart b/dio/lib/src/transformer.dart index f876fbaff..e915a0e8c 100644 --- a/dio/lib/src/transformer.dart +++ b/dio/lib/src/transformer.dart @@ -22,9 +22,10 @@ abstract class Transformer { /// [transformResponse] allows changes to the response data before /// it is passed to [ResponseInterceptor]. /// - /// **Note**: As an agreement, you must return the [response] + /// **Note**: As an agreement, you must return the [responseBody] /// when the Options.responseType is [ResponseType.stream]. - Future transformResponse(RequestOptions options, ResponseBody response); + // TODO(AlexV525): Add generic type for the method in v6.0.0. + Future transformResponse(RequestOptions options, ResponseBody responseBody); /// Deep encode the [Map] to percent-encoding. /// It is mostly used with the "application/x-www-form-urlencoded" content-type. diff --git a/dio/lib/src/transformers/sync_transformer.dart b/dio/lib/src/transformers/sync_transformer.dart index 725a893f6..8fa5a4b14 100644 --- a/dio/lib/src/transformers/sync_transformer.dart +++ b/dio/lib/src/transformers/sync_transformer.dart @@ -42,86 +42,97 @@ class SyncTransformer extends Transformer { } } - /// As an agreement, we return the [response] when the - /// Options.responseType is [ResponseType.stream]. @override Future transformResponse( RequestOptions options, - ResponseBody response, + ResponseBody responseBody, ) async { + final responseType = options.responseType; + // Do not handled the body for streams. if (options.responseType == ResponseType.stream) { - return response; + return responseBody; } - int length = 0; - int received = 0; + final showDownloadProgress = options.onReceiveProgress != null; + final int totalLength; if (showDownloadProgress) { - length = int.parse( - response.headers[Headers.contentLengthHeader]?.first ?? '-1', + totalLength = int.parse( + responseBody.headers[Headers.contentLengthHeader]?.first ?? '-1', ); + } else { + totalLength = 0; } - final completer = Completer(); - final stream = response.stream.transform( + + int received = 0; + final stream = responseBody.stream.transform( StreamTransformer.fromHandlers( handleData: (data, sink) { sink.add(data); if (showDownloadProgress) { received += data.length; - options.onReceiveProgress?.call(received, length); + options.onReceiveProgress?.call(received, totalLength); } }, ), ); + + final streamCompleter = Completer(); + int finalLength = 0; // Keep references to the data chunks and concatenate them later. final chunks = []; - int finalSize = 0; - final StreamSubscription subscription = stream.listen( + final subscription = stream.listen( (chunk) { - finalSize += chunk.length; + finalLength += chunk.length; chunks.add(chunk); }, onError: (Object error, StackTrace stackTrace) { - completer.completeError(error, stackTrace); + streamCompleter.completeError(error, stackTrace); + }, + onDone: () { + streamCompleter.complete(); }, - onDone: () => completer.complete(), cancelOnError: true, ); options.cancelToken?.whenCancel.then((_) { return subscription.cancel(); }); - await completer.future; - // Copy all chunks into a final Uint8List. - final responseBytes = Uint8List(finalSize); + await streamCompleter.future; + + // Copy all chunks into the final bytes. + final responseBytes = Uint8List(finalLength); int chunkOffset = 0; for (final chunk in chunks) { responseBytes.setAll(chunkOffset, chunk); chunkOffset += chunk.length; } - if (options.responseType == ResponseType.bytes) { + // Return the finalized bytes if the response type is bytes. + if (responseType == ResponseType.bytes) { return responseBytes; } - final String? responseBody; + final isJsonContent = Transformer.isJsonMimeType( + responseBody.headers[Headers.contentTypeHeader]?.first, + ); + final String? response; if (options.responseDecoder != null) { - responseBody = options.responseDecoder!( + response = options.responseDecoder!( responseBytes, options, - response..stream = Stream.empty(), + responseBody..stream = Stream.empty(), ); - } else if (responseBytes.isNotEmpty) { - responseBody = utf8.decode(responseBytes, allowMalformed: true); + } else if (!isJsonContent || responseBytes.isNotEmpty) { + response = utf8.decode(responseBytes, allowMalformed: true); } else { - responseBody = null; + response = null; } - if (responseBody != null && - responseBody.isNotEmpty && - options.responseType == ResponseType.json && - Transformer.isJsonMimeType( - response.headers[Headers.contentTypeHeader]?.first, - )) { - return jsonDecodeCallback(responseBody); + + if (response != null && + response.isNotEmpty && + responseType == ResponseType.json && + isJsonContent) { + return jsonDecodeCallback(response); } - return responseBody; + return response; } } diff --git a/dio/test/mock/http_mock.mocks.dart b/dio/test/mock/http_mock.mocks.dart index 02a40644b..c1af20389 100644 --- a/dio/test/mock/http_mock.mocks.dart +++ b/dio/test/mock/http_mock.mocks.dart @@ -672,8 +672,8 @@ class MockTransformer extends _i1.Mock implements _i5.Transformer { returnValue: Future.value('')) as _i4.Future); @override _i4.Future transformResponse( - _i6.RequestOptions? options, _i7.ResponseBody? response) => + _i6.RequestOptions? options, _i7.ResponseBody? responseBody) => (super.noSuchMethod( - Invocation.method(#transformResponse, [options, response]), + Invocation.method(#transformResponse, [options, responseBody]), returnValue: Future.value()) as _i4.Future); } diff --git a/dio/test/options_test.dart b/dio/test/options_test.dart index 7f18566e6..25a1df293 100644 --- a/dio/test/options_test.dart +++ b/dio/test/options_test.dart @@ -346,6 +346,14 @@ void main() { r10.requestOptions.contentType, startsWith(Headers.multipartFormDataContentType), ); + + // Regression: https://github.com/cfug/dio/issues/1834 + final r11 = await dio.get(''); + expect(r11.data, ''); + final r12 = await dio.get(''); + expect(r12.data, null); + final r13 = await dio.get>(''); + expect(r13.data, null); }); test('default content-type 2', () async { diff --git a/dio/test/transformer_test.dart b/dio/test/transformer_test.dart new file mode 100644 index 000000000..5b8381162 --- /dev/null +++ b/dio/test/transformer_test.dart @@ -0,0 +1,52 @@ +import 'package:dio/dio.dart'; +import 'package:test/test.dart'; + +void main() { + group(BackgroundTransformer(), () { + test('transformResponse transforms the request', () async { + final transformer = BackgroundTransformer(); + final response = await transformer.transformResponse( + RequestOptions(responseType: ResponseType.json), + ResponseBody.fromString( + '{"foo": "bar"}', + 200, + headers: { + Headers.contentTypeHeader: ['application/json'], + }, + ), + ); + expect(response, {'foo': 'bar'}); + }); + }); + + // Regression: https://github.com/cfug/dio/issues/1834 + test('null response body only when the response is JSON', () async { + final transformer = BackgroundTransformer(); + final r1 = await transformer.transformResponse( + RequestOptions(responseType: ResponseType.json), + ResponseBody.fromBytes([], 200), + ); + expect(r1, ''); + final r2 = await transformer.transformResponse( + RequestOptions(responseType: ResponseType.bytes), + ResponseBody.fromBytes([], 200), + ); + expect(r2, []); + final r3 = await transformer.transformResponse( + RequestOptions(responseType: ResponseType.plain), + ResponseBody.fromBytes([], 200), + ); + expect(r3, ''); + final r4 = await transformer.transformResponse( + RequestOptions(responseType: ResponseType.json), + ResponseBody.fromBytes( + [], + 200, + headers: { + Headers.contentTypeHeader: [Headers.jsonContentType], + }, + ), + ); + expect(r4, null); + }); +} diff --git a/dio/test/transformers/background_transformer_test.dart b/dio/test/transformers/background_transformer_test.dart deleted file mode 100644 index c4f08cf1a..000000000 --- a/dio/test/transformers/background_transformer_test.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:test/test.dart'; - -void main() { - test('transformResponse transforms the request', () async { - final transformer = BackgroundTransformer(); - - final response = await transformer.transformResponse( - RequestOptions(responseType: ResponseType.json), - ResponseBody.fromString( - '{"foo": "bar"}', - 200, - headers: { - Headers.contentTypeHeader: ['application/json'], - }, - ), - ); - - expect(response, {'foo': 'bar'}); - }); -} diff --git a/example/lib/transformer.dart b/example/lib/transformer.dart index 6ca955937..1673e251c 100644 --- a/example/lib/transformer.dart +++ b/example/lib/transformer.dart @@ -26,10 +26,10 @@ class MyTransformer extends BackgroundTransformer { @override Future transformResponse( RequestOptions options, - ResponseBody response, + ResponseBody responseBody, ) async { options.extra['self'] = 'XX'; - return super.transformResponse(options, response); + return super.transformResponse(options, responseBody); } }