Skip to content

Commit c301ffe

Browse files
authored
πŸ› Only produce null response body when the response is a JSON type (#1874)
Resolves #1834. ### 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
1 parent ec88e89 commit c301ffe

9 files changed

+127
-64
lines changed

β€Ždio/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ See the [Migration Guide][] for the complete breaking changes list.**
66
## Unreleased
77

88
- Remove `http` from `dev_dependencies`.
9+
- Only produce null response body when `ResponseType.json`.
910

1011
## 5.2.1+1
1112

β€Ždio/lib/src/dio_mixin.dart

+14-3
Original file line numberDiff line numberDiff line change
@@ -537,10 +537,9 @@ abstract class DioMixin implements Dio {
537537
// Initiate Http requests
538538
Future<Response<dynamic>> _dispatchRequest<T>(RequestOptions reqOpt) async {
539539
final cancelToken = reqOpt.cancelToken;
540-
ResponseBody responseBody;
541540
try {
542541
final stream = await _transformData(reqOpt);
543-
responseBody = await httpClientAdapter.fetch(
542+
final responseBody = await httpClientAdapter.fetch(
544543
reqOpt,
545544
stream,
546545
cancelToken?.whenCancel,
@@ -559,7 +558,19 @@ abstract class DioMixin implements Dio {
559558
);
560559
final statusOk = reqOpt.validateStatus(responseBody.statusCode);
561560
if (statusOk || reqOpt.receiveDataWhenStatusError == true) {
562-
ret.data = await transformer.transformResponse(reqOpt, responseBody);
561+
Object? data = await transformer.transformResponse(
562+
reqOpt,
563+
responseBody,
564+
);
565+
// Make the response as null before returned as JSON.
566+
if (data is String &&
567+
data.isEmpty &&
568+
T != dynamic &&
569+
T != String &&
570+
reqOpt.responseType == ResponseType.json) {
571+
data = null;
572+
}
573+
ret.data = data;
563574
} else {
564575
await responseBody.stream.listen(null).cancel();
565576
}

β€Ždio/lib/src/transformer.dart

+3-2
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ abstract class Transformer {
2222
/// [transformResponse] allows changes to the response data before
2323
/// it is passed to [ResponseInterceptor].
2424
///
25-
/// **Note**: As an agreement, you must return the [response]
25+
/// **Note**: As an agreement, you must return the [responseBody]
2626
/// when the Options.responseType is [ResponseType.stream].
27-
Future transformResponse(RequestOptions options, ResponseBody response);
27+
// TODO(AlexV525): Add generic type for the method in v6.0.0.
28+
Future transformResponse(RequestOptions options, ResponseBody responseBody);
2829

2930
/// Deep encode the [Map<String, dynamic>] to percent-encoding.
3031
/// It is mostly used with the "application/x-www-form-urlencoded" content-type.

β€Ždio/lib/src/transformers/sync_transformer.dart

+45-34
Original file line numberDiff line numberDiff line change
@@ -42,86 +42,97 @@ class SyncTransformer extends Transformer {
4242
}
4343
}
4444

45-
/// As an agreement, we return the [response] when the
46-
/// Options.responseType is [ResponseType.stream].
4745
@override
4846
Future<dynamic> transformResponse(
4947
RequestOptions options,
50-
ResponseBody response,
48+
ResponseBody responseBody,
5149
) async {
50+
final responseType = options.responseType;
51+
// Do not handled the body for streams.
5252
if (options.responseType == ResponseType.stream) {
53-
return response;
53+
return responseBody;
5454
}
55-
int length = 0;
56-
int received = 0;
55+
5756
final showDownloadProgress = options.onReceiveProgress != null;
57+
final int totalLength;
5858
if (showDownloadProgress) {
59-
length = int.parse(
60-
response.headers[Headers.contentLengthHeader]?.first ?? '-1',
59+
totalLength = int.parse(
60+
responseBody.headers[Headers.contentLengthHeader]?.first ?? '-1',
6161
);
62+
} else {
63+
totalLength = 0;
6264
}
63-
final completer = Completer();
64-
final stream = response.stream.transform<Uint8List>(
65+
66+
int received = 0;
67+
final stream = responseBody.stream.transform<Uint8List>(
6568
StreamTransformer.fromHandlers(
6669
handleData: (data, sink) {
6770
sink.add(data);
6871
if (showDownloadProgress) {
6972
received += data.length;
70-
options.onReceiveProgress?.call(received, length);
73+
options.onReceiveProgress?.call(received, totalLength);
7174
}
7275
},
7376
),
7477
);
78+
79+
final streamCompleter = Completer<void>();
80+
int finalLength = 0;
7581
// Keep references to the data chunks and concatenate them later.
7682
final chunks = <Uint8List>[];
77-
int finalSize = 0;
78-
final StreamSubscription subscription = stream.listen(
83+
final subscription = stream.listen(
7984
(chunk) {
80-
finalSize += chunk.length;
85+
finalLength += chunk.length;
8186
chunks.add(chunk);
8287
},
8388
onError: (Object error, StackTrace stackTrace) {
84-
completer.completeError(error, stackTrace);
89+
streamCompleter.completeError(error, stackTrace);
90+
},
91+
onDone: () {
92+
streamCompleter.complete();
8593
},
86-
onDone: () => completer.complete(),
8794
cancelOnError: true,
8895
);
8996
options.cancelToken?.whenCancel.then((_) {
9097
return subscription.cancel();
9198
});
92-
await completer.future;
93-
// Copy all chunks into a final Uint8List.
94-
final responseBytes = Uint8List(finalSize);
99+
await streamCompleter.future;
100+
101+
// Copy all chunks into the final bytes.
102+
final responseBytes = Uint8List(finalLength);
95103
int chunkOffset = 0;
96104
for (final chunk in chunks) {
97105
responseBytes.setAll(chunkOffset, chunk);
98106
chunkOffset += chunk.length;
99107
}
100108

101-
if (options.responseType == ResponseType.bytes) {
109+
// Return the finalized bytes if the response type is bytes.
110+
if (responseType == ResponseType.bytes) {
102111
return responseBytes;
103112
}
104113

105-
final String? responseBody;
114+
final isJsonContent = Transformer.isJsonMimeType(
115+
responseBody.headers[Headers.contentTypeHeader]?.first,
116+
);
117+
final String? response;
106118
if (options.responseDecoder != null) {
107-
responseBody = options.responseDecoder!(
119+
response = options.responseDecoder!(
108120
responseBytes,
109121
options,
110-
response..stream = Stream.empty(),
122+
responseBody..stream = Stream.empty(),
111123
);
112-
} else if (responseBytes.isNotEmpty) {
113-
responseBody = utf8.decode(responseBytes, allowMalformed: true);
124+
} else if (!isJsonContent || responseBytes.isNotEmpty) {
125+
response = utf8.decode(responseBytes, allowMalformed: true);
114126
} else {
115-
responseBody = null;
127+
response = null;
116128
}
117-
if (responseBody != null &&
118-
responseBody.isNotEmpty &&
119-
options.responseType == ResponseType.json &&
120-
Transformer.isJsonMimeType(
121-
response.headers[Headers.contentTypeHeader]?.first,
122-
)) {
123-
return jsonDecodeCallback(responseBody);
129+
130+
if (response != null &&
131+
response.isNotEmpty &&
132+
responseType == ResponseType.json &&
133+
isJsonContent) {
134+
return jsonDecodeCallback(response);
124135
}
125-
return responseBody;
136+
return response;
126137
}
127138
}

β€Ždio/test/mock/http_mock.mocks.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -672,8 +672,8 @@ class MockTransformer extends _i1.Mock implements _i5.Transformer {
672672
returnValue: Future<String>.value('')) as _i4.Future<String>);
673673
@override
674674
_i4.Future<dynamic> transformResponse(
675-
_i6.RequestOptions? options, _i7.ResponseBody? response) =>
675+
_i6.RequestOptions? options, _i7.ResponseBody? responseBody) =>
676676
(super.noSuchMethod(
677-
Invocation.method(#transformResponse, [options, response]),
677+
Invocation.method(#transformResponse, [options, responseBody]),
678678
returnValue: Future<dynamic>.value()) as _i4.Future<dynamic>);
679679
}

β€Ždio/test/options_test.dart

+8
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,14 @@ void main() {
346346
r10.requestOptions.contentType,
347347
startsWith(Headers.multipartFormDataContentType),
348348
);
349+
350+
// Regression: https://github.com/cfug/dio/issues/1834
351+
final r11 = await dio.get('');
352+
expect(r11.data, '');
353+
final r12 = await dio.get<Map>('');
354+
expect(r12.data, null);
355+
final r13 = await dio.get<Map<String, Object>>('');
356+
expect(r13.data, null);
349357
});
350358

351359
test('default content-type 2', () async {

β€Ždio/test/transformer_test.dart

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import 'package:dio/dio.dart';
2+
import 'package:test/test.dart';
3+
4+
void main() {
5+
group(BackgroundTransformer(), () {
6+
test('transformResponse transforms the request', () async {
7+
final transformer = BackgroundTransformer();
8+
final response = await transformer.transformResponse(
9+
RequestOptions(responseType: ResponseType.json),
10+
ResponseBody.fromString(
11+
'{"foo": "bar"}',
12+
200,
13+
headers: {
14+
Headers.contentTypeHeader: ['application/json'],
15+
},
16+
),
17+
);
18+
expect(response, {'foo': 'bar'});
19+
});
20+
});
21+
22+
// Regression: https://github.com/cfug/dio/issues/1834
23+
test('null response body only when the response is JSON', () async {
24+
final transformer = BackgroundTransformer();
25+
final r1 = await transformer.transformResponse(
26+
RequestOptions(responseType: ResponseType.json),
27+
ResponseBody.fromBytes([], 200),
28+
);
29+
expect(r1, '');
30+
final r2 = await transformer.transformResponse(
31+
RequestOptions(responseType: ResponseType.bytes),
32+
ResponseBody.fromBytes([], 200),
33+
);
34+
expect(r2, []);
35+
final r3 = await transformer.transformResponse(
36+
RequestOptions(responseType: ResponseType.plain),
37+
ResponseBody.fromBytes([], 200),
38+
);
39+
expect(r3, '');
40+
final r4 = await transformer.transformResponse(
41+
RequestOptions(responseType: ResponseType.json),
42+
ResponseBody.fromBytes(
43+
[],
44+
200,
45+
headers: {
46+
Headers.contentTypeHeader: [Headers.jsonContentType],
47+
},
48+
),
49+
);
50+
expect(r4, null);
51+
});
52+
}

β€Ždio/test/transformers/background_transformer_test.dart

-21
This file was deleted.

β€Žexample/lib/transformer.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ class MyTransformer extends BackgroundTransformer {
2626
@override
2727
Future transformResponse(
2828
RequestOptions options,
29-
ResponseBody response,
29+
ResponseBody responseBody,
3030
) async {
3131
options.extra['self'] = 'XX';
32-
return super.transformResponse(options, response);
32+
return super.transformResponse(options, responseBody);
3333
}
3434
}
3535

0 commit comments

Comments
Β (0)