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 : /// 38 5 : static String urlEncodeMap(Map map) { 39 7 : return encodeMap(map, (key, value) { 40 : if (value == null) return key; 41 6 : return '$key=${Uri.encodeQueryComponent(value.toString())}'; 42 : }); 43 : } 44 : } 45 : 46 : /// The default [Transformer] for [Dio]. If you want to custom the transformation of 47 : /// request/response data, you can provide a [Transformer] by your self, and 48 : /// replace the [DefaultTransformer] by setting the [dio.Transformer]. 49 : 50 : typedef JsonDecodeCallback = dynamic Function(String); 51 : 52 : class DefaultTransformer extends Transformer { 53 5 : DefaultTransformer({this.jsonDecodeCallback}); 54 : 55 : JsonDecodeCallback jsonDecodeCallback; 56 : 57 : @override 58 1 : Future<String> transformRequest(RequestOptions options) async { 59 1 : var data = options.data ?? ''; 60 1 : if (data is! String) { 61 2 : if (_isJsonMime(options.contentType)) { 62 2 : return json.encode(options.data); 63 0 : } else if (data is Map) { 64 0 : return Transformer.urlEncodeMap(data); 65 : } 66 : } 67 0 : return data.toString(); 68 : } 69 : 70 : /// As an agreement, you must return the [response] 71 : /// when the Options.responseType is [ResponseType.stream]. 72 : @override 73 3 : Future transformResponse( 74 : RequestOptions options, ResponseBody response) async { 75 6 : if (options.responseType == ResponseType.stream) { 76 : return response; 77 : } 78 : var length = 0; 79 : var received = 0; 80 3 : var showDownloadProgress = options.onReceiveProgress != null; 81 : if (showDownloadProgress) { 82 1 : length = int.parse( 83 3 : response.headers[Headers.contentLengthHeader]?.first ?? '-1'); 84 : } 85 3 : var completer = Completer(); 86 : var stream = 87 9 : response.stream.transform<Uint8List>(StreamTransformer.fromHandlers( 88 3 : handleData: (data, sink) { 89 3 : sink.add(data); 90 : if (showDownloadProgress) { 91 2 : received += data.length; 92 1 : options.onReceiveProgress(received, length); 93 : } 94 : }, 95 : )); 96 : // let's keep references to the data chunks and concatenate them later 97 3 : final chunks = <Uint8List>[]; 98 : var finalSize = 0; 99 3 : StreamSubscription subscription = stream.listen( 100 3 : (chunk) { 101 6 : finalSize += chunk.length; 102 3 : chunks.add(chunk); 103 : }, 104 0 : onError: (e) { 105 0 : completer.completeError(e); 106 : }, 107 3 : onDone: () { 108 3 : completer.complete(); 109 : }, 110 : cancelOnError: true, 111 : ); 112 : // ignore: unawaited_futures 113 5 : options.cancelToken?.whenCancel?.then((_) { 114 0 : return subscription.cancel(); 115 : }); 116 6 : if (options.receiveTimeout > 0) { 117 : try { 118 2 : await completer.future 119 3 : .timeout(Duration(milliseconds: options.receiveTimeout)); 120 0 : } on TimeoutException { 121 0 : await subscription.cancel(); 122 0 : throw DioError( 123 : request: options, 124 0 : error: 'Receiving data timeout[${options.receiveTimeout}ms]', 125 : type: DioErrorType.RECEIVE_TIMEOUT, 126 : ); 127 : } 128 : } else { 129 4 : await completer.future; 130 : } 131 : // we create a final Uint8List and copy all chunks into it 132 3 : final responseBytes = Uint8List(finalSize); 133 : var chunkOffset = 0; 134 6 : for (var chunk in chunks) { 135 3 : responseBytes.setAll(chunkOffset, chunk); 136 6 : chunkOffset += chunk.length; 137 : } 138 : 139 6 : if (options.responseType == ResponseType.bytes) return responseBytes; 140 : 141 : String responseBody; 142 3 : if (options.responseDecoder != null) { 143 0 : responseBody = options.responseDecoder( 144 0 : responseBytes, options, response..stream = null); 145 : } else { 146 3 : responseBody = utf8.decode(responseBytes, allowMalformed: true); 147 : } 148 : if (responseBody != null && 149 3 : responseBody.isNotEmpty && 150 6 : options.responseType == ResponseType.json && 151 12 : _isJsonMime(response.headers[Headers.contentTypeHeader]?.first)) { 152 2 : if (jsonDecodeCallback != null) { 153 0 : return jsonDecodeCallback(responseBody); 154 : } else { 155 2 : return json.decode(responseBody); 156 : } 157 : } 158 : return responseBody; 159 : } 160 : 161 3 : bool _isJsonMime(String contentType) { 162 : if (contentType == null) return false; 163 12 : return MediaType.parse(contentType).mimeType.toLowerCase() == 164 6 : Headers.jsonMimeType.mimeType; 165 : } 166 : }