Refactor to async/await (#328)

Use an `async*` for `MultipartRequest.finalize()`.

Refactor `.then` chains to async methods.

Add test for error forwarding in multipart request. 
This relates to a TODO that had been solved but
had not been removed.
diff --git a/lib/src/base_client.dart b/lib/src/base_client.dart
index f19fb07..3be18a8 100644
--- a/lib/src/base_client.dart
+++ b/lib/src/base_client.dart
@@ -121,11 +121,10 @@
   /// For more fine-grained control over the request and response, use [send] or
   /// [get] instead.
   @override
-  Future<String> read(url, {Map<String, String> headers}) {
-    return get(url, headers: headers).then((response) {
-      _checkResponseSuccess(url, response);
-      return response.body;
-    });
+  Future<String> read(url, {Map<String, String> headers}) async {
+    final response = await get(url, headers: headers);
+    _checkResponseSuccess(url, response);
+    return response.body;
   }
 
   /// Sends an HTTP GET request with the given headers to the given URL, which
@@ -138,11 +137,10 @@
   /// For more fine-grained control over the request and response, use [send] or
   /// [get] instead.
   @override
-  Future<Uint8List> readBytes(url, {Map<String, String> headers}) {
-    return get(url, headers: headers).then((response) {
-      _checkResponseSuccess(url, response);
-      return response.bodyBytes;
-    });
+  Future<Uint8List> readBytes(url, {Map<String, String> headers}) async {
+    final response = await get(url, headers: headers);
+    _checkResponseSuccess(url, response);
+    return response.bodyBytes;
   }
 
   /// Sends an HTTP request and asynchronously returns the response.
diff --git a/lib/src/mock_client.dart b/lib/src/mock_client.dart
index 6dc3a61..45030ab 100644
--- a/lib/src/mock_client.dart
+++ b/lib/src/mock_client.dart
@@ -30,42 +30,39 @@
   /// Creates a [MockClient] with a handler that receives [Request]s and sends
   /// [Response]s.
   MockClient(MockClientHandler fn)
-      : this._((baseRequest, bodyStream) {
-          return bodyStream.toBytes().then((bodyBytes) {
-            var request = Request(baseRequest.method, baseRequest.url)
-              ..persistentConnection = baseRequest.persistentConnection
-              ..followRedirects = baseRequest.followRedirects
-              ..maxRedirects = baseRequest.maxRedirects
-              ..headers.addAll(baseRequest.headers)
-              ..bodyBytes = bodyBytes
-              ..finalize();
+      : this._((baseRequest, bodyStream) async {
+          final bodyBytes = await bodyStream.toBytes();
+          var request = Request(baseRequest.method, baseRequest.url)
+            ..persistentConnection = baseRequest.persistentConnection
+            ..followRedirects = baseRequest.followRedirects
+            ..maxRedirects = baseRequest.maxRedirects
+            ..headers.addAll(baseRequest.headers)
+            ..bodyBytes = bodyBytes
+            ..finalize();
 
-            return fn(request);
-          }).then((response) {
-            return StreamedResponse(
-                ByteStream.fromBytes(response.bodyBytes), response.statusCode,
-                contentLength: response.contentLength,
-                request: baseRequest,
-                headers: response.headers,
-                isRedirect: response.isRedirect,
-                persistentConnection: response.persistentConnection,
-                reasonPhrase: response.reasonPhrase);
-          });
+          final response = await fn(request);
+          return StreamedResponse(
+              ByteStream.fromBytes(response.bodyBytes), response.statusCode,
+              contentLength: response.contentLength,
+              request: baseRequest,
+              headers: response.headers,
+              isRedirect: response.isRedirect,
+              persistentConnection: response.persistentConnection,
+              reasonPhrase: response.reasonPhrase);
         });
 
   /// Creates a [MockClient] with a handler that receives [StreamedRequest]s and
   /// sends [StreamedResponse]s.
   MockClient.streaming(MockClientStreamHandler fn)
-      : this._((request, bodyStream) {
-          return fn(request, bodyStream).then((response) {
-            return StreamedResponse(response.stream, response.statusCode,
-                contentLength: response.contentLength,
-                request: request,
-                headers: response.headers,
-                isRedirect: response.isRedirect,
-                persistentConnection: response.persistentConnection,
-                reasonPhrase: response.reasonPhrase);
-          });
+      : this._((request, bodyStream) async {
+          final response = await fn(request, bodyStream);
+          return StreamedResponse(response.stream, response.statusCode,
+              contentLength: response.contentLength,
+              request: request,
+              headers: response.headers,
+              isRedirect: response.isRedirect,
+              persistentConnection: response.persistentConnection,
+              reasonPhrase: response.reasonPhrase);
         });
 
   @override
diff --git a/lib/src/multipart_request.dart b/lib/src/multipart_request.dart
index 79a971f..0cf13b4 100644
--- a/lib/src/multipart_request.dart
+++ b/lib/src/multipart_request.dart
@@ -87,40 +87,31 @@
   /// Freezes all mutable fields and returns a single-subscription [ByteStream]
   /// that will emit the request body.
   @override
-  ByteStream finalize() {
+  ByteStream finalize() => ByteStream(_finalize());
+
+  Stream<List<int>> _finalize() async* {
     // TODO(nweiz): freeze fields and files
     var boundary = _boundaryString();
     headers['content-type'] = 'multipart/form-data; boundary=$boundary';
     super.finalize();
+    const line = [13, 10]; // \r\n
+    final separator = utf8.encode('--$boundary\r\n');
+    final close = utf8.encode('--$boundary--\r\n');
 
-    var controller = StreamController<List<int>>(sync: true);
-
-    void writeAscii(String string) {
-      controller.add(utf8.encode(string));
+    for (var field in fields.entries) {
+      yield separator;
+      yield utf8.encode(_headerForField(field.key, field.value));
+      yield utf8.encode(field.value);
+      yield line;
     }
 
-    writeUtf8(String string) => controller.add(utf8.encode(string));
-    writeLine() => controller.add([13, 10]); // \r\n
-
-    fields.forEach((name, value) {
-      writeAscii('--$boundary\r\n');
-      writeAscii(_headerForField(name, value));
-      writeUtf8(value);
-      writeLine();
-    });
-
-    Future.forEach(_files, (MultipartFile file) {
-      writeAscii('--$boundary\r\n');
-      writeAscii(_headerForFile(file));
-      return controller.addStream(file.finalize()).then((_) => writeLine());
-    }).then((_) {
-      // TODO(nweiz): pass any errors propagated through this future on to
-      // the stream. See issue 3657.
-      writeAscii('--$boundary--\r\n');
-      controller.close();
-    });
-
-    return ByteStream(controller.stream);
+    for (final file in _files) {
+      yield separator;
+      yield utf8.encode(_headerForFile(file));
+      yield* file.finalize();
+      yield line;
+    }
+    yield close;
   }
 
   /// Returns the header string for a field.
diff --git a/lib/src/response.dart b/lib/src/response.dart
index 1e4e4cd..36d9614 100644
--- a/lib/src/response.dart
+++ b/lib/src/response.dart
@@ -60,15 +60,14 @@
 
   /// Creates a new HTTP response by waiting for the full body to become
   /// available from a [StreamedResponse].
-  static Future<Response> fromStream(StreamedResponse response) {
-    return response.stream.toBytes().then((body) {
-      return Response.bytes(body, response.statusCode,
-          request: response.request,
-          headers: response.headers,
-          isRedirect: response.isRedirect,
-          persistentConnection: response.persistentConnection,
-          reasonPhrase: response.reasonPhrase);
-    });
+  static Future<Response> fromStream(StreamedResponse response) async {
+    final body = await response.stream.toBytes();
+    return Response.bytes(body, response.statusCode,
+        request: response.request,
+        headers: response.headers,
+        isRedirect: response.isRedirect,
+        persistentConnection: response.persistentConnection,
+        reasonPhrase: response.reasonPhrase);
   }
 }
 
diff --git a/test/multipart_test.dart b/test/multipart_test.dart
index 5bc12d2..52dcbc6 100644
--- a/test/multipart_test.dart
+++ b/test/multipart_test.dart
@@ -239,4 +239,11 @@
         --{{boundary}}--
         '''));
   });
+
+  test('with a file that has an error', () async {
+    var file = http.MultipartFile(
+        'file', Future<List<int>>.error('error').asStream(), 1);
+    var request = http.MultipartRequest('POST', dummyUrl)..files.add(file);
+    expect(request.finalize().drain(), throwsA('error'));
+  });
 }