Line data Source code
1 : import 'dart:async'; 2 : import 'dart:convert'; 3 : import 'dart:typed_data'; 4 : 5 : import 'package:http_parser/http_parser.dart'; 6 : 7 : import 'adapter.dart'; 8 : import 'dio_error.dart'; 9 : import 'headers.dart'; 10 : import 'options.dart'; 11 : import 'utils.dart'; 12 : 13 : /// [Transformer] allows changes to the request/response data before 14 : /// it is sent/received to/from the server. 15 : /// 16 : /// Dio has already implemented a [DefaultTransformer], and as the default 17 : /// [Transformer]. If you want to custom the transformation of 18 : /// request/response data, you can provide a [Transformer] by your self, and 19 : /// replace the [DefaultTransformer] by setting the [dio.Transformer]. 20 : 21 : abstract class Transformer { 22 : /// `transformRequest` allows changes to the request data before it is 23 : /// sent to the server, but **after** the [RequestInterceptor]. 24 : /// 25 : /// This is only applicable for request methods 'PUT', 'POST', and 'PATCH' 26 : Future<String> transformRequest(RequestOptions options); 27 : 28 : /// `transformResponse` allows changes to the response data before 29 : /// it is passed to [ResponseInterceptor]. 30 : /// 31 : /// **Note**: As an agreement, you must return the [response] 32 : /// when the Options.responseType is [ResponseType.stream]. 33 : Future transformResponse(RequestOptions options, ResponseBody response); 34 : 35 : /// Deep encode the [Map<String, dynamic>] to percent-encoding. 36 : /// It is mostly used with the "application/x-www-form-urlencoded" content-type. 37 9 : static String urlEncodeMap( 38 : Map map, [ 39 : ListFormat listFormat = ListFormat.multi, 40 : ]) { 41 9 : return encodeMap( 42 : map, 43 2 : (key, value) { 44 : if (value == null) return key; 45 6 : return '$key=${Uri.encodeQueryComponent(value.toString())}'; 46 : }, 47 : listFormat: listFormat, 48 : ); 49 : } 50 : } 51 : 52 : /// The default [Transformer] for [Dio]. If you want to custom the transformation of 53 : /// request/response data, you can provide a [Transformer] by your self, and 54 : /// replace the [DefaultTransformer] by setting the [dio.Transformer]. 55 : 56 : typedef JsonDecodeCallback = dynamic Function(String); 57 : 58 : class DefaultTransformer extends Transformer { 59 8 : DefaultTransformer({this.jsonDecodeCallback}); 60 : 61 : JsonDecodeCallback? jsonDecodeCallback; 62 : 63 : @override 64 3 : Future<String> transformRequest(RequestOptions options) async { 65 3 : var data = options.data ?? ''; 66 3 : if (data is! String) { 67 2 : if (_isJsonMime(options.contentType)) { 68 2 : return json.encode(options.data); 69 0 : } else if (data is Map) { 70 0 : options.contentType = 71 0 : options.contentType ?? Headers.formUrlEncodedContentType; 72 0 : return Transformer.urlEncodeMap(data); 73 : } 74 : } 75 2 : return data.toString(); 76 : } 77 : 78 : /// As an agreement, we return the [response] when the 79 : /// Options.responseType is [ResponseType.stream]. 80 : @override 81 7 : Future transformResponse( 82 : RequestOptions options, ResponseBody response) async { 83 14 : if (options.responseType == ResponseType.stream) { 84 : return response; 85 : } 86 : var length = 0; 87 : var received = 0; 88 7 : var showDownloadProgress = options.onReceiveProgress != null; 89 : if (showDownloadProgress) { 90 1 : length = int.parse( 91 3 : response.headers[Headers.contentLengthHeader]?.first ?? '-1'); 92 : } 93 7 : var completer = Completer(); 94 : var stream = 95 21 : response.stream.transform<Uint8List>(StreamTransformer.fromHandlers( 96 6 : handleData: (data, sink) { 97 6 : sink.add(data); 98 : if (showDownloadProgress) { 99 2 : received += data.length; 100 1 : options.onReceiveProgress?.call(received, length); 101 : } 102 : }, 103 : )); 104 : // let's keep references to the data chunks and concatenate them later 105 7 : final chunks = <Uint8List>[]; 106 : var finalSize = 0; 107 7 : StreamSubscription subscription = stream.listen( 108 6 : (chunk) { 109 12 : finalSize += chunk.length; 110 6 : chunks.add(chunk); 111 : }, 112 0 : onError: (Object error, StackTrace stackTrace) { 113 0 : completer.completeError(error, stackTrace); 114 : }, 115 14 : onDone: () => completer.complete(), 116 : cancelOnError: true, 117 : ); 118 : // ignore: unawaited_futures 119 9 : options.cancelToken?.whenCancel.then((_) { 120 0 : return subscription.cancel(); 121 : }); 122 14 : if (options.receiveTimeout > 0) { 123 : try { 124 2 : await completer.future 125 3 : .timeout(Duration(milliseconds: options.receiveTimeout)); 126 0 : } on TimeoutException { 127 0 : await subscription.cancel(); 128 0 : throw DioError( 129 : requestOptions: options, 130 0 : error: 'Receiving data timeout[${options.receiveTimeout}ms]', 131 : type: DioErrorType.receiveTimeout, 132 : ); 133 : } 134 : } else { 135 12 : await completer.future; 136 : } 137 : // we create a final Uint8List and copy all chunks into it 138 7 : final responseBytes = Uint8List(finalSize); 139 : var chunkOffset = 0; 140 13 : for (var chunk in chunks) { 141 6 : responseBytes.setAll(chunkOffset, chunk); 142 12 : chunkOffset += chunk.length; 143 : } 144 : 145 14 : if (options.responseType == ResponseType.bytes) return responseBytes; 146 : 147 : String? responseBody; 148 7 : if (options.responseDecoder != null) { 149 0 : responseBody = options.responseDecoder!( 150 : responseBytes, 151 : options, 152 0 : response..stream = Stream.empty(), 153 : ); 154 : } else { 155 7 : responseBody = utf8.decode(responseBytes, allowMalformed: true); 156 : } 157 7 : if (responseBody.isNotEmpty && 158 10 : options.responseType == ResponseType.json && 159 20 : _isJsonMime(response.headers[Headers.contentTypeHeader]?.first)) { 160 3 : final callback = jsonDecodeCallback; 161 : if (callback != null) { 162 : return callback(responseBody); 163 : } else { 164 3 : return json.decode(responseBody); 165 : } 166 : } 167 : return responseBody; 168 : } 169 : 170 5 : bool _isJsonMime(String? contentType) { 171 : if (contentType == null) return false; 172 15 : return MediaType.parse(contentType).mimeType == 173 10 : Headers.jsonMimeType.mimeType; 174 : } 175 : }