From a68b67ae9750de416b64aab480ad3b7e282e94b6 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Wed, 15 Jun 2022 11:35:34 -0800
Subject: [PATCH 01/33] chore!(api): migrate API category type definitions
 (#1640)

---
 .../src/category/amplify_api_category.dart    | 134 +++++----
 .../lib/src/category/amplify_categories.dart  |   1 +
 .../plugin/amplify_api_plugin_interface.dart  |  74 +++--
 .../lib/src/types/api/api_types.dart          |   2 +-
 .../types/api/exceptions/api_exception.dart   |   9 -
 .../types/api/graphql/graphql_operation.dart  |  15 +-
 .../lib/src/types/api/rest/http_payload.dart  |  82 ++++++
 .../src/types/api/rest/rest_exception.dart    |  14 +-
 .../src/types/api/rest/rest_operation.dart    |  20 +-
 .../lib/src/types/api/rest/rest_response.dart |  59 ----
 packages/api/amplify_api/.gitignore           |  40 ++-
 .../example/lib/graphql_api_view.dart         |   3 +-
 .../api/amplify_api/example/lib/main.dart     |   2 +-
 .../example/lib/rest_api_view.dart            |  65 ++---
 packages/api/amplify_api/example/pubspec.yaml |   1 +
 .../lib/src/method_channel_api.dart           | 229 +++++++++++----
 packages/api/amplify_api/pubspec.yaml         |   3 +-
 .../test/amplify_rest_api_methods_test.dart   | 270 ++++++++----------
 .../example/lib/main.dart                     |  13 +-
 19 files changed, 611 insertions(+), 425 deletions(-)
 create mode 100644 packages/amplify_core/lib/src/types/api/rest/http_payload.dart
 delete mode 100644 packages/amplify_core/lib/src/types/api/rest/rest_response.dart

diff --git a/packages/amplify_core/lib/src/category/amplify_api_category.dart b/packages/amplify_core/lib/src/category/amplify_api_category.dart
index 99406d31fc..7d9692a725 100644
--- a/packages/amplify_core/lib/src/category/amplify_api_category.dart
+++ b/packages/amplify_core/lib/src/category/amplify_api_category.dart
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License").
  * You may not use this file except in compliance with the License.
@@ -21,17 +21,13 @@ class APICategory extends AmplifyCategory<APIPluginInterface> {
   Category get category => Category.api;
 
   // ====== GraphQL =======
-  GraphQLOperation<T> query<T>({required GraphQLRequest<T> request}) {
-    return plugins.length == 1
-        ? plugins[0].query(request: request)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<GraphQLResponse<T>> query<T>(
+          {required GraphQLRequest<T> request}) =>
+      defaultPlugin.query(request: request);
 
-  GraphQLOperation<T> mutate<T>({required GraphQLRequest<T> request}) {
-    return plugins.length == 1
-        ? plugins[0].mutate(request: request)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<GraphQLResponse<T>> mutate<T>(
+          {required GraphQLRequest<T> request}) =>
+      defaultPlugin.mutate(request: request);
 
   /// Subscribes to the given [request] and returns the stream of response events.
   /// An optional [onEstablished] callback can be used to be alerted when the
@@ -42,52 +38,88 @@ class APICategory extends AmplifyCategory<APIPluginInterface> {
   Stream<GraphQLResponse<T>> subscribe<T>(
     GraphQLRequest<T> request, {
     void Function()? onEstablished,
-  }) {
-    return plugins.length == 1
-        ? plugins[0].subscribe(request, onEstablished: onEstablished)
-        : throw _pluginNotAddedException('Api');
-  }
+  }) =>
+      defaultPlugin.subscribe(request, onEstablished: onEstablished);
 
   // ====== RestAPI ======
-  void cancelRequest(String cancelToken) {
-    return plugins.length == 1
-        ? plugins[0].cancelRequest(cancelToken)
-        : throw _pluginNotAddedException('Api');
-  }
 
-  RestOperation get({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].get(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> delete(
+    String path, {
+    Map<String, String>? headers,
+    HttpPayload? body,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.delete(
+        path,
+        headers: headers,
+        body: body,
+        apiName: apiName,
+      );
 
-  RestOperation put({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].put(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> get(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.get(
+        path,
+        headers: headers,
+        apiName: apiName,
+      );
 
-  RestOperation post({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].post(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> head(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.head(
+        path,
+        headers: headers,
+        apiName: apiName,
+      );
 
-  RestOperation delete({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].delete(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> patch(
+    String path, {
+    Map<String, String>? headers,
+    HttpPayload? body,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.patch(
+        path,
+        headers: headers,
+        body: body,
+        apiName: apiName,
+      );
 
-  RestOperation head({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].head(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> post(
+    String path, {
+    Map<String, String>? headers,
+    HttpPayload? body,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.post(
+        path,
+        headers: headers,
+        body: body,
+        apiName: apiName,
+      );
 
-  RestOperation patch({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].patch(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> put(
+    String path, {
+    Map<String, String>? headers,
+    HttpPayload? body,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.put(
+        path,
+        headers: headers,
+        body: body,
+        apiName: apiName,
+      );
 }
diff --git a/packages/amplify_core/lib/src/category/amplify_categories.dart b/packages/amplify_core/lib/src/category/amplify_categories.dart
index 969ea3ebc7..4c014d05dc 100644
--- a/packages/amplify_core/lib/src/category/amplify_categories.dart
+++ b/packages/amplify_core/lib/src/category/amplify_categories.dart
@@ -18,6 +18,7 @@ library amplify_interface;
 import 'dart:async';
 
 import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
 import 'package:collection/collection.dart';
 import 'package:meta/meta.dart';
 
diff --git a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart
index d318db6e13..5169acb091 100644
--- a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart
+++ b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License").
  * You may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@
  */
 
 import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
 import 'package:meta/meta.dart';
 
 abstract class APIPluginInterface extends AmplifyPluginInterface {
@@ -25,11 +26,13 @@ abstract class APIPluginInterface extends AmplifyPluginInterface {
   ModelProviderInterface? get modelProvider => throw UnimplementedError();
 
   // ====== GraphQL =======
-  GraphQLOperation<T> query<T>({required GraphQLRequest<T> request}) {
+  CancelableOperation<GraphQLResponse<T>> query<T>(
+      {required GraphQLRequest<T> request}) {
     throw UnimplementedError('query() has not been implemented.');
   }
 
-  GraphQLOperation<T> mutate<T>({required GraphQLRequest<T> request}) {
+  CancelableOperation<GraphQLResponse<T>> mutate<T>(
+      {required GraphQLRequest<T> request}) {
     throw UnimplementedError('mutate() has not been implemented.');
   }
 
@@ -50,31 +53,64 @@ abstract class APIPluginInterface extends AmplifyPluginInterface {
   void registerAuthProvider(APIAuthProvider authProvider);
 
   // ====== RestAPI ======
-  void cancelRequest(String cancelToken) {
-    throw UnimplementedError('cancelRequest has not been implemented.');
-  }
-
-  RestOperation get({required RestOptions restOptions}) {
-    throw UnimplementedError('get has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> delete(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('delete() has not been implemented');
   }
 
-  RestOperation put({required RestOptions restOptions}) {
-    throw UnimplementedError('put has not been implemented.');
+  /// Uses Amplify configuration to authorize request to [path] and returns
+  /// [CancelableOperation] which resolves to standard HTTP
+  /// [Response](https://pub.dev/documentation/http/latest/http/Response-class.html).
+  CancelableOperation<AWSStreamedHttpResponse> get(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('get() has not been implemented');
   }
 
-  RestOperation post({required RestOptions restOptions}) {
-    throw UnimplementedError('post has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> head(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('head() has not been implemented');
   }
 
-  RestOperation delete({required RestOptions restOptions}) {
-    throw UnimplementedError('delete has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> patch(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('patch() has not been implemented');
   }
 
-  RestOperation head({required RestOptions restOptions}) {
-    throw UnimplementedError('head has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> post(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('post() has not been implemented');
   }
 
-  RestOperation patch({required RestOptions restOptions}) {
-    throw UnimplementedError('patch has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> put(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('put() has not been implemented');
   }
 }
diff --git a/packages/amplify_core/lib/src/types/api/api_types.dart b/packages/amplify_core/lib/src/types/api/api_types.dart
index 3e69a1dc4b..299fd03412 100644
--- a/packages/amplify_core/lib/src/types/api/api_types.dart
+++ b/packages/amplify_core/lib/src/types/api/api_types.dart
@@ -27,10 +27,10 @@ export 'graphql/graphql_response.dart';
 export 'graphql/graphql_response_error.dart';
 export 'graphql/graphql_subscription_operation.dart';
 
+export 'rest/http_payload.dart';
 export 'rest/rest_exception.dart';
 export 'rest/rest_operation.dart';
 export 'rest/rest_options.dart';
-export 'rest/rest_response.dart';
 
 export 'types/pagination/paginated_model_type.dart';
 export 'types/pagination/paginated_result.dart';
diff --git a/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart b/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart
index 9f9d833110..2ec1bf37ac 100644
--- a/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart
+++ b/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart
@@ -19,18 +19,11 @@ import 'package:amplify_core/amplify_core.dart';
 /// Exception thrown from the API Category.
 /// {@endtemplate}
 class ApiException extends AmplifyException {
-  /// HTTP status of response, only available if error
-  @Deprecated(
-      'Use RestException instead to retrieve the HTTP response. Existing uses of '
-      'ApiException for handling REST errors can be safely replaced with RestException')
-  final int? httpStatusCode;
-
   /// {@macro api_exception}
   const ApiException(
     String message, {
     String? recoverySuggestion,
     String? underlyingException,
-    this.httpStatusCode,
   }) : super(
           message,
           recoverySuggestion: recoverySuggestion,
@@ -40,7 +33,6 @@ class ApiException extends AmplifyException {
   /// Constructor for down casting an AmplifyException to this exception
   ApiException._private(
     AmplifyException exception,
-    this.httpStatusCode,
   ) : super(
           exception.message,
           recoverySuggestion: exception.recoverySuggestion,
@@ -57,7 +49,6 @@ class ApiException extends AmplifyException {
     }
     return ApiException._private(
       AmplifyException.fromMap(serializedException),
-      statusCode,
     );
   }
 }
diff --git a/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart b/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart
index 94035a8997..b9f72dbd37 100644
--- a/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart
+++ b/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License").
  * You may not use this file except in compliance with the License.
@@ -13,11 +13,14 @@
  * permissions and limitations under the License.
  */
 
-import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
 
-class GraphQLOperation<T> {
-  final Function cancel;
-  final Future<GraphQLResponse<T>> response;
+import 'graphql_response.dart';
 
-  const GraphQLOperation({required this.response, required this.cancel});
+/// Allows callers to synchronously get the unstreamed response with decoded body.
+extension GraphQLOperation<T> on CancelableOperation<GraphQLResponse<T>> {
+  @Deprecated('use .value instead')
+  Future<GraphQLResponse<T>> get response {
+    return value;
+  }
 }
diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart
new file mode 100644
index 0000000000..eb657d7543
--- /dev/null
+++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
+
+/// {@template amplify_core.http_payload}
+/// An HTTP request's payload.
+/// {@endtemplate}
+class HttpPayload extends StreamView<List<int>> {
+  String contentType = 'text/plain';
+
+  /// {@macro amplify_core.http_payload}
+  factory HttpPayload([Object? body]) {
+    if (body == null) {
+      return HttpPayload.empty();
+    }
+    if (body is String) {
+      return HttpPayload.string(body);
+    }
+    if (body is List<int>) {
+      return HttpPayload.bytes(body);
+    }
+    if (body is Stream<List<int>>) {
+      return HttpPayload.streaming(body);
+    }
+    if (body is Map<String, String>) {
+      return HttpPayload.formFields(body);
+    }
+    throw ArgumentError('Invalid HTTP payload type: ${body.runtimeType}');
+  }
+
+  /// An empty HTTP body.
+  HttpPayload.empty() : super(const Stream.empty());
+
+  /// A UTF-8 encoded HTTP body.
+  HttpPayload.string(String body, {Encoding encoding = utf8})
+      : super(LazyStream(() => Stream.value(encoding.encode(body))));
+
+  /// A byte buffer HTTP body.
+  HttpPayload.bytes(List<int> body) : super(Stream.value(body));
+
+  /// A form-encoded body of `key=value` pairs.
+  HttpPayload.formFields(Map<String, String> body, {Encoding encoding = utf8})
+      : contentType = 'application/x-www-form-urlencoded',
+        super(LazyStream(() => Stream.value(
+            encoding.encode(_mapToQuery(body, encoding: encoding)))));
+
+  /// Encodes body as a JSON string and sets Content-Type to 'application/json'
+  HttpPayload.json(Object body, {Encoding encoding = utf8})
+      : contentType = 'application/json',
+        super(
+            LazyStream(() => Stream.value(encoding.encode(json.encode(body)))));
+
+  /// A streaming HTTP body.
+  HttpPayload.streaming(Stream<List<int>> body) : super(body);
+}
+
+/// Converts a [Map] from parameter names to values to a URL query string.
+///
+///     _mapToQuery({"foo": "bar", "baz": "bang"});
+///     //=> "foo=bar&baz=bang"
+///
+/// Similar util at https://github.com/dart-lang/http/blob/06649afbb5847dbb0293816ba8348766b116e419/pkgs/http/lib/src/utils.dart#L15
+String _mapToQuery(Map<String, String> map, {required Encoding encoding}) => map
+    .entries
+    .map((e) =>
+        '${Uri.encodeQueryComponent(e.key, encoding: encoding)}=${Uri.encodeQueryComponent(e.value, encoding: encoding)}')
+    .join('&');
diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart
index fe6a6a8ee5..1f6dc18c2e 100644
--- a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart
+++ b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart
@@ -19,16 +19,8 @@ import 'package:amplify_core/amplify_core.dart';
 /// An HTTP error encountered during a REST API call, i.e. for calls returning
 /// non-2xx status codes.
 /// {@endtemplate}
-class RestException extends ApiException {
-  /// The HTTP response from the server.
-  final RestResponse response;
-
+@Deprecated('BREAKING CHANGE: No longer thrown for non-200 responses.')
+abstract class RestException extends ApiException {
   /// {@macro rest_exception}
-  RestException(this.response)
-      : super(response.body, httpStatusCode: response.statusCode);
-
-  @override
-  String toString() {
-    return 'RestException{response=$response}';
-  }
+  const RestException() : super('REST exception.');
 }
diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart
index eb84a0ea42..a24ad39ad2 100644
--- a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart
+++ b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License").
  * You may not use this file except in compliance with the License.
@@ -13,11 +13,17 @@
  * permissions and limitations under the License.
  */
 
-import 'rest_response.dart';
+import 'package:async/async.dart';
+import 'package:aws_common/aws_common.dart';
 
-class RestOperation {
-  final Function cancel;
-  final Future<RestResponse> response;
-
-  const RestOperation({required this.response, required this.cancel});
+/// Allows callers to synchronously get unstreamed response with the decoded body.
+extension RestOperation on CancelableOperation<AWSStreamedHttpResponse> {
+  Future<AWSHttpResponse> get response async {
+    final value = await this.value;
+    return AWSHttpResponse(
+      body: await value.bodyBytes,
+      statusCode: value.statusCode,
+      headers: value.headers,
+    );
+  }
 }
diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_response.dart b/packages/amplify_core/lib/src/types/api/rest/rest_response.dart
deleted file mode 100644
index f93a2079e4..0000000000
--- a/packages/amplify_core/lib/src/types/api/rest/rest_response.dart
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-import 'dart:convert';
-import 'dart:typed_data';
-
-import 'package:amplify_core/amplify_core.dart';
-import 'package:meta/meta.dart';
-
-/// {@template rest_response}
-/// An HTTP response from a REST API call.
-/// {@endtemplate}
-@immutable
-class RestResponse with AWSEquatable<RestResponse> {
-  /// The response status code.
-  final int statusCode;
-
-  /// The response headers.
-  ///
-  /// Will be `null` if unavailable from the platform.
-  final Map<String, String>? headers;
-
-  /// The response body bytes.
-  final Uint8List data;
-
-  /// The decoded body (using UTF-8).
-  ///
-  /// For custom processing, use [data] to get the raw body bytes.
-  late final String body;
-
-  /// {@macro rest_response}
-  RestResponse({
-    required Uint8List? data,
-    required this.headers,
-    required this.statusCode,
-  }) : data = data ?? Uint8List(0) {
-    body = utf8.decode(this.data, allowMalformed: true);
-  }
-
-  @override
-  List<Object?> get props => [statusCode, headers, data];
-
-  @override
-  String toString() {
-    return 'RestResponse{statusCode=$statusCode, headers=$headers, body=$body}';
-  }
-}
diff --git a/packages/api/amplify_api/.gitignore b/packages/api/amplify_api/.gitignore
index e9dc58d3d6..6bb69a50e0 100644
--- a/packages/api/amplify_api/.gitignore
+++ b/packages/api/amplify_api/.gitignore
@@ -1,7 +1,43 @@
+# See https://dart.dev/guides/libraries/private-files
+
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
 .DS_Store
-.dart_tool/
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
 
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
 .packages
+.pub-cache/
 .pub/
-
 build/
+
+# Code coverage
+coverage/
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/api/amplify_api/example/lib/graphql_api_view.dart b/packages/api/amplify_api/example/lib/graphql_api_view.dart
index 53a218efcd..6644dad380 100644
--- a/packages/api/amplify_api/example/lib/graphql_api_view.dart
+++ b/packages/api/amplify_api/example/lib/graphql_api_view.dart
@@ -14,6 +14,7 @@
  */
 
 import 'package:amplify_flutter/amplify_flutter.dart';
+import 'package:async/async.dart';
 import 'package:flutter/material.dart';
 
 class GraphQLApiView extends StatefulWidget {
@@ -29,7 +30,7 @@ class GraphQLApiView extends StatefulWidget {
 class _GraphQLApiViewState extends State<GraphQLApiView> {
   String _result = '';
   void Function()? _unsubscribe;
-  late GraphQLOperation _lastOperation;
+  late CancelableOperation _lastOperation;
 
   Future<void> subscribe() async {
     String graphQLDocument = '''subscription MySubscription {
diff --git a/packages/api/amplify_api/example/lib/main.dart b/packages/api/amplify_api/example/lib/main.dart
index 5c044e7aec..6e5dbf862d 100644
--- a/packages/api/amplify_api/example/lib/main.dart
+++ b/packages/api/amplify_api/example/lib/main.dart
@@ -44,7 +44,7 @@ class _MyAppState extends State<MyApp> {
   }
 
   void _configureAmplify() async {
-    Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]);
+    await Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]);
 
     try {
       await Amplify.configure(amplifyconfig);
diff --git a/packages/api/amplify_api/example/lib/rest_api_view.dart b/packages/api/amplify_api/example/lib/rest_api_view.dart
index aeca89c97f..68f8a414f1 100644
--- a/packages/api/amplify_api/example/lib/rest_api_view.dart
+++ b/packages/api/amplify_api/example/lib/rest_api_view.dart
@@ -13,9 +13,8 @@
  * permissions and limitations under the License.
  */
 
-import 'dart:convert';
-
 import 'package:amplify_flutter/amplify_flutter.dart';
+import 'package:async/async.dart';
 import 'package:flutter/material.dart';
 
 class RestApiView extends StatefulWidget {
@@ -27,7 +26,7 @@ class RestApiView extends StatefulWidget {
 
 class _RestApiViewState extends State<RestApiView> {
   late TextEditingController _apiPathController;
-  late RestOperation _lastRestOperation;
+  late CancelableOperation _lastRestOperation;
 
   @override
   void initState() {
@@ -39,18 +38,16 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onPutPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.put(
-        restOptions: RestOptions(
-          path: _apiPathController.text,
-          body: ascii.encode('{"name":"Mow the lawn"}'),
-        ),
+      final restOperation = Amplify.API.put(
+        _apiPathController.text,
+        body: HttpPayload.json({'name': 'Mow the lawn'}),
       );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Put SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on Exception catch (e) {
       print('Put FAILED');
       print(e);
@@ -59,18 +56,16 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onPostPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.post(
-        restOptions: RestOptions(
-          path: _apiPathController.text,
-          body: ascii.encode('{"name":"Mow the lawn"}'),
-        ),
+      final restOperation = Amplify.API.post(
+        _apiPathController.text,
+        body: HttpPayload.json({'name': 'Mow the lawn'}),
       );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Post SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on Exception catch (e) {
       print('Post FAILED');
       print(e);
@@ -79,16 +74,15 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onGetPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.get(
-          restOptions: RestOptions(
-        path: _apiPathController.text,
-      ));
+      final restOperation = Amplify.API.get(
+        _apiPathController.text,
+      );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Get SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on ApiException catch (e) {
       print('Get FAILED');
       print(e.toString());
@@ -97,15 +91,14 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onDeletePressed() async {
     try {
-      RestOperation restOperation = Amplify.API.delete(
-        restOptions: RestOptions(path: _apiPathController.text),
+      final restOperation = Amplify.API.delete(
+        _apiPathController.text,
       );
-
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Delete SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on Exception catch (e) {
       print('Delete FAILED');
       print(e);
@@ -123,15 +116,14 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onHeadPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.head(
-        restOptions: RestOptions(path: _apiPathController.text),
+      final restOperation = Amplify.API.head(
+        _apiPathController.text,
       );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      await restOperation.response;
 
       print('Head SUCCESS');
-      print(response);
     } on ApiException catch (e) {
       print('Head FAILED');
       print(e.toString());
@@ -140,15 +132,16 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onPatchPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.patch(
-        restOptions: RestOptions(path: _apiPathController.text),
+      final restOperation = Amplify.API.patch(
+        _apiPathController.text,
+        body: HttpPayload.json({'name': 'Mow the lawn'}),
       );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Patch SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on ApiException catch (e) {
       print('Patch FAILED');
       print(e.toString());
diff --git a/packages/api/amplify_api/example/pubspec.yaml b/packages/api/amplify_api/example/pubspec.yaml
index 4e57ade772..7b017f9370 100644
--- a/packages/api/amplify_api/example/pubspec.yaml
+++ b/packages/api/amplify_api/example/pubspec.yaml
@@ -21,6 +21,7 @@ dependencies:
     path: ../../../auth/amplify_auth_cognito
   amplify_flutter:
     path: ../../../amplify/amplify_flutter
+  async: ^2.8.2
   aws_common: ^0.1.0
 
   # The following adds the Cupertino Icons font to your application.
diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart
index 59deb7fca0..95e8f5c17d 100644
--- a/packages/api/amplify_api/lib/src/method_channel_api.dart
+++ b/packages/api/amplify_api/lib/src/method_channel_api.dart
@@ -14,13 +14,14 @@
  */
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:typed_data';
 
 import 'package:amplify_api/src/graphql/graphql_response_decoder.dart';
 import 'package:amplify_api/src/graphql/graphql_subscription_event.dart';
 import 'package:amplify_api/src/graphql/graphql_subscription_transformer.dart';
 import 'package:amplify_core/amplify_core.dart';
-
+import 'package:async/async.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 
@@ -150,31 +151,19 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
   }
 
   @override
-  GraphQLOperation<T> query<T>({required GraphQLRequest<T> request}) {
-    Future<GraphQLResponse<T>> response =
+  CancelableOperation<GraphQLResponse<T>> query<T>(
+      {required GraphQLRequest<T> request}) {
+    Future<GraphQLResponse<T>> responseFuture =
         _getMethodChannelResponse(methodName: 'query', request: request);
-
-    //TODO: Cancel implementation will be added along with REST API as it is shared
-    GraphQLOperation<T> result = GraphQLOperation<T>(
-      cancel: () => cancelRequest(request.id),
-      response: response,
-    );
-
-    return result;
+    return CancelableOperation.fromFuture(responseFuture);
   }
 
   @override
-  GraphQLOperation<T> mutate<T>({required GraphQLRequest<T> request}) {
-    Future<GraphQLResponse<T>> response =
+  CancelableOperation<GraphQLResponse<T>> mutate<T>(
+      {required GraphQLRequest<T> request}) {
+    Future<GraphQLResponse<T>> responseFuture =
         _getMethodChannelResponse(methodName: 'mutate', request: request);
-
-    //TODO: Cancel implementation will be added along with REST API as it is shared
-    GraphQLOperation<T> result = GraphQLOperation<T>(
-      cancel: () => cancelRequest(request.id),
-      response: response,
-    );
-
-    return result;
+    return CancelableOperation.fromFuture(responseFuture);
   }
 
   @override
@@ -248,21 +237,73 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
   }
 
   // ====== RestAPI ======
-  RestOperation _restFunctionHelper(
-      {required String methodName, required RestOptions restOptions}) {
-    // Send Request cancelToken to Native
-    String cancelToken = UUID.getUUID();
 
-    Future<RestResponse> futureResponse =
-        _callNativeRestMethod(methodName, cancelToken, restOptions);
+  Future<AWSStreamedHttpResponse> _restResponseHelper({
+    required String methodName,
+    required String path,
+    required String cancelToken,
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) async {
+    Uint8List? bodyBytes;
+    if (body != null) {
+      final completer = Completer<Uint8List>();
+      final sink = ByteConversionSink.withCallback(
+        (bytes) => completer.complete(Uint8List.fromList(bytes)),
+      );
+      body.listen(
+        sink.add,
+        onError: completer.completeError,
+        onDone: sink.close,
+        cancelOnError: true,
+      );
+      bodyBytes = await completer.future;
+    }
 
-    return RestOperation(
-      response: futureResponse,
-      cancel: () => cancelRequest(cancelToken),
+    final restOptions = RestOptions(
+      path: path,
+      body: bodyBytes,
+      apiName: apiName,
+      queryParameters: queryParameters,
+      headers: headers,
+    );
+    return _callNativeRestMethod(methodName, cancelToken, restOptions);
+  }
+
+  CancelableOperation<AWSStreamedHttpResponse> _restFunctionHelper({
+    required String methodName,
+    required String path,
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    // Send Request cancelToken to Native
+    String cancelToken = uuid();
+    // Ensure Content-Type header matches payload.
+    var modifiedHeaders = headers != null ? Map.of(headers) : null;
+    final contentType = body?.contentType;
+    if (contentType != null) {
+      modifiedHeaders = modifiedHeaders ?? {};
+      modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType);
+    }
+    final responseFuture = _restResponseHelper(
+      methodName: methodName,
+      path: path,
+      cancelToken: cancelToken,
+      body: body,
+      headers: modifiedHeaders,
+      queryParameters: queryParameters,
+      apiName: apiName,
     );
+
+    return CancelableOperation.fromFuture(responseFuture,
+        onCancel: () => cancelRequest(cancelToken));
   }
 
-  Future<RestResponse> _callNativeRestMethod(
+  Future<AWSStreamedHttpResponse> _callNativeRestMethod(
       String methodName, String cancelToken, RestOptions restOptions) async {
     // Prepare map input
     Map<String, dynamic> inputsMap = <String, dynamic>{};
@@ -284,55 +325,125 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
     }
   }
 
-  bool _shouldThrow(int statusCode) {
-    return statusCode < 200 || statusCode > 299;
-  }
-
-  RestResponse _formatRestResponse(Map<String, dynamic> res) {
+  AWSStreamedHttpResponse _formatRestResponse(Map<String, dynamic> res) {
     final statusCode = res['statusCode'] as int;
-    final headers = res['headers'] as Map?;
-    final response = RestResponse(
-      data: res['data'] as Uint8List?,
-      headers: headers?.cast<String, String>(),
-      statusCode: statusCode,
-    );
-    if (_shouldThrow(statusCode)) {
-      throw RestException(response);
-    }
-    return response;
+    // Make type-safe version of response headers.
+    final serializedHeaders = res['headers'] as Map?;
+    final headers = serializedHeaders?.cast<String, String>();
+    final rawResponseBody = res['data'] as Uint8List?;
+
+    return AWSStreamedHttpResponse(
+        statusCode: statusCode,
+        headers: headers,
+        body: Stream.value(rawResponseBody ?? []));
   }
 
   @override
-  RestOperation get({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'get', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> get(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'get',
+      path: path,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation put({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'put', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> put(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'put',
+      path: path,
+      body: body,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation post({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'post', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> post(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'post',
+      path: path,
+      body: body,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation delete({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'delete', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> delete(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'delete',
+      path: path,
+      body: body,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation head({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'head', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> head(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'head',
+      path: path,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation patch({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'patch', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> patch(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'patch',
+      path: path,
+      body: body,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
-  @override
+  /// Cancels a request with a given request ID.
+  @Deprecated('Use .cancel() on CancelableOperation instead.')
   Future<void> cancelRequest(String cancelToken) async {
     print('Attempting to cancel Operation $cancelToken');
 
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index 4305f09b30..869779172e 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -2,6 +2,7 @@ name: amplify_api
 description: The Amplify Flutter API category plugin, supporting GraphQL and REST operations.
 version: 0.5.0
 homepage: https://github.com/aws-amplify/amplify-flutter/tree/main/packages/amplify_api
+publish_to: none # until finalized
 
 environment:
   sdk: ">=2.17.0 <3.0.0"
@@ -12,6 +13,7 @@ dependencies:
   amplify_api_ios: 0.5.0
   amplify_core: 0.5.0
   amplify_flutter: 0.5.0
+  async: ^2.8.2
   aws_common: ^0.1.0
   collection: ^1.15.0
   flutter:
@@ -23,7 +25,6 @@ dev_dependencies:
   amplify_lints: ^2.0.0
   amplify_test:
     path: ../../amplify_test
-  async: ^2.6.0
   build_runner: ^2.0.0
   flutter_test:
     sdk: flutter
diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart
index 925c940b6b..5106ada1c2 100644
--- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart
+++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart
@@ -26,9 +26,19 @@ import 'graphql_helpers_test.dart';
 
 const statusOK = 200;
 const statusBadRequest = 400;
-
-// Matchers
-final throwsRestException = throwsA(isA<RestException>());
+const mowLawnBody = '{"name": "Mow the lawn"}';
+const hello = 'Hello from lambda!';
+final helloResponse = ascii.encode(hello);
+final encodedMowLoanBody = ascii.encode(mowLawnBody);
+const queryParameters = {
+  'queryParameterA': 'queryValueA',
+  'queryParameterB': 'queryValueB'
+};
+const headers = {
+  'headerA': 'headerValueA',
+  'headerB': 'headerValueB',
+  AWSHeaders.contentType: 'text/plain'
+};
 
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
@@ -42,184 +52,177 @@ void main() {
     await Amplify.addPlugin(api);
   });
 
-  test('PUT returns proper response.data', () async {
-    var responseData = Uint8List.fromList(
-        '{"success": "put call succeed!","url":/items?queryParameterA=queryValueA&queryParameterB=queryValueB","body": {"name": "Mow the lawn"}}'
-            .codeUnits);
-    var body = Uint8List.fromList('{"name":"Mow the lawn"}'.codeUnits);
-    var queryParameters = {
-      'queryParameterA': 'queryValueA',
-      'queryParameterB': 'queryValueB'
-    };
-    var headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'};
+  Future<void> _assertResponse(AWSStreamedHttpResponse response) async {
+    final actualResponseBody = await response.decodeBody();
+    expect(actualResponseBody, hello);
+    expect(response.statusCode, statusOK);
+  }
 
+  test('PUT returns proper response.data', () async {
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
       if (methodCall.method == 'put') {
         Map<dynamic, dynamic> restOptions =
             methodCall.arguments['restOptions'] as Map;
         expect(restOptions['apiName'], 'restapi');
         expect(restOptions['path'], '/items');
-        expect(restOptions['body'], body);
+        expect(restOptions['body'], encodedMowLoanBody);
         expect(restOptions['queryParameters'], queryParameters);
         expect(restOptions['headers'], headers);
-
-        return {'data': responseData, 'statusCode': statusOK};
+        return {'data': helloResponse, 'statusCode': statusOK};
       }
     });
 
-    RestOperation restOperation = api.put(
-      restOptions: RestOptions(
-        path: '/items',
-        body: body,
-        apiName: 'restapi',
-        queryParameters: queryParameters,
-        headers: headers,
-      ),
+    final restOperation = api.put(
+      '/items',
+      body: HttpPayload.string(mowLawnBody),
+      apiName: 'restapi',
+      queryParameters: queryParameters,
+      headers: headers,
     );
 
-    RestResponse response = await restOperation.response;
-
-    expect(response.data, responseData);
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
   test('POST returns proper response.data', () async {
-    var responseData = Uint8List.fromList(
-        '{"success": "post call succeed!","url":"/items?queryParameterA=queryValueA&queryParameterB=queryValueB","body": {"name": "Mow the lawn"}}'
-            .codeUnits);
-    var body = Uint8List.fromList('{"name":"Mow the lawn"}'.codeUnits);
-    var queryParameters = {
-      'queryParameterA': 'queryValueA',
-      'queryParameterB': 'queryValueB'
-    };
-    var headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'};
-
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
       if (methodCall.method == 'post') {
         Map<dynamic, dynamic> restOptions =
             methodCall.arguments['restOptions'] as Map;
         expect(restOptions['apiName'], 'restapi');
         expect(restOptions['path'], '/items');
-        expect(restOptions['body'], body);
+        expect(restOptions['body'], encodedMowLoanBody);
         expect(restOptions['queryParameters'], queryParameters);
         expect(restOptions['headers'], headers);
-
-        return {'data': responseData, 'statusCode': statusOK};
+        return {'data': helloResponse, 'statusCode': statusOK};
       }
     });
 
-    RestOperation restOperation = api.post(
-      restOptions: RestOptions(
-        path: '/items',
-        body: body,
-        apiName: 'restapi',
-        headers: headers,
-        queryParameters: queryParameters,
-      ),
+    final restOperation = api.post(
+      '/items',
+      body: HttpPayload.string(mowLawnBody),
+      apiName: 'restapi',
+      queryParameters: queryParameters,
+      headers: headers,
     );
 
-    RestResponse response = await restOperation.response;
-
-    expect(response.data, responseData);
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
   test('GET returns proper response.data', () async {
-    var responseData = Uint8List.fromList(
-        '{"success":"get call succeed!","url":"/items"}'.codeUnits);
-
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
       if (methodCall.method == 'get') {
         Map<dynamic, dynamic> restOptions =
             methodCall.arguments['restOptions'] as Map;
+        expect(restOptions['apiName'], 'restapi');
         expect(restOptions['path'], '/items');
-
-        return {'data': responseData, 'statusCode': statusOK};
+        expect(restOptions['queryParameters'], queryParameters);
+        expect(restOptions['headers'], headers);
+        return {'data': helloResponse, 'statusCode': statusOK};
       }
     });
 
-    RestOperation restOperation = api.get(
-        restOptions: const RestOptions(
-      path: '/items',
-    ));
-
-    RestResponse response = await restOperation.response;
+    final restOperation = api.get(
+      '/items',
+      apiName: 'restapi',
+      queryParameters: queryParameters,
+      headers: headers,
+    );
 
-    expect(response.data, responseData);
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
   test('DELETE returns proper response.data', () async {
-    var responseData = Uint8List.fromList(
-        '{"success":"delete call succeed!","url":"/items"}'.codeUnits);
-
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
       if (methodCall.method == 'delete') {
         Map<dynamic, dynamic> restOptions =
             methodCall.arguments['restOptions'] as Map;
+        expect(restOptions['apiName'], 'restapi');
         expect(restOptions['path'], '/items');
-
-        return {'data': responseData, 'statusCode': statusOK};
+        expect(restOptions['body'], encodedMowLoanBody);
+        expect(restOptions['queryParameters'], queryParameters);
+        expect(restOptions['headers'], headers);
+        return {'data': helloResponse, 'statusCode': statusOK};
       }
     });
 
-    RestOperation restOperation = api.delete(
-        restOptions: const RestOptions(
-      path: '/items',
-    ));
-
-    RestResponse response = await restOperation.response;
+    final restOperation = api.delete(
+      '/items',
+      body: HttpPayload.string(mowLawnBody),
+      apiName: 'restapi',
+      queryParameters: queryParameters,
+      headers: headers,
+    );
 
-    expect(response.data, responseData);
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
-  test('GET Status Code Error throws proper error', () async {
+  test(
+      'POST with form-encoded body gets proper response with response headers included',
+      () async {
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
-      if (methodCall.method == 'get') {
-        throw PlatformException(code: 'ApiException', details: {
-          'message': 'AMPLIFY_API_MUTATE_FAILED',
-          'recoverySuggestion': 'some insightful suggestion',
-          'underlyingException': 'Act of God'
-        });
+      if (methodCall.method == 'post') {
+        Map<dynamic, dynamic> restOptions =
+            methodCall.arguments['restOptions'] as Map;
+        expect(restOptions['apiName'], 'restapi');
+        expect(restOptions['path'], '/items');
+        expect(restOptions['queryParameters'], queryParameters);
+        expect(restOptions['headers'][AWSHeaders.contentType],
+            'application/x-www-form-urlencoded');
+        expect(utf8.decode(restOptions['body'] as List<int>), 'foo=bar');
+        return {
+          'data': helloResponse,
+          'statusCode': statusOK,
+          'headers': {'foo': 'bar'}
+        };
       }
     });
 
-    try {
-      RestOperation restOperation = api.get(
-          restOptions: const RestOptions(
-        path: '/items',
-      ));
-      await restOperation.response;
-    } on ApiException catch (e) {
-      expect(e.message, 'AMPLIFY_API_MUTATE_FAILED');
-      expect(e.recoverySuggestion, 'some insightful suggestion');
-      expect(e.underlyingException, 'Act of God');
-    }
+    final restOperation = api.post(
+      '/items',
+      apiName: 'restapi',
+      body: HttpPayload.formFields({'foo': 'bar'}),
+      queryParameters: queryParameters,
+    );
+
+    final response = await restOperation.value;
+    expect(response.headers['foo'], 'bar');
+    await _assertResponse(response);
   });
 
-  test('GET exception adds the httpStatusCode to exception if available',
+  test(
+      'POST with json-encoded body has property Content-Type and gets proper response',
       () async {
-    const statusCode = 500;
-    const data = 'Internal server error';
-
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
-      if (methodCall.method == 'get') {
+      if (methodCall.method == 'post') {
+        Map<dynamic, dynamic> restOptions =
+            methodCall.arguments['restOptions'] as Map;
+        expect(restOptions['apiName'], 'restapi');
+        expect(restOptions['path'], '/items');
+        expect(restOptions['queryParameters'], queryParameters);
+        expect(
+            restOptions['headers'][AWSHeaders.contentType], 'application/json');
+        expect(utf8.decode(restOptions['body'] as List<int>), '{"foo":"bar"}');
         return {
-          'statusCode': statusCode,
-          'headers': <String, String>{},
-          'data': Uint8List.fromList(data.codeUnits),
+          'data': helloResponse,
+          'statusCode': statusOK,
+          'headers': {'foo': 'bar'}
         };
       }
     });
 
-    try {
-      RestOperation restOperation = api.get(
-        restOptions: const RestOptions(
-          path: '/items',
-        ),
-      );
-      await restOperation.response;
-    } on RestException catch (e) {
-      expect(e.response.statusCode, 500);
-      expect(e.response.body, data);
-    }
+    final restOperation = api.post(
+      '/items',
+      apiName: 'restapi',
+      body: HttpPayload.json({'foo': 'bar'}),
+      queryParameters: queryParameters,
+    );
+
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
   test('CANCEL success does not throw error', () async {
@@ -237,50 +240,9 @@ void main() {
       }
     });
 
-    RestOperation restOperation = api.get(
-        restOptions: const RestOptions(
-      path: '/items',
-    ));
+    final restOperation = api.get('/items');
 
     //RestResponse response = await restOperation.response;
     restOperation.cancel();
   });
-
-  group('non-2xx status code', () {
-    const testBody = 'test';
-    const testResponseHeaders = {'key': 'value'};
-
-    setUpAll(() {
-      apiChannel.setMockMethodCallHandler((call) async {
-        return {
-          'data': utf8.encode(testBody),
-          'statusCode': statusBadRequest,
-          'headers': testResponseHeaders,
-        };
-      });
-    });
-
-    test('throws RestException', () async {
-      final restOp = api.get(restOptions: const RestOptions(path: '/'));
-      await expectLater(restOp.response, throwsRestException);
-    });
-
-    test('has valid RestResponse', () async {
-      final restOp = api.get(restOptions: const RestOptions(path: '/'));
-
-      RestException restException;
-      try {
-        await restOp.response;
-        fail('RestOperation should throw');
-      } on Exception catch (e) {
-        expect(e, isA<RestException>());
-        restException = e as RestException;
-      }
-
-      final response = restException.response;
-      expect(response.statusCode, statusBadRequest);
-      expect(response.headers, testResponseHeaders);
-      expect(response.body, testBody);
-    });
-  });
 }
diff --git a/packages/auth/amplify_auth_cognito/example/lib/main.dart b/packages/auth/amplify_auth_cognito/example/lib/main.dart
index 8a3980458b..1285d917bc 100644
--- a/packages/auth/amplify_auth_cognito/example/lib/main.dart
+++ b/packages/auth/amplify_auth_cognito/example/lib/main.dart
@@ -12,9 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import 'dart:convert';
 import 'dart:io';
-import 'dart:typed_data';
 
 import 'package:amplify_api/amplify_api.dart';
 import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
@@ -167,14 +165,13 @@ class _HomeScreenState extends State<HomeScreen> {
     try {
       final response = await Amplify.API
           .post(
-            restOptions: RestOptions(
-              path: '/hello',
-              body: utf8.encode(_controller.text) as Uint8List,
-            ),
+            '/hello',
+            body: HttpPayload.string(_controller.text),
           )
-          .response;
+          .value;
+      final decodedBody = await response.decodeBody();
       setState(() {
-        _greeting = response.body;
+        _greeting = decodedBody;
       });
     } on Exception catch (e) {
       setState(() {

From 6f4c84f19a06ee7c2f8d348377d9973ac0535c5a Mon Sep 17 00:00:00 2001
From: Elijah Quartey <Equartey@users.noreply.github.com>
Date: Thu, 23 Jun 2022 11:39:46 -0500
Subject: [PATCH 02/33] chore(api): API Native Bridge for .addPlugin() (#1756)

---
 packages/api/amplify_api/Makefile             |   4 +
 packages/api/amplify_api/example/pubspec.yaml |   3 +-
 packages/api/amplify_api/lib/amplify_api.dart |  21 ++--
 .../amplify_api/lib/src/api_plugin_impl.dart  |  81 ++++++++++++++
 .../lib/src/native_api_plugin.dart            |  63 +++++++++++
 .../pigeons/native_api_plugin.dart            |  43 ++++++++
 packages/api/amplify_api/pubspec.yaml         |  10 +-
 .../amplify/amplify_api/AmplifyApi.kt         |  52 +++++----
 .../amplify_api/NativeApiPluginBindings.java  |  87 +++++++++++++++
 packages/api/amplify_api_android/pubspec.yaml |   3 +-
 .../ios/Classes/NativeApiPlugin.h             |  35 ++++++
 .../ios/Classes/NativeApiPlugin.m             | 102 ++++++++++++++++++
 .../ios/Classes/SwiftAmplifyApiPlugin.swift   |  43 ++++----
 .../ios/Classes/amplify_api_ios.h             |  21 ++++
 .../ios/amplify_api_ios.podspec               |  14 +++
 .../api/amplify_api_ios/ios/module.modulemap  |   6 ++
 packages/api/amplify_api_ios/pubspec.yaml     |   3 +-
 17 files changed, 526 insertions(+), 65 deletions(-)
 create mode 100644 packages/api/amplify_api/Makefile
 create mode 100644 packages/api/amplify_api/lib/src/api_plugin_impl.dart
 create mode 100644 packages/api/amplify_api/lib/src/native_api_plugin.dart
 create mode 100644 packages/api/amplify_api/pigeons/native_api_plugin.dart
 create mode 100644 packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
 create mode 100644 packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
 create mode 100644 packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
 create mode 100644 packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h
 create mode 100644 packages/api/amplify_api_ios/ios/module.modulemap

diff --git a/packages/api/amplify_api/Makefile b/packages/api/amplify_api/Makefile
new file mode 100644
index 0000000000..f1c3ac38ba
--- /dev/null
+++ b/packages/api/amplify_api/Makefile
@@ -0,0 +1,4 @@
+.PHONY: pigeons
+pigeons:
+	flutter pub run pigeon --input pigeons/native_api_plugin.dart
+	flutter format --fix lib/src/native_api_plugin.dart
diff --git a/packages/api/amplify_api/example/pubspec.yaml b/packages/api/amplify_api/example/pubspec.yaml
index 7b017f9370..9a2ddbe654 100644
--- a/packages/api/amplify_api/example/pubspec.yaml
+++ b/packages/api/amplify_api/example/pubspec.yaml
@@ -32,7 +32,8 @@ dependencies:
     sdk: flutter
 
 dev_dependencies:
-  amplify_lints: ^2.0.0
+  amplify_lints: 
+    path: ../../../amplify_lints
   amplify_test:
     path: ../../../amplify_test
   flutter_driver:
diff --git a/packages/api/amplify_api/lib/amplify_api.dart b/packages/api/amplify_api/lib/amplify_api.dart
index f0ca3c2c4f..a4db7b1e97 100644
--- a/packages/api/amplify_api/lib/amplify_api.dart
+++ b/packages/api/amplify_api/lib/amplify_api.dart
@@ -15,9 +15,7 @@
 
 library amplify_api_plugin;
 
-import 'dart:io';
-
-import 'package:amplify_api/src/method_channel_api.dart';
+import 'package:amplify_api/src/api_plugin_impl.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:meta/meta.dart';
 
@@ -32,18 +30,11 @@ export './model_subscriptions.dart';
 /// {@endtemplate}
 abstract class AmplifyAPI extends APIPluginInterface {
   /// {@macro amplify_api.amplify_api}
-  factory AmplifyAPI({
-    List<APIAuthProvider> authProviders = const [],
-    ModelProviderInterface? modelProvider,
-  }) {
-    if (zIsWeb || Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
-      throw UnsupportedError('This platform is not supported yet');
-    }
-    return AmplifyAPIMethodChannel(
-      authProviders: authProviders,
-      modelProvider: modelProvider,
-    );
-  }
+  factory AmplifyAPI(
+          {List<APIAuthProvider> authProviders = const [],
+          ModelProviderInterface? modelProvider}) =>
+      AmplifyAPIDart(
+          authProviders: authProviders, modelProvider: modelProvider);
 
   /// Protected constructor for subclasses.
   @protected
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
new file mode 100644
index 0000000000..5ac4fc36ff
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -0,0 +1,81 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+library amplify_api;
+
+import 'dart:io';
+
+import 'package:amplify_api/amplify_api.dart';
+import 'package:amplify_api/src/native_api_plugin.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter/services.dart';
+
+/// {@template amplify_api.amplify_api_dart}
+/// The AWS implementation of the Amplify API category.
+/// {@endtemplate}
+class AmplifyAPIDart extends AmplifyAPI {
+  late final AWSApiPluginConfig _apiConfig;
+
+  /// The registered [APIAuthProvider] instances.
+  final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};
+
+  /// {@macro amplify_api.amplify_api_dart}
+  AmplifyAPIDart({
+    List<APIAuthProvider> authProviders = const [],
+    this.modelProvider,
+  }) : super.protected() {
+    authProviders.forEach(registerAuthProvider);
+  }
+
+  @override
+  Future<void> configure({AmplifyConfig? config}) async {
+    final apiConfig = config?.api?.awsPlugin;
+    if (apiConfig == null) {
+      throw const ApiException('No AWS API config found',
+          recoverySuggestion: 'Add API from the Amplify CLI. See '
+              'https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/flutter/#configure-api');
+    }
+    _apiConfig = apiConfig;
+  }
+
+  @override
+  Future<void> addPlugin() async {
+    if (zIsWeb || !(Platform.isAndroid || Platform.isIOS)) {
+      return;
+    }
+
+    final nativeBridge = NativeApiBridge();
+    try {
+      final authProvidersList =
+          _authProviders.keys.map((key) => key.rawValue).toList();
+      await nativeBridge.addPlugin(authProvidersList);
+    } on PlatformException catch (e) {
+      if (e.code == 'AmplifyAlreadyConfiguredException') {
+        throw const AmplifyAlreadyConfiguredException(
+            AmplifyExceptionMessages.alreadyConfiguredDefaultMessage,
+            recoverySuggestion:
+                AmplifyExceptionMessages.alreadyConfiguredDefaultSuggestion);
+      }
+      throw AmplifyException.fromMap((e.details as Map).cast());
+    }
+  }
+
+  @override
+  final ModelProviderInterface? modelProvider;
+
+  @override
+  void registerAuthProvider(APIAuthProvider authProvider) {
+    _authProviders[authProvider.type] = authProvider;
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/native_api_plugin.dart b/packages/api/amplify_api/lib/src/native_api_plugin.dart
new file mode 100644
index 0000000000..e7c5af4d04
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/native_api_plugin.dart
@@ -0,0 +1,63 @@
+//
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//
+// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
+import 'package:flutter/services.dart';
+
+class _NativeApiBridgeCodec extends StandardMessageCodec {
+  const _NativeApiBridgeCodec();
+}
+
+class NativeApiBridge {
+  /// Constructor for [NativeApiBridge].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  NativeApiBridge({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _NativeApiBridgeCodec();
+
+  Future<void> addPlugin(List<String?> arg_authProvidersList) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.NativeApiBridge.addPlugin', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap = await channel
+        .send(<Object?>[arg_authProvidersList]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+}
diff --git a/packages/api/amplify_api/pigeons/native_api_plugin.dart b/packages/api/amplify_api/pigeons/native_api_plugin.dart
new file mode 100644
index 0000000000..a36f7397f9
--- /dev/null
+++ b/packages/api/amplify_api/pigeons/native_api_plugin.dart
@@ -0,0 +1,43 @@
+//
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//
+
+// ignore_for_file: avoid_positional_boolean_parameters
+
+@ConfigurePigeon(
+  PigeonOptions(
+    copyrightHeader: '../../../tool/license.txt',
+    dartOptions: DartOptions(),
+    dartOut: 'lib/src/native_Api_plugin.dart',
+    javaOptions: JavaOptions(
+      className: 'NativeApiPluginBindings',
+      package: 'com.amazonaws.amplify.amplify_api',
+    ),
+    javaOut:
+        '../amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java',
+    objcOptions: ObjcOptions(
+      header: 'NativeApiPlugin.h',
+    ),
+    objcHeaderOut: '../amplify_api_ios/ios/Classes/NativeApiPlugin.h',
+    objcSourceOut: '../amplify_api_ios/ios/Classes/NativeApiPlugin.m',
+  ),
+)
+library native_api_plugin;
+
+import 'package:pigeon/pigeon.dart';
+
+@HostApi()
+abstract class NativeApiBridge {
+  void addPlugin(List<String> authProvidersList);
+}
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index 869779172e..b65a92bfec 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -21,14 +21,22 @@ dependencies:
   meta: ^1.7.0
   plugin_platform_interface: ^2.0.0
 
+dependency_overrides:
+  # TODO(dnys1): Remove when pigeon is bumped
+  # https://github.com/flutter/flutter/issues/105090
+  analyzer: ^3.0.0
+
+
 dev_dependencies:
-  amplify_lints: ^2.0.0
+  amplify_lints: 
+    path: ../../amplify_lints
   amplify_test:
     path: ../../amplify_test
   build_runner: ^2.0.0
   flutter_test:
     sdk: flutter
   mockito: ^5.0.0
+  pigeon: ^3.1.5
 
 # The following section is specific to Flutter.
 flutter:
diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
index 02de711722..0205877bf7 100644
--- a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
+++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
@@ -39,7 +39,7 @@ import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
 
 /** AmplifyApiPlugin */
-class AmplifyApi : FlutterPlugin, MethodCallHandler {
+class AmplifyApi : FlutterPlugin, MethodCallHandler, NativeApiPluginBindings.NativeApiBridge {
 
     private companion object {
         /**
@@ -83,6 +83,11 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler {
             "com.amazonaws.amplify/api_observe_events"
         )
         eventchannel!!.setStreamHandler(graphqlSubscriptionStreamHandler)
+
+        NativeApiPluginBindings.NativeApiBridge.setup(
+            flutterPluginBinding.binaryMessenger,
+            this
+        )
     }
 
     @Suppress("UNCHECKED_CAST")
@@ -94,27 +99,6 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler {
         if (methodName == "cancel") {
             onCancel(result, (call.arguments as String))
             return
-        } else if (methodName == "addPlugin") {
-            try {
-                val authProvidersList: List<String> =
-                    (arguments["authProviders"] as List<*>?)?.cast() ?: listOf()
-                val authProviders = authProvidersList.map { AuthorizationType.valueOf(it) }
-                if (flutterAuthProviders == null) {
-                    flutterAuthProviders = FlutterAuthProviders(authProviders)
-                }
-                flutterAuthProviders!!.setMethodChannel(channel)
-                Amplify.addPlugin(
-                    AWSApiPlugin
-                        .builder()
-                        .apiAuthProviders(flutterAuthProviders!!.factory)
-                        .build()
-                )
-                logger.info("Added API plugin")
-                result.success(null)
-            } catch (e: Exception) {
-                handleAddPluginException("API", e, result)
-            }
-            return
         }
 
         try {
@@ -168,5 +152,29 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler {
         eventchannel = null
         graphqlSubscriptionStreamHandler?.close()
         graphqlSubscriptionStreamHandler = null
+
+        NativeApiPluginBindings.NativeApiBridge.setup(
+            flutterPluginBinding.binaryMessenger,
+            null,
+        )
+    }
+
+    override fun addPlugin(authProvidersList: MutableList<String>) {
+        try {
+            val authProviders = authProvidersList.map { AuthorizationType.valueOf(it) }
+            if (flutterAuthProviders == null) {
+                flutterAuthProviders = FlutterAuthProviders(authProviders)
+            }
+            flutterAuthProviders!!.setMethodChannel(channel)
+            Amplify.addPlugin(
+                AWSApiPlugin
+                    .builder()
+                    .apiAuthProviders(flutterAuthProviders!!.factory)
+                    .build()
+            )
+            logger.info("Added API plugin")
+        } catch (e: Exception) {
+            logger.error(e.message)
+        }
     }
 }
diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
new file mode 100644
index 0000000000..d8d07f4add
--- /dev/null
+++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
@@ -0,0 +1,87 @@
+// 
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// 
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+// 
+//  http://aws.amazon.com/apache2.0
+// 
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//  
+// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package com.amazonaws.amplify.amplify_api;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
+public class NativeApiPluginBindings {
+  private static class NativeApiBridgeCodec extends StandardMessageCodec {
+    public static final NativeApiBridgeCodec INSTANCE = new NativeApiBridgeCodec();
+    private NativeApiBridgeCodec() {}
+  }
+
+  /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
+  public interface NativeApiBridge {
+    void addPlugin(@NonNull List<String> authProvidersList);
+
+    /** The codec used by NativeApiBridge. */
+    static MessageCodec<Object> getCodec() {
+      return NativeApiBridgeCodec.INSTANCE;
+    }
+
+    /** Sets up an instance of `NativeApiBridge` to handle messages through the `binaryMessenger`. */
+    static void setup(BinaryMessenger binaryMessenger, NativeApiBridge api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeApiBridge.addPlugin", getCodec());
+        if (api != null) {
+          channel.setMessageHandler((message, reply) -> {
+            Map<String, Object> wrapped = new HashMap<>();
+            try {
+              ArrayList<Object> args = (ArrayList<Object>)message;
+              List<String> authProvidersListArg = (List<String>)args.get(0);
+              if (authProvidersListArg == null) {
+                throw new NullPointerException("authProvidersListArg unexpectedly null.");
+              }
+              api.addPlugin(authProvidersListArg);
+              wrapped.put("result", null);
+            }
+            catch (Error | RuntimeException exception) {
+              wrapped.put("error", wrapError(exception));
+            }
+            reply.reply(wrapped);
+          });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+    }
+  }
+  private static Map<String, Object> wrapError(Throwable exception) {
+    Map<String, Object> errorMap = new HashMap<>();
+    errorMap.put("message", exception.toString());
+    errorMap.put("code", exception.getClass().getSimpleName());
+    errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
+    return errorMap;
+  }
+}
diff --git a/packages/api/amplify_api_android/pubspec.yaml b/packages/api/amplify_api_android/pubspec.yaml
index 186f102aff..920cdc661b 100644
--- a/packages/api/amplify_api_android/pubspec.yaml
+++ b/packages/api/amplify_api_android/pubspec.yaml
@@ -12,7 +12,8 @@ dependencies:
     sdk: flutter
 
 dev_dependencies:
-  amplify_lints: ^2.0.0
+  amplify_lints: 
+    path: ../../amplify_lints
   flutter_test:
     sdk: flutter
 
diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
new file mode 100644
index 0000000000..7b3bad24ed
--- /dev/null
+++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
@@ -0,0 +1,35 @@
+// 
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// 
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+// 
+//  http://aws.amazon.com/apache2.0
+// 
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//  
+// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import <Foundation/Foundation.h>
+@protocol FlutterBinaryMessenger;
+@protocol FlutterMessageCodec;
+@class FlutterError;
+@class FlutterStandardTypedData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+
+/// The codec used by NativeApiBridge.
+NSObject<FlutterMessageCodec> *NativeApiBridgeGetCodec(void);
+
+@protocol NativeApiBridge
+- (void)addPluginAuthProvidersList:(NSArray<NSString *> *)authProvidersList error:(FlutterError *_Nullable *_Nonnull)error;
+@end
+
+extern void NativeApiBridgeSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<NativeApiBridge> *_Nullable api);
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
new file mode 100644
index 0000000000..c936591be5
--- /dev/null
+++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
@@ -0,0 +1,102 @@
+// 
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// 
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+// 
+//  http://aws.amazon.com/apache2.0
+// 
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//  
+// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import "NativeApiPlugin.h"
+#import <Flutter/Flutter.h>
+
+#if !__has_feature(objc_arc)
+#error File requires ARC to be enabled.
+#endif
+
+static NSDictionary<NSString *, id> *wrapResult(id result, FlutterError *error) {
+  NSDictionary *errorDict = (NSDictionary *)[NSNull null];
+  if (error) {
+    errorDict = @{
+        @"code": (error.code ?: [NSNull null]),
+        @"message": (error.message ?: [NSNull null]),
+        @"details": (error.details ?: [NSNull null]),
+        };
+  }
+  return @{
+      @"result": (result ?: [NSNull null]),
+      @"error": errorDict,
+      };
+}
+static id GetNullableObject(NSDictionary* dict, id key) {
+  id result = dict[key];
+  return (result == [NSNull null]) ? nil : result;
+}
+static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) {
+  id result = array[key];
+  return (result == [NSNull null]) ? nil : result;
+}
+
+
+
+@interface NativeApiBridgeCodecReader : FlutterStandardReader
+@end
+@implementation NativeApiBridgeCodecReader
+@end
+
+@interface NativeApiBridgeCodecWriter : FlutterStandardWriter
+@end
+@implementation NativeApiBridgeCodecWriter
+@end
+
+@interface NativeApiBridgeCodecReaderWriter : FlutterStandardReaderWriter
+@end
+@implementation NativeApiBridgeCodecReaderWriter
+- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
+  return [[NativeApiBridgeCodecWriter alloc] initWithData:data];
+}
+- (FlutterStandardReader *)readerWithData:(NSData *)data {
+  return [[NativeApiBridgeCodecReader alloc] initWithData:data];
+}
+@end
+
+NSObject<FlutterMessageCodec> *NativeApiBridgeGetCodec() {
+  static dispatch_once_t sPred = 0;
+  static FlutterStandardMessageCodec *sSharedObject = nil;
+  dispatch_once(&sPred, ^{
+    NativeApiBridgeCodecReaderWriter *readerWriter = [[NativeApiBridgeCodecReaderWriter alloc] init];
+    sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];
+  });
+  return sSharedObject;
+}
+
+
+void NativeApiBridgeSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<NativeApiBridge> *api) {
+  {
+    FlutterBasicMessageChannel *channel =
+      [[FlutterBasicMessageChannel alloc]
+        initWithName:@"dev.flutter.pigeon.NativeApiBridge.addPlugin"
+        binaryMessenger:binaryMessenger
+        codec:NativeApiBridgeGetCodec()        ];
+    if (api) {
+      NSCAssert([api respondsToSelector:@selector(addPluginAuthProvidersList:error:)], @"NativeApiBridge api (%@) doesn't respond to @selector(addPluginAuthProvidersList:error:)", api);
+      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+        NSArray *args = message;
+        NSArray<NSString *> *arg_authProvidersList = GetNullableObjectAtIndex(args, 0);
+        FlutterError *error;
+        [api addPluginAuthProvidersList:arg_authProvidersList error:&error];
+        callback(wrapResult(nil, error));
+      }];
+    }
+    else {
+      [channel setMessageHandler:nil];
+    }
+  }
+}
diff --git a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
index 7ad1accd1a..63ce5c373c 100644
--- a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
+++ b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
@@ -20,7 +20,7 @@ import AmplifyPlugins
 import amplify_flutter_ios
 import AWSPluginsCore
 
-public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin {
+public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
     private let bridge: ApiBridge
     private let graphQLSubscriptionsStreamHandler: GraphQLSubscriptionsStreamHandler
     static var methodChannel: FlutterMethodChannel!
@@ -43,6 +43,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin {
         let instance = SwiftAmplifyApiPlugin()
         eventchannel.setStreamHandler(instance.graphQLSubscriptionsStreamHandler)
         registrar.addMethodCallDelegate(instance, channel: methodChannel)
+        NativeApiBridgeSetup(registrar.messenger(), instance)
     }
 
     public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@@ -62,33 +63,26 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin {
 
             let arguments = try FlutterApiRequest.getMap(args: callArgs)
 
-            if method == "addPlugin"{
-                let authProvidersList = arguments["authProviders"] as? [String] ?? []
-                let authProviders = authProvidersList.compactMap {
-                    AWSAuthorizationType(rawValue: $0)
-                }
-                addPlugin(authProviders: authProviders, result: result)
-                return
-            }
-
             try innerHandle(method: method, arguments: arguments, result: result)
         } catch {
             print("Failed to parse query arguments with \(error)")
             FlutterApiErrorHandler.handleApiError(error: APIError(error: error), flutterResult: result)
         }
     }
-
-    private func addPlugin(authProviders: [AWSAuthorizationType], result: FlutterResult) {
+    
+    public func addPluginAuthProvidersList(_ authProvidersList: [String], error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
         do {
+            let authProviders = authProvidersList.compactMap {
+                AWSAuthorizationType(rawValue: $0)
+            }
             try Amplify.add(
                 plugin: AWSAPIPlugin(
                     sessionFactory: FlutterURLSessionBehaviorFactory(),
                     apiAuthProviderFactory: FlutterAuthProviders(authProviders)))
-            result(true)
         } catch let apiError as APIError {
-            ErrorUtil.postErrorToFlutterChannel(
-                result: result,
-                errorCode: "APIException",
+            error.pointee = FlutterError(
+                code: "APIException",
+                message: apiError.localizedDescription,
                 details: [
                     "message": apiError.errorDescription,
                     "recoverySuggestion": apiError.recoverySuggestion,
@@ -100,20 +94,21 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin {
             if case .amplifyAlreadyConfigured = configError {
                 errorCode = "AmplifyAlreadyConfiguredException"
             }
-            ErrorUtil.postErrorToFlutterChannel(
-                result: result,
-                errorCode: errorCode,
+            error.pointee = FlutterError(
+                code: errorCode,
+                message: configError.localizedDescription,
                 details: [
                     "message": configError.errorDescription,
                     "recoverySuggestion": configError.recoverySuggestion,
                     "underlyingError": configError.underlyingError?.localizedDescription ?? ""
                 ]
             )
-        } catch {
-            ErrorUtil.postErrorToFlutterChannel(
-                result: result,
-                errorCode: "UNKNOWN",
-                details: ["message": error.localizedDescription])
+        } catch let e {
+            error.pointee = FlutterError(
+                code: "UNKNOWN",
+                message: e.localizedDescription,
+                details: nil
+            )
         }
     }
 
diff --git a/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h b/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h
new file mode 100644
index 0000000000..0b890efd4f
--- /dev/null
+++ b/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h
@@ -0,0 +1,21 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//      http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef amplify_api_ios_h
+#define amplify_api_ios_h
+
+#import "NativeApiPlugin.h"
+#import "AmplifyApi.h"
+
+#endif /* amplify_api_ios_h */
diff --git a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
index 181063b97c..276c97012b 100644
--- a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
+++ b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
@@ -21,6 +21,20 @@ The API module for Amplify Flutter.
   s.platform = :ios, '11.0'
   s.swift_version = '5.0'
 
+  # Use a custom module map with a manually written umbrella header.
+  #
+  # Since we use `package:pigeon` to generate our platform interface 
+  # in ObjC, and since the rest of the module is written in Swift, we
+  # fall victim to this issue: https://github.com/CocoaPods/CocoaPods/issues/10544
+  # 
+  # This is because we have an ObjC -> Swift -> ObjC import cycle:
+  # ApiPlugin -> SwiftAmplifyApiPlugin -> NativeApiPlugin
+  # 
+  # The easiest solution to this problem is to create the umbrella
+  # header which would otherwise be auto-generated by Cocoapods but
+  # name it what's expected by the Swift compiler (amplify_api_ios.h).
+  s.module_map = 'module.modulemap'
+
   # Flutter.framework does not contain a i386 slice.
   s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
 end
diff --git a/packages/api/amplify_api_ios/ios/module.modulemap b/packages/api/amplify_api_ios/ios/module.modulemap
new file mode 100644
index 0000000000..acac87c311
--- /dev/null
+++ b/packages/api/amplify_api_ios/ios/module.modulemap
@@ -0,0 +1,6 @@
+framework module amplify_api_ios {
+    umbrella header "amplify_api_ios.h"
+
+    export *
+    module * { export * }
+}
diff --git a/packages/api/amplify_api_ios/pubspec.yaml b/packages/api/amplify_api_ios/pubspec.yaml
index 75c7a121e6..eea5904683 100644
--- a/packages/api/amplify_api_ios/pubspec.yaml
+++ b/packages/api/amplify_api_ios/pubspec.yaml
@@ -13,7 +13,8 @@ dependencies:
     sdk: flutter
 
 dev_dependencies:
-  amplify_lints: ^2.0.0
+  amplify_lints: 
+    path: ../../amplify_lints
   flutter_test:
     sdk: flutter
 

From 7b4e0caeb4a8ce1866a69720489bf8d27b03313f Mon Sep 17 00:00:00 2001
From: Elijah Quartey <Equartey@users.noreply.github.com>
Date: Mon, 27 Jun 2022 11:43:25 -0500
Subject: [PATCH 03/33] chore(api): API Pigeon update (#1813)

---
 .../lib/src/native_api_plugin.dart            |  2 +-
 .../pigeons/native_api_plugin.dart            |  1 +
 packages/api/amplify_api/pubspec.yaml         |  8 +-----
 .../amplify/amplify_api/AmplifyApi.kt         |  7 +++++-
 .../amplify_api/NativeApiPluginBindings.java  | 25 +++++++++++++++----
 .../ios/Classes/NativeApiPlugin.h             |  4 +--
 .../ios/Classes/NativeApiPlugin.m             | 10 ++++----
 .../ios/Classes/SwiftAmplifyApiPlugin.swift   |  9 ++++---
 .../ios/amplify_api_ios.podspec               |  6 +++--
 9 files changed, 45 insertions(+), 27 deletions(-)

diff --git a/packages/api/amplify_api/lib/src/native_api_plugin.dart b/packages/api/amplify_api/lib/src/native_api_plugin.dart
index e7c5af4d04..3ff74bd774 100644
--- a/packages/api/amplify_api/lib/src/native_api_plugin.dart
+++ b/packages/api/amplify_api/lib/src/native_api_plugin.dart
@@ -12,7 +12,7 @@
 // express or implied. See the License for the specific language governing
 // permissions and limitations under the License.
 //
-// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// Autogenerated from Pigeon (v3.2.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
diff --git a/packages/api/amplify_api/pigeons/native_api_plugin.dart b/packages/api/amplify_api/pigeons/native_api_plugin.dart
index a36f7397f9..0e54029724 100644
--- a/packages/api/amplify_api/pigeons/native_api_plugin.dart
+++ b/packages/api/amplify_api/pigeons/native_api_plugin.dart
@@ -39,5 +39,6 @@ import 'package:pigeon/pigeon.dart';
 
 @HostApi()
 abstract class NativeApiBridge {
+  @async
   void addPlugin(List<String> authProvidersList);
 }
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index b65a92bfec..2bc42e55cb 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -21,12 +21,6 @@ dependencies:
   meta: ^1.7.0
   plugin_platform_interface: ^2.0.0
 
-dependency_overrides:
-  # TODO(dnys1): Remove when pigeon is bumped
-  # https://github.com/flutter/flutter/issues/105090
-  analyzer: ^3.0.0
-
-
 dev_dependencies:
   amplify_lints: 
     path: ../../amplify_lints
@@ -36,7 +30,7 @@ dev_dependencies:
   flutter_test:
     sdk: flutter
   mockito: ^5.0.0
-  pigeon: ^3.1.5
+  pigeon: ^3.1.6
 
 # The following section is specific to Flutter.
 flutter:
diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
index 0205877bf7..e49a66932a 100644
--- a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
+++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
@@ -159,7 +159,10 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler, NativeApiPluginBindings.Nat
         )
     }
 
-    override fun addPlugin(authProvidersList: MutableList<String>) {
+    override fun addPlugin(
+        authProvidersList: MutableList<String>,
+        result: NativeApiPluginBindings.Result<Void>
+    ) {
         try {
             val authProviders = authProvidersList.map { AuthorizationType.valueOf(it) }
             if (flutterAuthProviders == null) {
@@ -173,8 +176,10 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler, NativeApiPluginBindings.Nat
                     .build()
             )
             logger.info("Added API plugin")
+            result.success(null)
         } catch (e: Exception) {
             logger.error(e.message)
+            result.error(e)
         }
     }
 }
diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
index d8d07f4add..70c59352c8 100644
--- a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
+++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
@@ -12,7 +12,7 @@
 // express or implied. See the License for the specific language governing
 // permissions and limitations under the License.
 //  
-// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// Autogenerated from Pigeon (v3.2.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 package com.amazonaws.amplify.amplify_api;
@@ -35,6 +35,11 @@
 /** Generated class from Pigeon. */
 @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
 public class NativeApiPluginBindings {
+
+  public interface Result<T> {
+    void success(T result);
+    void error(Throwable error);
+  }
   private static class NativeApiBridgeCodec extends StandardMessageCodec {
     public static final NativeApiBridgeCodec INSTANCE = new NativeApiBridgeCodec();
     private NativeApiBridgeCodec() {}
@@ -42,7 +47,7 @@ private NativeApiBridgeCodec() {}
 
   /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
   public interface NativeApiBridge {
-    void addPlugin(@NonNull List<String> authProvidersList);
+    void addPlugin(@NonNull List<String> authProvidersList, Result<Void> result);
 
     /** The codec used by NativeApiBridge. */
     static MessageCodec<Object> getCodec() {
@@ -63,13 +68,23 @@ static void setup(BinaryMessenger binaryMessenger, NativeApiBridge api) {
               if (authProvidersListArg == null) {
                 throw new NullPointerException("authProvidersListArg unexpectedly null.");
               }
-              api.addPlugin(authProvidersListArg);
-              wrapped.put("result", null);
+              Result<Void> resultCallback = new Result<Void>() {
+                public void success(Void result) {
+                  wrapped.put("result", null);
+                  reply.reply(wrapped);
+                }
+                public void error(Throwable error) {
+                  wrapped.put("error", wrapError(error));
+                  reply.reply(wrapped);
+                }
+              };
+
+              api.addPlugin(authProvidersListArg, resultCallback);
             }
             catch (Error | RuntimeException exception) {
               wrapped.put("error", wrapError(exception));
+              reply.reply(wrapped);
             }
-            reply.reply(wrapped);
           });
         } else {
           channel.setMessageHandler(null);
diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
index 7b3bad24ed..cf89fcb539 100644
--- a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
+++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
@@ -12,7 +12,7 @@
 // express or implied. See the License for the specific language governing
 // permissions and limitations under the License.
 //  
-// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// Autogenerated from Pigeon (v3.2.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 #import <Foundation/Foundation.h>
 @protocol FlutterBinaryMessenger;
@@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
 NSObject<FlutterMessageCodec> *NativeApiBridgeGetCodec(void);
 
 @protocol NativeApiBridge
-- (void)addPluginAuthProvidersList:(NSArray<NSString *> *)authProvidersList error:(FlutterError *_Nullable *_Nonnull)error;
+- (void)addPluginAuthProvidersList:(NSArray<NSString *> *)authProvidersList completion:(void(^)(FlutterError *_Nullable))completion;
 @end
 
 extern void NativeApiBridgeSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<NativeApiBridge> *_Nullable api);
diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
index c936591be5..bae599aa4b 100644
--- a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
+++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
@@ -12,7 +12,7 @@
 // express or implied. See the License for the specific language governing
 // permissions and limitations under the License.
 //  
-// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// Autogenerated from Pigeon (v3.2.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 #import "NativeApiPlugin.h"
 #import <Flutter/Flutter.h>
@@ -86,13 +86,13 @@ void NativeApiBridgeSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<N
         binaryMessenger:binaryMessenger
         codec:NativeApiBridgeGetCodec()        ];
     if (api) {
-      NSCAssert([api respondsToSelector:@selector(addPluginAuthProvidersList:error:)], @"NativeApiBridge api (%@) doesn't respond to @selector(addPluginAuthProvidersList:error:)", api);
+      NSCAssert([api respondsToSelector:@selector(addPluginAuthProvidersList:completion:)], @"NativeApiBridge api (%@) doesn't respond to @selector(addPluginAuthProvidersList:completion:)", api);
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
         NSArray *args = message;
         NSArray<NSString *> *arg_authProvidersList = GetNullableObjectAtIndex(args, 0);
-        FlutterError *error;
-        [api addPluginAuthProvidersList:arg_authProvidersList error:&error];
-        callback(wrapResult(nil, error));
+        [api addPluginAuthProvidersList:arg_authProvidersList completion:^(FlutterError *_Nullable error) {
+          callback(wrapResult(nil, error));
+        }];
       }];
     }
     else {
diff --git a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
index 63ce5c373c..01c14b8e0c 100644
--- a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
+++ b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
@@ -70,7 +70,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
         }
     }
     
-    public func addPluginAuthProvidersList(_ authProvidersList: [String], error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
+    public func addPluginAuthProvidersList(_ authProvidersList: [String]) async -> FlutterError? {
         do {
             let authProviders = authProvidersList.compactMap {
                 AWSAuthorizationType(rawValue: $0)
@@ -79,8 +79,9 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
                 plugin: AWSAPIPlugin(
                     sessionFactory: FlutterURLSessionBehaviorFactory(),
                     apiAuthProviderFactory: FlutterAuthProviders(authProviders)))
+            return nil
         } catch let apiError as APIError {
-            error.pointee = FlutterError(
+            return FlutterError(
                 code: "APIException",
                 message: apiError.localizedDescription,
                 details: [
@@ -94,7 +95,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
             if case .amplifyAlreadyConfigured = configError {
                 errorCode = "AmplifyAlreadyConfiguredException"
             }
-            error.pointee = FlutterError(
+            return FlutterError(
                 code: errorCode,
                 message: configError.localizedDescription,
                 details: [
@@ -104,7 +105,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
                 ]
             )
         } catch let e {
-            error.pointee = FlutterError(
+            return FlutterError(
                 code: "UNKNOWN",
                 message: e.localizedDescription,
                 details: nil
diff --git a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
index 276c97012b..f5a6147bff 100644
--- a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
+++ b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
@@ -18,8 +18,10 @@ The API module for Amplify Flutter.
   s.dependency 'Amplify', '1.23.0'
   s.dependency 'AmplifyPlugins/AWSAPIPlugin', '1.23.0'
   s.dependency 'amplify_flutter_ios'
-  s.platform = :ios, '11.0'
-  s.swift_version = '5.0'
+
+  # These are needed to support async/await with pigeon
+  s.platform = :ios, '13.0'
+  s.swift_version = '5.5'
 
   # Use a custom module map with a manually written umbrella header.
   #

From 26b23b608d53fdee3787f91bce09a5a03138307f Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Mon, 27 Jun 2022 14:28:39 -0800
Subject: [PATCH 04/33] feat(api): REST methods in dart with auth mode none
 (#1783)

---
 .../amplify_flutter/lib/src/hybrid_impl.dart  |   1 +
 .../lib/src/amplify_api_config.dart           |  74 ++++++++
 .../amplify_authorization_rest_client.dart    |  58 +++++++
 .../amplify_api/lib/src/api_plugin_impl.dart  | 163 +++++++++++++++++-
 .../lib/src/method_channel_api.dart           |  10 +-
 packages/api/amplify_api/lib/src/util.dart    |  32 ++++
 packages/api/amplify_api/pubspec.yaml         |   1 +
 .../test/amplify_dart_rest_methods_test.dart  | 103 +++++++++++
 .../test_data/fake_amplify_configuration.dart |  79 +++++++++
 packages/api/amplify_api/test/util_test.dart  |  42 +++++
 10 files changed, 554 insertions(+), 9 deletions(-)
 create mode 100644 packages/api/amplify_api/lib/src/amplify_api_config.dart
 create mode 100644 packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
 create mode 100644 packages/api/amplify_api/lib/src/util.dart
 create mode 100644 packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
 create mode 100644 packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
 create mode 100644 packages/api/amplify_api/test/util_test.dart

diff --git a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
index a8c91af3a0..5eb3f1257e 100644
--- a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
+++ b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
@@ -34,6 +34,7 @@ class AmplifyHybridImpl extends AmplifyClassImpl {
     );
     await Future.wait(
       [
+        ...API.plugins,
         ...Auth.plugins,
       ].map((p) => p.configure(config: amplifyConfig)),
       eagerError: true,
diff --git a/packages/api/amplify_api/lib/src/amplify_api_config.dart b/packages/api/amplify_api/lib/src/amplify_api_config.dart
new file mode 100644
index 0000000000..960f11bf9b
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/amplify_api_config.dart
@@ -0,0 +1,74 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:collection/collection.dart';
+import 'package:meta/meta.dart';
+
+const _slash = '/';
+
+@internal
+class EndpointConfig with AWSEquatable<EndpointConfig> {
+  const EndpointConfig(this.name, this.config);
+
+  final String name;
+  final AWSApiConfig config;
+
+  @override
+  List<Object?> get props => [name, config];
+
+  /// Gets the host with environment path prefix from Amplify config and combines
+  /// with [path] and [queryParameters] to return a full [Uri].
+  Uri getUri(String path, Map<String, dynamic>? queryParameters) {
+    final parsed = Uri.parse(config.endpoint);
+    // Remove leading slashes which are suggested in public documentation.
+    // https://docs.amplify.aws/lib/restapi/getting-started/q/platform/flutter/#make-a-post-request
+    if (path.startsWith(_slash)) {
+      path = path.substring(1);
+    }
+    // Avoid URI-encoding slashes in path from caller.
+    final pathSegmentsFromPath = path.split(_slash);
+    return parsed.replace(pathSegments: [
+      ...parsed.pathSegments,
+      ...pathSegmentsFromPath,
+    ], queryParameters: queryParameters);
+  }
+}
+
+@internal
+extension AWSApiPluginConfigHelpers on AWSApiPluginConfig {
+  EndpointConfig getEndpoint({
+    required EndpointType type,
+    String? apiName,
+  }) {
+    final typeConfigs =
+        entries.where((config) => config.value.endpointType == type);
+    if (apiName != null) {
+      final config = typeConfigs.firstWhere(
+        (config) => config.key == apiName,
+        orElse: () => throw ApiException(
+          'No API endpoint found matching apiName $apiName.',
+        ),
+      );
+      return EndpointConfig(config.key, config.value);
+    }
+    final onlyConfig = typeConfigs.singleOrNull;
+    if (onlyConfig == null) {
+      throw const ApiException(
+        'Multiple API endpoints defined. Pass apiName parameter to specify '
+        'which one to use.',
+      );
+    }
+    return EndpointConfig(onlyConfig.key, onlyConfig.value);
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
new file mode 100644
index 0000000000..e58885385c
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
@@ -0,0 +1,58 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+/// Implementation of http [http.Client] that authorizes HTTP requests with
+/// Amplify.
+@internal
+class AmplifyAuthorizationRestClient extends http.BaseClient
+    implements Closeable {
+  /// Determines how requests with this client are authorized.
+  final AWSApiConfig endpointConfig;
+  final http.Client _baseClient;
+  final bool _useDefaultBaseClient;
+
+  /// Provide an [AWSApiConfig] which will determine how requests from this
+  /// client are authorized.
+  AmplifyAuthorizationRestClient({
+    required this.endpointConfig,
+    http.Client? baseClient,
+  })  : _useDefaultBaseClient = baseClient == null,
+        _baseClient = baseClient ?? http.Client();
+
+  /// Implementation of [send] that authorizes any request without "Authorization"
+  /// header already set.
+  @override
+  Future<http.StreamedResponse> send(http.BaseRequest request) async =>
+      _baseClient.send(_authorizeRequest(request));
+
+  @override
+  void close() {
+    if (_useDefaultBaseClient) _baseClient.close();
+  }
+
+  http.BaseRequest _authorizeRequest(http.BaseRequest request) {
+    if (!request.headers.containsKey(AWSHeaders.authorization) &&
+        endpointConfig.authorizationType != APIAuthorizationType.none) {
+      // ignore: todo
+      // TODO: Use auth providers from core to transform the request.
+    }
+    return request;
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index 5ac4fc36ff..0926c0a462 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -19,13 +19,25 @@ import 'dart:io';
 import 'package:amplify_api/amplify_api.dart';
 import 'package:amplify_api/src/native_api_plugin.dart';
 import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
 import 'package:flutter/services.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+import 'amplify_api_config.dart';
+import 'amplify_authorization_rest_client.dart';
+import 'util.dart';
 
 /// {@template amplify_api.amplify_api_dart}
 /// The AWS implementation of the Amplify API category.
 /// {@endtemplate}
 class AmplifyAPIDart extends AmplifyAPI {
   late final AWSApiPluginConfig _apiConfig;
+  final http.Client? _baseHttpClient;
+
+  /// A map of the keys from the Amplify API config to HTTP clients to use for
+  /// requests to that endpoint.
+  final Map<String, AmplifyAuthorizationRestClient> _clientPool = {};
 
   /// The registered [APIAuthProvider] instances.
   final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};
@@ -33,8 +45,10 @@ class AmplifyAPIDart extends AmplifyAPI {
   /// {@macro amplify_api.amplify_api_dart}
   AmplifyAPIDart({
     List<APIAuthProvider> authProviders = const [],
+    http.Client? baseHttpClient,
     this.modelProvider,
-  }) : super.protected() {
+  })  : _baseHttpClient = baseHttpClient,
+        super.protected() {
     authProviders.forEach(registerAuthProvider);
   }
 
@@ -71,6 +85,43 @@ class AmplifyAPIDart extends AmplifyAPI {
     }
   }
 
+  /// Returns the HTTP client to be used for REST operations.
+  ///
+  /// Use [apiName] if there are multiple REST endpoints.
+  @visibleForTesting
+  http.Client getRestClient({String? apiName}) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.rest,
+      apiName: apiName,
+    );
+    return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
+      endpointConfig: endpoint.config,
+      baseClient: _baseHttpClient,
+    );
+  }
+
+  Uri _getRestUri(
+      String path, String? apiName, Map<String, dynamic>? queryParameters) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.rest,
+      apiName: apiName,
+    );
+    return endpoint.getUri(path, queryParameters);
+  }
+
+  /// NOTE: http does not support request abort https://github.com/dart-lang/http/issues/424.
+  /// For now, just make a [CancelableOperation] to cancel the future.
+  /// To actually abort calls at network layer, need to call in
+  /// dart:io/dart:html or other library with custom http default Client() implementation.
+  CancelableOperation<T> _makeCancelable<T>(Future<T> responseFuture) {
+    return CancelableOperation.fromFuture(responseFuture);
+  }
+
+  CancelableOperation<AWSStreamedHttpResponse> _prepareRestResponse(
+      Future<AWSStreamedHttpResponse> responseFuture) {
+    return _makeCancelable(responseFuture);
+  }
+
   @override
   final ModelProviderInterface? modelProvider;
 
@@ -78,4 +129,114 @@ class AmplifyAPIDart extends AmplifyAPI {
   void registerAuthProvider(APIAuthProvider authProvider) {
     _authProviders[authProvider.type] = authProvider;
   }
+
+  // ====== REST =======
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> delete(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(AWSStreamedHttpRequest.delete(
+      uri,
+      body: body ?? HttpPayload.empty(),
+      headers: addContentTypeToHeaders(headers, body),
+    ).send(client));
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> get(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSHttpRequest.get(
+        uri,
+        headers: headers,
+      ).send(client),
+    );
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> head(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSHttpRequest.head(
+        uri,
+        headers: headers,
+      ).send(client),
+    );
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> patch(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSStreamedHttpRequest.patch(
+        uri,
+        headers: addContentTypeToHeaders(headers, body),
+        body: body ?? HttpPayload.empty(),
+      ).send(client),
+    );
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> post(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSStreamedHttpRequest.post(
+        uri,
+        headers: addContentTypeToHeaders(headers, body),
+        body: body ?? HttpPayload.empty(),
+      ).send(client),
+    );
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> put(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSStreamedHttpRequest.put(
+        uri,
+        headers: addContentTypeToHeaders(headers, body),
+        body: body ?? HttpPayload.empty(),
+      ).send(client),
+    );
+  }
 }
diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart
index 95e8f5c17d..c0285a6305 100644
--- a/packages/api/amplify_api/lib/src/method_channel_api.dart
+++ b/packages/api/amplify_api/lib/src/method_channel_api.dart
@@ -26,6 +26,7 @@ import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 
 import '../amplify_api.dart';
+import 'util.dart';
 
 part 'auth_token.dart';
 
@@ -282,19 +283,12 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
   }) {
     // Send Request cancelToken to Native
     String cancelToken = uuid();
-    // Ensure Content-Type header matches payload.
-    var modifiedHeaders = headers != null ? Map.of(headers) : null;
-    final contentType = body?.contentType;
-    if (contentType != null) {
-      modifiedHeaders = modifiedHeaders ?? {};
-      modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType);
-    }
     final responseFuture = _restResponseHelper(
       methodName: methodName,
       path: path,
       cancelToken: cancelToken,
       body: body,
-      headers: modifiedHeaders,
+      headers: addContentTypeToHeaders(headers, body),
       queryParameters: queryParameters,
       apiName: apiName,
     );
diff --git a/packages/api/amplify_api/lib/src/util.dart b/packages/api/amplify_api/lib/src/util.dart
new file mode 100644
index 0000000000..d91d58ce48
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/util.dart
@@ -0,0 +1,32 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:meta/meta.dart';
+
+/// Sets the 'Content-Type' of headers to match the [HttpPayload] body.
+@internal
+Map<String, String>? addContentTypeToHeaders(
+  Map<String, String>? headers,
+  HttpPayload? body,
+) {
+  final contentType = body?.contentType;
+  if (contentType == null) {
+    return headers;
+  }
+  // Create new map to avoid modifying input headers which may be unmodifiable.
+  final modifiedHeaders = Map<String, String>.of(headers ?? {});
+  modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType);
+  return modifiedHeaders;
+}
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index 2bc42e55cb..00041dcf57 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -18,6 +18,7 @@ dependencies:
   collection: ^1.15.0
   flutter:
     sdk: flutter
+  http: ^0.13.4
   meta: ^1.7.0
   plugin_platform_interface: ^2.0.0
 
diff --git a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
new file mode 100644
index 0000000000..d8c5162377
--- /dev/null
+++ b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
@@ -0,0 +1,103 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import 'dart:convert';
+
+import 'package:amplify_api/amplify_api.dart';
+import 'package:amplify_api/src/api_plugin_impl.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+
+import 'test_data/fake_amplify_configuration.dart';
+
+const _expectedRestResponseBody = '"Hello from Lambda!"';
+const _pathThatShouldFail = 'notHere';
+
+class MockAmplifyAPI extends AmplifyAPIDart {
+  @override
+  http.Client getRestClient({String? apiName}) => MockClient((request) async {
+        if (request.body.isNotEmpty) {
+          expect(request.headers['Content-Type'], 'application/json');
+        }
+        if (request.url.path.contains(_pathThatShouldFail)) {
+          return http.Response('Not found', 404);
+        }
+        return http.Response(_expectedRestResponseBody, 200);
+      });
+}
+
+void main() {
+  late AmplifyAPI api;
+
+  setUpAll(() async {
+    await Amplify.addPlugin(MockAmplifyAPI());
+    await Amplify.configure(amplifyconfig);
+  });
+  group('REST API', () {
+    Future<void> _verifyRestOperation(
+      CancelableOperation<AWSStreamedHttpResponse> operation,
+    ) async {
+      final response =
+          await operation.value.timeout(const Duration(seconds: 3));
+      final body = await response.decodeBody();
+      expect(body, _expectedRestResponseBody);
+      expect(response.statusCode, 200);
+    }
+
+    test('get() should get 200', () async {
+      final operation = Amplify.API.get('items');
+      await _verifyRestOperation(operation);
+    });
+
+    test('head() should get 200', () async {
+      final operation = Amplify.API.head('items');
+      final response = await operation.value;
+      expect(response.statusCode, 200);
+    });
+
+    test('patch() should get 200', () async {
+      final operation = Amplify.API
+          .patch('items', body: HttpPayload.json({'name': 'Mow the lawn'}));
+      await _verifyRestOperation(operation);
+    });
+
+    test('post() should get 200', () async {
+      final operation = Amplify.API
+          .post('items', body: HttpPayload.json({'name': 'Mow the lawn'}));
+      await _verifyRestOperation(operation);
+    });
+
+    test('put() should get 200', () async {
+      final operation = Amplify.API
+          .put('items', body: HttpPayload.json({'name': 'Mow the lawn'}));
+      await _verifyRestOperation(operation);
+    });
+
+    test('delete() should get 200', () async {
+      final operation = Amplify.API
+          .delete('items', body: HttpPayload.json({'name': 'Mow the lawn'}));
+      await _verifyRestOperation(operation);
+    });
+
+    test('canceled request should never resolve', () async {
+      final operation = Amplify.API.get('items');
+      operation.cancel();
+      operation.then((p0) => fail('Request should have been cancelled.'));
+      await operation.valueOrCancellation();
+      expect(operation.isCanceled, isTrue);
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
new file mode 100644
index 0000000000..7b8fd53be0
--- /dev/null
+++ b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
@@ -0,0 +1,79 @@
+const amplifyconfig = '''{
+  "UserAgent": "aws-amplify-cli/2.0",
+  "Version": "1.0",
+  "api": {
+      "plugins": {
+          "awsAPIPlugin": {
+              "apiIntegrationTestGraphQL": {
+                  "endpointType": "GraphQL",
+                  "endpoint": "https://abc123.appsync-api.us-east-1.amazonaws.com/graphql",
+                  "region": "us-east-1",
+                  "authorizationType": "API_KEY",
+                  "apiKey": "abc123"
+              },
+              "api123": {
+                  "endpointType": "REST",
+                  "endpoint": "https://abc123.execute-api.us-east-1.amazonaws.com/test",
+                  "region": "us-east-1",
+                  "authorizationType": "AWS_IAM"
+              }
+          }
+      }
+  },
+  "auth": {
+      "plugins": {
+          "awsCognitoAuthPlugin": {
+              "UserAgent": "aws-amplify-cli/0.1.0",
+              "Version": "0.1.0",
+              "IdentityManager": {
+                  "Default": {}
+              },
+              "AppSync": {
+                  "Default": {
+                      "ApiUrl": "https://abc123.appsync-api.us-east-1.amazonaws.com/graphql",
+                      "Region": "us-east-1",
+                      "AuthMode": "API_KEY",
+                      "ApiKey": "abc123",
+                      "ClientDatabasePrefix": "apiIntegrationTestGraphQL_API_KEY"
+                  }
+              },
+              "CredentialsProvider": {
+                  "CognitoIdentity": {
+                      "Default": {
+                          "PoolId": "us-east-1:abc123",
+                          "Region": "us-east-1"
+                      }
+                  }
+              },
+              "CognitoUserPool": {
+                  "Default": {
+                      "PoolId": "us-east-1_abc123",
+                      "AppClientId": "abc123",
+                      "Region": "us-east-1"
+                  }
+              },
+              "Auth": {
+                  "Default": {
+                      "authenticationFlowType": "USER_SRP_AUTH",
+                      "socialProviders": [],
+                      "usernameAttributes": [],
+                      "signupAttributes": [
+                          "EMAIL"
+                      ],
+                      "passwordProtectionSettings": {
+                          "passwordPolicyMinLength": 8,
+                          "passwordPolicyCharacters": []
+                      },
+                      "mfaConfiguration": "OFF",
+                      "mfaTypes": [
+                          "SMS"
+                      ],
+                      "verificationMechanisms": [
+                          "EMAIL"
+                      ]
+                  }
+              }
+          }
+      }
+  }
+}''';
diff --git a/packages/api/amplify_api/test/util_test.dart b/packages/api/amplify_api/test/util_test.dart
new file mode 100644
index 0000000000..062b8f7276
--- /dev/null
+++ b/packages/api/amplify_api/test/util_test.dart
@@ -0,0 +1,42 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+import 'package:amplify_api/src/util.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('addContentTypeToHeaders', () {
+    test('adds Content-Type header from payload', () {
+      final resultHeaders = addContentTypeToHeaders(
+          null, HttpPayload.json({'name': 'now the lawn'}));
+      expect(resultHeaders?['Content-Type'], 'application/json');
+    });
+
+    test('no-op when payload null', () {
+      final inputHeaders = {'foo': 'bar'};
+      final resultHeaders = addContentTypeToHeaders(inputHeaders, null);
+      expect(resultHeaders, inputHeaders);
+    });
+
+    test('does not change input headers', () {
+      final inputHeaders = {'foo': 'bar'};
+      final resultHeaders = addContentTypeToHeaders(
+          inputHeaders, HttpPayload.json({'name': 'now the lawn'}));
+      expect(resultHeaders?['Content-Type'], 'application/json');
+      expect(inputHeaders['Content-Type'], isNull);
+    });
+  });
+}

From e3c9cf6b665dc288ca170bb3a2c2a6ba0a390c63 Mon Sep 17 00:00:00 2001
From: Elijah Quartey <Equartey@users.noreply.github.com>
Date: Wed, 13 Jul 2022 15:27:11 -0500
Subject: [PATCH 05/33] feat!(api): GraphQL API key auth mode (#1858)

* feat(api): GraphQL API key auth mode

* BREAKING CHANGE: GraphQL response errors now nullable
---
 .../types/api/graphql/graphql_response.dart   |   9 +-
 packages/api/amplify_api/LICENSE              |  29 ++-
 .../amplify/amplify_api/MainActivityTest.kt   |  16 ++
 .../integration_test/graphql_tests.dart       |   8 +-
 .../provision_integration_test_resources.sh   |  14 ++
 .../lib/src/amplify_api_config.dart           |   3 +-
 .../amplify_authorization_rest_client.dart    |  14 +-
 .../amplify_api/lib/src/api_plugin_impl.dart  |  48 +++-
 .../src/graphql/graphql_response_decoder.dart |   2 +-
 .../src/graphql/model_mutations_factory.dart  |  14 ++
 .../lib/src/graphql/send_graphql_request.dart |  57 +++++
 .../lib/src/method_channel_api.dart           |  17 +-
 packages/api/amplify_api/lib/src/util.dart    |  18 ++
 .../test/amplify_api_config_test.dart         |  89 +++++++
 .../amplify_api/test/dart_graphql_test.dart   | 229 ++++++++++++++++++
 .../amplify_api/test/graphql_error_test.dart  |   2 +-
 .../query_predicate_graphql_filter_test.dart  |  14 ++
 .../test_data/fake_amplify_configuration.dart |  14 ++
 18 files changed, 568 insertions(+), 29 deletions(-)
 create mode 100644 packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
 create mode 100644 packages/api/amplify_api/test/amplify_api_config_test.dart
 create mode 100644 packages/api/amplify_api/test/dart_graphql_test.dart

diff --git a/packages/amplify_core/lib/src/types/api/graphql/graphql_response.dart b/packages/amplify_core/lib/src/types/api/graphql/graphql_response.dart
index 8a7580fd7d..dc8d2345d6 100644
--- a/packages/amplify_core/lib/src/types/api/graphql/graphql_response.dart
+++ b/packages/amplify_core/lib/src/types/api/graphql/graphql_response.dart
@@ -22,8 +22,8 @@ class GraphQLResponse<T> {
   /// This will be `null` if there are any GraphQL errors during execution.
   final T? data;
 
-  /// A list of errors from execution. If no errors, it will be an empty list.
-  final List<GraphQLResponseError> errors;
+  /// A list of errors from execution. If no errors, it will be `null`.
+  final List<GraphQLResponseError>? errors;
 
   const GraphQLResponse({
     this.data,
@@ -36,7 +36,10 @@ class GraphQLResponse<T> {
   }) {
     return GraphQLResponse(
       data: data,
-      errors: errors ?? const [],
+      errors: errors,
     );
   }
+
+  // Returns true when errors are present and not empty.
+  bool get hasErrors => errors != null && errors!.isNotEmpty;
 }
diff --git a/packages/api/amplify_api/LICENSE b/packages/api/amplify_api/LICENSE
index 19dc35b243..d645695673 100644
--- a/packages/api/amplify_api/LICENSE
+++ b/packages/api/amplify_api/LICENSE
@@ -172,4 +172,31 @@
       of any other Contributor, and only if You agree to indemnify,
       defend, and hold each Contributor harmless for any liability
       incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
\ No newline at end of file
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/packages/api/amplify_api/example/android/app/src/androidTest/java/com/amazonaws/amplify/amplify_api/MainActivityTest.kt b/packages/api/amplify_api/example/android/app/src/androidTest/java/com/amazonaws/amplify/amplify_api/MainActivityTest.kt
index 6f677739be..8b9960a876 100644
--- a/packages/api/amplify_api/example/android/app/src/androidTest/java/com/amazonaws/amplify/amplify_api/MainActivityTest.kt
+++ b/packages/api/amplify_api/example/android/app/src/androidTest/java/com/amazonaws/amplify/amplify_api/MainActivityTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.amazonaws.amplify.amplify_api_example
 
 import androidx.test.rule.ActivityTestRule
diff --git a/packages/api/amplify_api/example/integration_test/graphql_tests.dart b/packages/api/amplify_api/example/integration_test/graphql_tests.dart
index d632e2ef14..f1a9a42362 100644
--- a/packages/api/amplify_api/example/integration_test/graphql_tests.dart
+++ b/packages/api/amplify_api/example/integration_test/graphql_tests.dart
@@ -44,7 +44,7 @@ void main() {
       final req = GraphQLRequest<String>(
           document: graphQLDocument, variables: <String, String>{'id': id});
       final response = await Amplify.API.mutate(request: req).response;
-      if (response.errors.isNotEmpty) {
+      if (response.hasErrors) {
         fail(
             'GraphQL error while deleting a blog: ${response.errors.toString()}');
       }
@@ -561,7 +561,7 @@ void main() {
         // With stream established, exec callback with stream events.
         final subscription = await _getEstablishedSubscriptionOperation<T>(
             subscriptionRequest, (event) {
-          if (event.errors.isNotEmpty) {
+          if (event.hasErrors) {
             fail('subscription errors: ${event.errors}');
           }
           dataCompleter.complete(event);
@@ -657,6 +657,8 @@ void main() {
 
         expect(postFromEvent?.title, equals(title));
       });
-    });
+    },
+        skip:
+            'TODO(ragingsquirrel3): re-enable tests once subscriptions are implemented.');
   });
 }
diff --git a/packages/api/amplify_api/example/tool/provision_integration_test_resources.sh b/packages/api/amplify_api/example/tool/provision_integration_test_resources.sh
index 072ebabbda..d74e2dc37d 100755
--- a/packages/api/amplify_api/example/tool/provision_integration_test_resources.sh
+++ b/packages/api/amplify_api/example/tool/provision_integration_test_resources.sh
@@ -1,4 +1,18 @@
 #!/bin/bash
+# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 set -e
 IFS='|'
 
diff --git a/packages/api/amplify_api/lib/src/amplify_api_config.dart b/packages/api/amplify_api/lib/src/amplify_api_config.dart
index 960f11bf9b..4d4c21e9fa 100644
--- a/packages/api/amplify_api/lib/src/amplify_api_config.dart
+++ b/packages/api/amplify_api/lib/src/amplify_api_config.dart
@@ -29,7 +29,8 @@ class EndpointConfig with AWSEquatable<EndpointConfig> {
 
   /// Gets the host with environment path prefix from Amplify config and combines
   /// with [path] and [queryParameters] to return a full [Uri].
-  Uri getUri(String path, Map<String, dynamic>? queryParameters) {
+  Uri getUri({String? path, Map<String, dynamic>? queryParameters}) {
+    path = path ?? '';
     final parsed = Uri.parse(config.endpoint);
     // Remove leading slashes which are suggested in public documentation.
     // https://docs.amplify.aws/lib/restapi/getting-started/q/platform/flutter/#make-a-post-request
diff --git a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
index e58885385c..8a2d0678b5 100644
--- a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
+++ b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
@@ -18,6 +18,8 @@ import 'package:amplify_core/amplify_core.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
 
+const _xApiKey = 'X-Api-Key';
+
 /// Implementation of http [http.Client] that authorizes HTTP requests with
 /// Amplify.
 @internal
@@ -50,8 +52,16 @@ class AmplifyAuthorizationRestClient extends http.BaseClient
   http.BaseRequest _authorizeRequest(http.BaseRequest request) {
     if (!request.headers.containsKey(AWSHeaders.authorization) &&
         endpointConfig.authorizationType != APIAuthorizationType.none) {
-      // ignore: todo
-      // TODO: Use auth providers from core to transform the request.
+      // TODO(ragingsquirrel3): Use auth providers from core to transform the request.
+      final apiKey = endpointConfig.apiKey;
+      if (endpointConfig.authorizationType == APIAuthorizationType.apiKey) {
+        if (apiKey == null) {
+          throw const ApiException(
+              'Auth mode is API Key, but no API Key was found in config.');
+        }
+
+        request.headers.putIfAbsent(_xApiKey, () => apiKey);
+      }
     }
     return request;
   }
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index 0926c0a462..a54ad5ee2b 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -26,6 +26,7 @@ import 'package:meta/meta.dart';
 
 import 'amplify_api_config.dart';
 import 'amplify_authorization_rest_client.dart';
+import 'graphql/send_graphql_request.dart';
 import 'util.dart';
 
 /// {@template amplify_api.amplify_api_dart}
@@ -85,6 +86,19 @@ class AmplifyAPIDart extends AmplifyAPI {
     }
   }
 
+  /// Returns the HTTP client to be used for GraphQL operations.
+  ///
+  /// Use [apiName] if there are multiple GraphQL endpoints.
+  @visibleForTesting
+  http.Client getGraphQLClient({String? apiName}) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.graphQL,
+      apiName: apiName,
+    );
+    return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
+        endpointConfig: endpoint.config, baseClient: _baseHttpClient);
+  }
+
   /// Returns the HTTP client to be used for REST operations.
   ///
   /// Use [apiName] if there are multiple REST endpoints.
@@ -100,13 +114,21 @@ class AmplifyAPIDart extends AmplifyAPI {
     );
   }
 
+  Uri _getGraphQLUri(String? apiName) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.graphQL,
+      apiName: apiName,
+    );
+    return endpoint.getUri(path: null, queryParameters: null);
+  }
+
   Uri _getRestUri(
       String path, String? apiName, Map<String, dynamic>? queryParameters) {
     final endpoint = _apiConfig.getEndpoint(
       type: EndpointType.rest,
       apiName: apiName,
     );
-    return endpoint.getUri(path, queryParameters);
+    return endpoint.getUri(path: path, queryParameters: queryParameters);
   }
 
   /// NOTE: http does not support request abort https://github.com/dart-lang/http/issues/424.
@@ -130,6 +152,30 @@ class AmplifyAPIDart extends AmplifyAPI {
     _authProviders[authProvider.type] = authProvider;
   }
 
+  // ====== GraphQL ======
+
+  @override
+  CancelableOperation<GraphQLResponse<T>> query<T>(
+      {required GraphQLRequest<T> request}) {
+    final graphQLClient = getGraphQLClient(apiName: request.apiName);
+    final uri = _getGraphQLUri(request.apiName);
+
+    final responseFuture = sendGraphQLRequest<T>(
+        request: request, client: graphQLClient, uri: uri);
+    return _makeCancelable<GraphQLResponse<T>>(responseFuture);
+  }
+
+  @override
+  CancelableOperation<GraphQLResponse<T>> mutate<T>(
+      {required GraphQLRequest<T> request}) {
+    final graphQLClient = getGraphQLClient(apiName: request.apiName);
+    final uri = _getGraphQLUri(request.apiName);
+
+    final responseFuture = sendGraphQLRequest<T>(
+        request: request, client: graphQLClient, uri: uri);
+    return _makeCancelable<GraphQLResponse<T>>(responseFuture);
+  }
+
   // ====== REST =======
 
   @override
diff --git a/packages/api/amplify_api/lib/src/graphql/graphql_response_decoder.dart b/packages/api/amplify_api/lib/src/graphql/graphql_response_decoder.dart
index 3a66a4cafb..ec77157480 100644
--- a/packages/api/amplify_api/lib/src/graphql/graphql_response_decoder.dart
+++ b/packages/api/amplify_api/lib/src/graphql/graphql_response_decoder.dart
@@ -34,7 +34,7 @@ class GraphQLResponseDecoder {
   GraphQLResponse<T> decode<T>(
       {required GraphQLRequest request,
       String? data,
-      required List<GraphQLResponseError> errors}) {
+      List<GraphQLResponseError>? errors}) {
     if (data == null) {
       return GraphQLResponse(data: null, errors: errors);
     }
diff --git a/packages/api/amplify_api/lib/src/graphql/model_mutations_factory.dart b/packages/api/amplify_api/lib/src/graphql/model_mutations_factory.dart
index c0c2a4927a..1793cbee49 100644
--- a/packages/api/amplify_api/lib/src/graphql/model_mutations_factory.dart
+++ b/packages/api/amplify_api/lib/src/graphql/model_mutations_factory.dart
@@ -1,3 +1,17 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import 'package:amplify_api/src/graphql/graphql_request_factory.dart';
 import 'package:amplify_core/amplify_core.dart';
 
diff --git a/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart b/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
new file mode 100644
index 0000000000..6eab7deadd
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import 'dart:convert';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+import '../util.dart';
+import 'graphql_response_decoder.dart';
+
+/// Converts the [GraphQLRequest] to an HTTP POST request and sends with ///[client].
+@internal
+Future<GraphQLResponse<T>> sendGraphQLRequest<T>({
+  required GraphQLRequest<T> request,
+  required http.Client client,
+  required Uri uri,
+}) async {
+  try {
+    final body = {'variables': request.variables, 'query': request.document};
+    final graphQLResponse = await client.post(uri, body: json.encode(body));
+
+    final responseBody = json.decode(graphQLResponse.body);
+
+    if (responseBody is! Map<String, dynamic>) {
+      throw ApiException(
+          'unable to parse GraphQLResponse from server response which was not a JSON object.',
+          underlyingException: graphQLResponse.body);
+    }
+
+    final responseData = responseBody['data'];
+    // Preserve `null`. json.encode(null) returns "null" not `null`
+    final responseDataJson =
+        responseData != null ? json.encode(responseData) : null;
+
+    final errors = deserializeGraphQLResponseErrors(responseBody);
+
+    return GraphQLResponseDecoder.instance
+        .decode<T>(request: request, data: responseDataJson, errors: errors);
+  } on Exception catch (e) {
+    throw ApiException('unable to send GraphQLRequest to client.',
+        underlyingException: e.toString());
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart
index c0285a6305..45f5eb862f 100644
--- a/packages/api/amplify_api/lib/src/method_channel_api.dart
+++ b/packages/api/amplify_api/lib/src/method_channel_api.dart
@@ -207,7 +207,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
           AmplifyExceptionMessages.nullReturnedFromMethodChannel,
         );
       }
-      final errors = _deserializeGraphQLResponseErrors(result);
+      final errors = deserializeGraphQLResponseErrors(result);
 
       GraphQLResponse<T> response = GraphQLResponseDecoder.instance.decode<T>(
           request: request, data: result['data'] as String?, errors: errors);
@@ -466,19 +466,4 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
       );
     }
   }
-
-  List<GraphQLResponseError> _deserializeGraphQLResponseErrors(
-    Map<String, dynamic> response,
-  ) {
-    final errors = response['errors'] as List?;
-    if (errors == null || errors.isEmpty) {
-      return const [];
-    }
-    return errors
-        .cast<Map>()
-        .map((message) => GraphQLResponseError.fromJson(
-              message.cast<String, dynamic>(),
-            ))
-        .toList();
-  }
 }
diff --git a/packages/api/amplify_api/lib/src/util.dart b/packages/api/amplify_api/lib/src/util.dart
index d91d58ce48..2d28b59afc 100644
--- a/packages/api/amplify_api/lib/src/util.dart
+++ b/packages/api/amplify_api/lib/src/util.dart
@@ -30,3 +30,21 @@ Map<String, String>? addContentTypeToHeaders(
   modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType);
   return modifiedHeaders;
 }
+
+/// Grabs errors from GraphQL Response. Is used in method channel and Dart first code.
+/// TODO(Equartey): Move to Dart first code when method channel GraphQL implementation is removed.
+@internal
+List<GraphQLResponseError>? deserializeGraphQLResponseErrors(
+  Map<String, dynamic> response,
+) {
+  final errors = response['errors'] as List?;
+  if (errors == null || errors.isEmpty) {
+    return null;
+  }
+  return errors
+      .cast<Map>()
+      .map((message) => GraphQLResponseError.fromJson(
+            message.cast<String, dynamic>(),
+          ))
+      .toList();
+}
diff --git a/packages/api/amplify_api/test/amplify_api_config_test.dart b/packages/api/amplify_api/test/amplify_api_config_test.dart
new file mode 100644
index 0000000000..5168adfa04
--- /dev/null
+++ b/packages/api/amplify_api/test/amplify_api_config_test.dart
@@ -0,0 +1,89 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:convert';
+
+import 'package:amplify_api/src/amplify_api_config.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'test_data/fake_amplify_configuration.dart';
+
+void main() {
+  late EndpointConfig endpointConfig;
+
+  group('GraphQL Config', () {
+    const endpointType = EndpointType.graphQL;
+    const endpoint =
+        'https://abc123.appsync-api.us-east-1.amazonaws.com/graphql';
+    const region = 'us-east-1';
+    const authorizationType = APIAuthorizationType.apiKey;
+    const apiKey = 'abc-123';
+
+    setUpAll(() async {
+      const config = AWSApiConfig(
+          endpointType: endpointType,
+          endpoint: endpoint,
+          region: region,
+          authorizationType: authorizationType,
+          apiKey: apiKey);
+
+      endpointConfig = const EndpointConfig('GraphQL', config);
+    });
+
+    test('should return valid URI with null params', () async {
+      final uri = endpointConfig.getUri();
+      final expected = Uri.parse('$endpoint/');
+
+      expect(uri, equals(expected));
+    });
+  });
+
+  group('REST Config', () {
+    const endpointType = EndpointType.rest;
+    const endpoint = 'https://abc123.appsync-api.us-east-1.amazonaws.com/test';
+    const region = 'us-east-1';
+    const authorizationType = APIAuthorizationType.iam;
+
+    setUpAll(() async {
+      const config = AWSApiConfig(
+          endpointType: endpointType,
+          endpoint: endpoint,
+          region: region,
+          authorizationType: authorizationType);
+
+      endpointConfig = const EndpointConfig('REST', config);
+    });
+
+    test('should return valid URI with params', () async {
+      final path = 'path/to/nowhere';
+      final params = {'foo': 'bar', 'bar': 'baz'};
+      final uri = endpointConfig.getUri(path: path, queryParameters: params);
+
+      final expected = Uri.parse('$endpoint/$path?foo=bar&bar=baz');
+
+      expect(uri, equals(expected));
+    });
+
+    test('should handle a leading slash', () async {
+      final path = '/path/to/nowhere';
+      final params = {'foo': 'bar', 'bar': 'baz'};
+      final uri = endpointConfig.getUri(path: path, queryParameters: params);
+
+      final expected = Uri.parse('$endpoint$path?foo=bar&bar=baz');
+
+      expect(uri, equals(expected));
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/dart_graphql_test.dart b/packages/api/amplify_api/test/dart_graphql_test.dart
new file mode 100644
index 0000000000..bedd0092f2
--- /dev/null
+++ b/packages/api/amplify_api/test/dart_graphql_test.dart
@@ -0,0 +1,229 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:convert';
+
+import 'package:amplify_api/amplify_api.dart';
+import 'package:amplify_api/src/api_plugin_impl.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:amplify_test/test_models/ModelProvider.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+
+import 'test_data/fake_amplify_configuration.dart';
+
+final _deepEquals = const DeepCollectionEquality().equals;
+
+// Success Mocks
+const _expectedQuerySuccessResponseBody = {
+  'data': {
+    'listBlogs': {
+      'items': [
+        {
+          'id': 'TEST_ID',
+          'name': 'Test App Blog',
+          'createdAt': '2022-06-28T17:36:52.460Z'
+        }
+      ]
+    }
+  }
+};
+
+final _modelQueryId = uuid();
+final _expectedModelQueryResult = {
+  'data': {
+    'getBlog': {
+      'createdAt': '2021-07-21T22:23:33.707Z',
+      'id': _modelQueryId,
+      'name': 'Test App Blog'
+    }
+  }
+};
+const _expectedMutateSuccessResponseBody = {
+  'data': {
+    'createBlog': {
+      'id': 'TEST_ID',
+      'name': 'Test App Blog',
+      'createdAt': '2022-07-06T18:42:26.126Z'
+    }
+  }
+};
+
+// Error Mocks
+const _errorMessage = 'Unable to parse GraphQL query.';
+const _errorLocations = [
+  {'line': 2, 'column': 3},
+  {'line': 4, 'column': 5}
+];
+const _errorPath = ['a', 1, 'b'];
+const _errorExtensions = {
+  'a': 'blah',
+  'b': {'c': 'd'}
+};
+const _expectedErrorResponseBody = {
+  'data': null,
+  'errors': [
+    {
+      'message': _errorMessage,
+      'locations': _errorLocations,
+      'path': _errorPath,
+      'extensions': _errorExtensions,
+    },
+  ]
+};
+
+class MockAmplifyAPI extends AmplifyAPIDart {
+  MockAmplifyAPI({
+    ModelProviderInterface? modelProvider,
+  }) : super(modelProvider: modelProvider);
+
+  @override
+  http.Client getGraphQLClient({String? apiName}) =>
+      MockClient((request) async {
+        if (request.body.contains('getBlog')) {
+          return http.Response(json.encode(_expectedModelQueryResult), 200);
+        }
+        if (request.body.contains('TestMutate')) {
+          return http.Response(
+              json.encode(_expectedMutateSuccessResponseBody), 400);
+        }
+        if (request.body.contains('TestError')) {
+          return http.Response(json.encode(_expectedErrorResponseBody), 400);
+        }
+
+        return http.Response(
+            json.encode(_expectedQuerySuccessResponseBody), 200);
+      });
+}
+
+void main() {
+  setUpAll(() async {
+    await Amplify.addPlugin(MockAmplifyAPI(
+      modelProvider: ModelProvider.instance,
+    ));
+    await Amplify.configure(amplifyconfig);
+  });
+  group('Vanilla GraphQL', () {
+    test('Query returns proper response.data', () async {
+      String graphQLDocument = ''' query TestQuery {
+          listBlogs {
+            items {
+              id
+              name
+              createdAt
+            }
+          }
+        } ''';
+      final req = GraphQLRequest(document: graphQLDocument, variables: {});
+
+      final operation = Amplify.API.query(request: req);
+      final res = await operation.value;
+
+      final expected = json.encode(_expectedQuerySuccessResponseBody['data']);
+
+      expect(res.data, equals(expected));
+      expect(res.errors, equals(null));
+    });
+
+    test('Mutate returns proper response.data', () async {
+      String graphQLDocument = ''' mutation TestMutate(\$name: String!) {
+          createBlog(input: {name: \$name}) {
+            id
+            name
+            createdAt
+          }
+        } ''';
+      final graphQLVariables = {'name': 'Test Blog 1'};
+      final req = GraphQLRequest(
+          document: graphQLDocument, variables: graphQLVariables);
+
+      final operation = Amplify.API.mutate(request: req);
+      final res = await operation.value;
+
+      final expected = json.encode(_expectedMutateSuccessResponseBody['data']);
+
+      expect(res.data, equals(expected));
+      expect(res.errors, equals(null));
+    });
+  });
+  group('Model Helpers', () {
+    const blogSelectionSet =
+        'id name createdAt file { bucket region key meta { name } } files { bucket region key meta { name } } updatedAt';
+
+    test('Query returns proper response.data for Models', () async {
+      const expectedDoc =
+          'query getBlog(\$id: ID!) { getBlog(id: \$id) { $blogSelectionSet } }';
+      const decodePath = 'getBlog';
+
+      GraphQLRequest<Blog> req =
+          ModelQueries.get<Blog>(Blog.classType, _modelQueryId);
+
+      final operation = Amplify.API.query(request: req);
+      final res = await operation.value;
+
+      // request asserts
+      expect(req.document, expectedDoc);
+      expect(_deepEquals(req.variables, {'id': _modelQueryId}), isTrue);
+      expect(req.modelType, Blog.classType);
+      expect(req.decodePath, decodePath);
+      // response asserts
+      expect(res.data, isA<Blog>());
+      expect(res.data?.id, _modelQueryId);
+      expect(res.errors, equals(null));
+    });
+  });
+
+  group('Error Handling', () {
+    test('response errors are decoded', () async {
+      String graphQLDocument = ''' TestError ''';
+      final req = GraphQLRequest(document: graphQLDocument, variables: {});
+
+      final operation = Amplify.API.query(request: req);
+      final res = await operation.value;
+
+      const errorExpected = GraphQLResponseError(
+        message: _errorMessage,
+        locations: [
+          GraphQLResponseErrorLocation(2, 3),
+          GraphQLResponseErrorLocation(4, 5),
+        ],
+        path: <dynamic>[..._errorPath],
+        extensions: <String, dynamic>{..._errorExtensions},
+      );
+
+      expect(res.data, equals(null));
+      expect(res.errors?.single, equals(errorExpected));
+    });
+
+    test('canceled query request should never resolve', () async {
+      final req = GraphQLRequest(document: '', variables: {});
+      final operation = Amplify.API.query(request: req);
+      operation.cancel();
+      operation.then((p0) => fail('Request should have been cancelled.'));
+      await operation.valueOrCancellation();
+      expect(operation.isCanceled, isTrue);
+    });
+
+    test('canceled mutation request should never resolve', () async {
+      final req = GraphQLRequest(document: '', variables: {});
+      final operation = Amplify.API.mutate(request: req);
+      operation.cancel();
+      operation.then((p0) => fail('Request should have been cancelled.'));
+      await operation.valueOrCancellation();
+      expect(operation.isCanceled, isTrue);
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/graphql_error_test.dart b/packages/api/amplify_api/test/graphql_error_test.dart
index ee6588691a..32752299ee 100644
--- a/packages/api/amplify_api/test/graphql_error_test.dart
+++ b/packages/api/amplify_api/test/graphql_error_test.dart
@@ -68,6 +68,6 @@ void main() {
         .response;
 
     expect(resp.data, equals(null));
-    expect(resp.errors.single, equals(expected));
+    expect(resp.errors?.single, equals(expected));
   });
 }
diff --git a/packages/api/amplify_api/test/query_predicate_graphql_filter_test.dart b/packages/api/amplify_api/test/query_predicate_graphql_filter_test.dart
index acf8cf18a8..850fd5e1a4 100644
--- a/packages/api/amplify_api/test/query_predicate_graphql_filter_test.dart
+++ b/packages/api/amplify_api/test/query_predicate_graphql_filter_test.dart
@@ -1,3 +1,17 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import 'package:amplify_api/src/graphql/graphql_request_factory.dart';
 import 'package:amplify_flutter/amplify_flutter.dart';
 import 'package:amplify_flutter/src/amplify_impl.dart';
diff --git a/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
index 7b8fd53be0..0b3c0dae01 100644
--- a/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
+++ b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
@@ -1,3 +1,17 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 const amplifyconfig = '''{
   "UserAgent": "aws-amplify-cli/2.0",
   "Version": "1.0",

From 18e427278213451093510ed113de2db566447e38 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Tue, 19 Jul 2022 08:42:49 -0800
Subject: [PATCH 06/33] feat!(core,auth): auth providers definition and
 CognitoIamAuthProvider registers in Auth (#1851)

---
 .../amplify_flutter/lib/src/hybrid_impl.dart  |   3 +-
 packages/amplify_core/lib/amplify_core.dart   |   3 +
 .../lib/src/amplify_class_impl.dart           |   8 +-
 .../src/plugin/amplify_plugin_interface.dart  |   5 +-
 .../api/auth/api_authorization_type.dart      |  18 ++-
 .../types/common/amplify_auth_provider.dart   |  79 +++++++++++
 packages/amplify_core/pubspec.yaml            |   2 +-
 .../test/amplify_auth_provider_test.dart      | 132 ++++++++++++++++++
 .../amplify_api/lib/src/api_plugin_impl.dart  |   5 +-
 .../lib/src/auth_plugin_impl.dart             |  14 +-
 .../src/util/cognito_iam_auth_provider.dart   |  83 +++++++++++
 .../test/plugin/auth_providers_test.dart      | 112 +++++++++++++++
 .../test/plugin/delete_user_test.dart         |  17 ++-
 .../test/plugin/sign_out_test.dart            |  52 +++++--
 14 files changed, 507 insertions(+), 26 deletions(-)
 create mode 100644 packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
 create mode 100644 packages/amplify_core/test/amplify_auth_provider_test.dart
 create mode 100644 packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart
 create mode 100644 packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart

diff --git a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
index 5eb3f1257e..8c166f03f6 100644
--- a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
+++ b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
@@ -36,7 +36,8 @@ class AmplifyHybridImpl extends AmplifyClassImpl {
       [
         ...API.plugins,
         ...Auth.plugins,
-      ].map((p) => p.configure(config: amplifyConfig)),
+      ].map((p) => p.configure(
+          config: amplifyConfig, authProviderRepo: authProviderRepo)),
       eagerError: true,
     );
     await _methodChannelAmplify.configurePlatform(config);
diff --git a/packages/amplify_core/lib/amplify_core.dart b/packages/amplify_core/lib/amplify_core.dart
index c2bf72c5b1..787626b3c0 100644
--- a/packages/amplify_core/lib/amplify_core.dart
+++ b/packages/amplify_core/lib/amplify_core.dart
@@ -77,6 +77,9 @@ export 'src/types/api/api_types.dart';
 /// Auth
 export 'src/types/auth/auth_types.dart';
 
+/// Auth providers
+export 'src/types/common/amplify_auth_provider.dart';
+
 /// Datastore
 export 'src/types/datastore/datastore_types.dart' hide DateTimeParse;
 
diff --git a/packages/amplify_core/lib/src/amplify_class_impl.dart b/packages/amplify_core/lib/src/amplify_class_impl.dart
index d802d4a69d..00c9cba346 100644
--- a/packages/amplify_core/lib/src/amplify_class_impl.dart
+++ b/packages/amplify_core/lib/src/amplify_class_impl.dart
@@ -24,6 +24,11 @@ import 'package:meta/meta.dart';
 /// {@endtemplate}
 @internal
 class AmplifyClassImpl extends AmplifyClass {
+  /// Share AmplifyAuthProviders with plugins.
+  @protected
+  final AmplifyAuthProviderRepository authProviderRepo =
+      AmplifyAuthProviderRepository();
+
   /// {@macro amplify_flutter.amplify_class_impl}
   AmplifyClassImpl() : super.protected();
 
@@ -57,7 +62,8 @@ class AmplifyClassImpl extends AmplifyClass {
         ...Auth.plugins,
         ...DataStore.plugins,
         ...Storage.plugins,
-      ].map((p) => p.configure(config: amplifyConfig)),
+      ].map((p) => p.configure(
+          config: amplifyConfig, authProviderRepo: authProviderRepo)),
       eagerError: true,
     );
   }
diff --git a/packages/amplify_core/lib/src/plugin/amplify_plugin_interface.dart b/packages/amplify_core/lib/src/plugin/amplify_plugin_interface.dart
index 821c6fe38e..4ca5f7c2a1 100644
--- a/packages/amplify_core/lib/src/plugin/amplify_plugin_interface.dart
+++ b/packages/amplify_core/lib/src/plugin/amplify_plugin_interface.dart
@@ -30,7 +30,10 @@ abstract class AmplifyPluginInterface {
   Future<void> addPlugin() async {}
 
   /// Configures the plugin using the registered [config].
-  Future<void> configure({AmplifyConfig? config}) async {}
+  Future<void> configure({
+    AmplifyConfig? config,
+    required AmplifyAuthProviderRepository authProviderRepo,
+  }) async {}
 
   /// Resets the plugin by removing all traces of it from the device.
   @visibleForTesting
diff --git a/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart b/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
index e81ef856f4..f15da13b9f 100644
--- a/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
+++ b/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
@@ -13,6 +13,7 @@
  * permissions and limitations under the License.
  */
 
+import 'package:amplify_core/src/types/common/amplify_auth_provider.dart';
 import 'package:collection/collection.dart';
 import 'package:json_annotation/json_annotation.dart';
 
@@ -24,17 +25,17 @@ part 'api_authorization_type.g.dart';
 /// See also:
 /// - [AppSync Security](https://docs.aws.amazon.com/appsync/latest/devguide/security.html)
 @JsonEnum(alwaysCreate: true)
-enum APIAuthorizationType {
+enum APIAuthorizationType<T extends AmplifyAuthProvider> {
   /// For public APIs.
   @JsonValue('NONE')
-  none,
+  none(AmplifyAuthProviderToken<AmplifyAuthProvider>()),
 
   /// A hardcoded key which can provide throttling for an unauthenticated API.
   ///
   /// See also:
   /// - [API Key Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#api-key-authorization)
   @JsonValue('API_KEY')
-  apiKey,
+  apiKey(AmplifyAuthProviderToken<AmplifyAuthProvider>()),
 
   /// Use an IAM access/secret key credential pair to authorize access to an API.
   ///
@@ -42,7 +43,7 @@ enum APIAuthorizationType {
   /// - [IAM Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security.html#aws-iam-authorization)
   /// - [IAM Introduction](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)
   @JsonValue('AWS_IAM')
-  iam,
+  iam(AmplifyAuthProviderToken<AWSIamAmplifyAuthProvider>()),
 
   /// OpenID Connect is a simple identity layer on top of OAuth2.0.
   ///
@@ -50,21 +51,24 @@ enum APIAuthorizationType {
   /// - [OpenID Connect Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#openid-connect-authorization)
   /// - [OpenID Connect Specification](https://openid.net/specs/openid-connect-core-1_0.html)
   @JsonValue('OPENID_CONNECT')
-  oidc,
+  oidc(AmplifyAuthProviderToken<TokenAmplifyAuthProvider>()),
 
   /// Control access to date by putting users into different permissions pools.
   ///
   /// See also:
   /// - [Amazon Cognito User Pools](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#amazon-cognito-user-pools-authorization)
   @JsonValue('AMAZON_COGNITO_USER_POOLS')
-  userPools,
+  userPools(AmplifyAuthProviderToken<TokenAmplifyAuthProvider>()),
 
   /// Control access by calling a lambda function.
   ///
   /// See also:
   /// - [Introducing Lambda authorization for AWS AppSync GraphQL APIs](https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/)
   @JsonValue('AWS_LAMBDA')
-  function
+  function(AmplifyAuthProviderToken<TokenAmplifyAuthProvider>());
+
+  const APIAuthorizationType(this.authProviderToken);
+  final AmplifyAuthProviderToken<T> authProviderToken;
 }
 
 /// Helper methods for [APIAuthorizationType].
diff --git a/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart b/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
new file mode 100644
index 0000000000..30c00ff053
--- /dev/null
+++ b/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:aws_signature_v4/aws_signature_v4.dart';
+
+/// An identifier to use as a key in an [AmplifyAuthProviderRepository] so that
+/// a retrieved auth provider can be typed more accurately.
+class AmplifyAuthProviderToken<T extends AmplifyAuthProvider> extends Token<T> {
+  const AmplifyAuthProviderToken();
+}
+
+abstract class AuthProviderOptions {
+  const AuthProviderOptions();
+}
+
+/// Options required by IAM to sign any given request at runtime.
+class IamAuthProviderOptions extends AuthProviderOptions {
+  final String region;
+  final AWSService service;
+
+  const IamAuthProviderOptions({required this.region, required this.service});
+}
+
+abstract class AmplifyAuthProvider {
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant AuthProviderOptions? options,
+  });
+}
+
+abstract class AWSIamAmplifyAuthProvider extends AmplifyAuthProvider
+    implements AWSCredentialsProvider {
+  @override
+  Future<AWSSignedRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant IamAuthProviderOptions options,
+  });
+}
+
+abstract class TokenAmplifyAuthProvider extends AmplifyAuthProvider {
+  Future<String> getLatestAuthToken();
+
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant AuthProviderOptions? options,
+  }) async {
+    final token = await getLatestAuthToken();
+    request.headers.putIfAbsent(AWSHeaders.authorization, () => token);
+    return request;
+  }
+}
+
+class AmplifyAuthProviderRepository {
+  final Map<AmplifyAuthProviderToken, AmplifyAuthProvider> _authProviders = {};
+
+  T? getAuthProvider<T extends AmplifyAuthProvider>(
+      AmplifyAuthProviderToken<T> token) {
+    return _authProviders[token] as T?;
+  }
+
+  void registerAuthProvider<T extends AmplifyAuthProvider>(
+      AmplifyAuthProviderToken<T> token, AmplifyAuthProvider authProvider) {
+    _authProviders[token] = authProvider;
+  }
+}
diff --git a/packages/amplify_core/pubspec.yaml b/packages/amplify_core/pubspec.yaml
index f3df87c4cb..0c9ca29999 100644
--- a/packages/amplify_core/pubspec.yaml
+++ b/packages/amplify_core/pubspec.yaml
@@ -11,7 +11,7 @@ dependencies:
   aws_common: ^0.1.0
   aws_signature_v4: ^0.1.0
   collection: ^1.15.0
-  http: ^0.13.0
+  http: ^0.13.4
   intl: ^0.17.0
   json_annotation: ^4.4.0
   logging: ^1.0.0
diff --git a/packages/amplify_core/test/amplify_auth_provider_test.dart b/packages/amplify_core/test/amplify_auth_provider_test.dart
new file mode 100644
index 0000000000..08a0e06e4d
--- /dev/null
+++ b/packages/amplify_core/test/amplify_auth_provider_test.dart
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:aws_signature_v4/aws_signature_v4.dart';
+import 'package:test/test.dart';
+
+const _testAuthKey = 'TestAuthKey';
+const _testToken = 'abc123-fake-token';
+
+AWSHttpRequest _generateTestRequest() {
+  return AWSHttpRequest(
+    method: AWSHttpMethod.get,
+    uri: Uri.parse('https://www.amazon.com'),
+  );
+}
+
+class TestAuthProvider extends AmplifyAuthProvider {
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant AuthProviderOptions? options,
+  }) async {
+    request.headers.putIfAbsent(_testAuthKey, () => 'foo');
+    return request;
+  }
+}
+
+class SecondTestAuthProvider extends AmplifyAuthProvider {
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant AuthProviderOptions? options,
+  }) async {
+    request.headers.putIfAbsent(_testAuthKey, () => 'bar');
+    return request;
+  }
+}
+
+class TestAWSCredentialsAuthProvider extends AWSIamAmplifyAuthProvider {
+  @override
+  Future<AWSCredentials> retrieve() async {
+    return const AWSCredentials(
+        'fake-access-key-123', 'fake-secret-access-key-456');
+  }
+
+  @override
+  Future<AWSSignedRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant IamAuthProviderOptions? options,
+  }) async {
+    request.headers.putIfAbsent(_testAuthKey, () => 'foo');
+    return request as AWSSignedRequest;
+  }
+}
+
+class TestTokenProvider extends TokenAmplifyAuthProvider {
+  @override
+  Future<String> getLatestAuthToken() async {
+    return _testToken;
+  }
+}
+
+void main() {
+  final authProvider = TestAuthProvider();
+
+  group('AmplifyAuthProvider', () {
+    test('can authorize an HTTP request', () async {
+      final authorizedRequest =
+          await authProvider.authorizeRequest(_generateTestRequest());
+      expect(authorizedRequest.headers[_testAuthKey], 'foo');
+    });
+  });
+
+  group('TokenAmplifyAuthProvider', () {
+    test('will assign the token to the "Authorization" header', () async {
+      final tokenAuthProvider = TestTokenProvider();
+      final authorizedRequest =
+          await tokenAuthProvider.authorizeRequest(_generateTestRequest());
+      expect(authorizedRequest.headers[AWSHeaders.authorization], _testToken);
+    });
+  });
+
+  group('AmplifyAuthProviderRepository', () {
+    test('can register a valid auth provider and use to retrieve', () async {
+      final authRepo = AmplifyAuthProviderRepository();
+
+      const providerKey = AmplifyAuthProviderToken();
+      authRepo.registerAuthProvider(providerKey, authProvider);
+      final actualAuthProvider = authRepo.getAuthProvider(providerKey);
+      final authorizedRequest =
+          await actualAuthProvider!.authorizeRequest(_generateTestRequest());
+      expect(authorizedRequest.headers[_testAuthKey], 'foo');
+    });
+
+    test('will correctly type the retrieved auth provider', () async {
+      final authRepo = AmplifyAuthProviderRepository();
+
+      final credentialAuthProvider = TestAWSCredentialsAuthProvider();
+      const providerKey = AmplifyAuthProviderToken<AWSIamAmplifyAuthProvider>();
+      authRepo.registerAuthProvider(providerKey, credentialAuthProvider);
+      AWSIamAmplifyAuthProvider? actualAuthProvider =
+          authRepo.getAuthProvider(providerKey);
+      expect(actualAuthProvider, isA<AWSIamAmplifyAuthProvider>());
+    });
+
+    test('will overwrite previous provider in same key', () async {
+      final authRepo = AmplifyAuthProviderRepository();
+
+      const providerKey = AmplifyAuthProviderToken();
+      authRepo.registerAuthProvider(providerKey, authProvider);
+      authRepo.registerAuthProvider(providerKey, SecondTestAuthProvider());
+      final actualAuthProvider = authRepo.getAuthProvider(providerKey);
+
+      final authorizedRequest =
+          await actualAuthProvider!.authorizeRequest(_generateTestRequest());
+      expect(authorizedRequest.headers[_testAuthKey], 'bar');
+    });
+  });
+}
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index a54ad5ee2b..a5dfd58ce6 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -54,7 +54,10 @@ class AmplifyAPIDart extends AmplifyAPI {
   }
 
   @override
-  Future<void> configure({AmplifyConfig? config}) async {
+  Future<void> configure({
+    AmplifyConfig? config,
+    required AmplifyAuthProviderRepository authProviderRepo,
+  }) async {
     final apiConfig = config?.api?.awsPlugin;
     if (apiConfig == null) {
       throw const ApiException('No AWS API config found',
diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
index cfd86898f0..bec1774a51 100644
--- a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
+++ b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
@@ -50,6 +50,7 @@ import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart
         VerifyUserAttributeRequest;
 import 'package:amplify_auth_cognito_dart/src/sdk/sdk_bridge.dart';
 import 'package:amplify_auth_cognito_dart/src/state/state.dart';
+import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:amplify_secure_storage_dart/amplify_secure_storage_dart.dart';
 import 'package:built_collection/built_collection.dart';
@@ -169,10 +170,21 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
   }
 
   @override
-  Future<void> configure({AmplifyConfig? config}) async {
+  Future<void> configure({
+    AmplifyConfig? config,
+    required AmplifyAuthProviderRepository authProviderRepo,
+  }) async {
     if (config == null) {
       throw const AuthException('No Cognito plugin config detected');
     }
+
+    // Register auth providers to provide auth functionality to other plugins
+    // without requiring other plugins to call `Amplify.Auth...` directly.
+    authProviderRepo.registerAuthProvider(
+      APIAuthorizationType.iam.authProviderToken,
+      CognitoIamAuthProvider(),
+    );
+
     if (_stateMachine.getOrCreate(AuthStateMachine.type).currentState.type !=
         AuthStateType.notConfigured) {
       throw const AmplifyAlreadyConfiguredException(
diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart
new file mode 100644
index 0000000000..b50be60932
--- /dev/null
+++ b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart
@@ -0,0 +1,83 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:aws_signature_v4/aws_signature_v4.dart';
+import 'package:meta/meta.dart';
+
+/// [AmplifyAuthProvider] implementation that signs a request using AWS credentials
+/// from `Amplify.Auth.fetchAuthSession()` or allows getting credentials directly.
+@internal
+class CognitoIamAuthProvider extends AWSIamAmplifyAuthProvider {
+  /// AWS credentials from Auth category.
+  @override
+  Future<AWSCredentials> retrieve() async {
+    final authSession = await Amplify.Auth.fetchAuthSession(
+      options: const CognitoSessionOptions(getAWSCredentials: true),
+    ) as CognitoAuthSession;
+    final credentials = authSession.credentials;
+    if (credentials == null) {
+      throw const InvalidCredentialsException(
+        'Unable to authorize request with IAM. No AWS credentials.',
+      );
+    }
+    return credentials;
+  }
+
+  /// Signs request with AWSSigV4Signer and AWS credentials from `.getCredentials()`.
+  @override
+  Future<AWSSignedRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    IamAuthProviderOptions? options,
+  }) async {
+    if (options == null) {
+      throw const AuthException(
+        'Unable to authorize request with IAM. No region or service provided.',
+      );
+    }
+
+    return _signRequest(
+      request,
+      region: options.region,
+      service: options.service,
+      credentials: await retrieve(),
+    );
+  }
+
+  /// Takes input [request] as canonical request and generates a signed version.
+  Future<AWSSignedRequest> _signRequest(
+    AWSBaseHttpRequest request, {
+    required String region,
+    required AWSService service,
+    required AWSCredentials credentials,
+  }) {
+    // Create signer helper params.
+    final signer = AWSSigV4Signer(
+      credentialsProvider: AWSCredentialsProvider(credentials),
+    );
+    final scope = AWSCredentialScope(
+      region: region,
+      service: service,
+    );
+
+    // Finally, create and sign canonical request.
+    return signer.sign(
+      request,
+      credentialScope: scope,
+    );
+  }
+}
diff --git a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
new file mode 100644
index 0000000000..acb126fa66
--- /dev/null
+++ b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
@@ -0,0 +1,112 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import 'dart:async';
+
+import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart'
+    hide InternalErrorException;
+import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:test/test.dart';
+
+import '../common/mock_config.dart';
+import '../common/mock_secure_storage.dart';
+
+AWSHttpRequest _generateTestRequest() {
+  return AWSHttpRequest(
+    method: AWSHttpMethod.get,
+    uri: Uri.parse('https://www.amazon.com'),
+  );
+}
+
+/// Returns dummy AWS credentials.
+class TestAmplifyAuth extends AmplifyAuthCognitoDart {
+  @override
+  Future<AuthSession> fetchAuthSession({
+    required AuthSessionRequest request,
+  }) async {
+    return const CognitoAuthSession(
+      isSignedIn: true,
+      credentials: AWSCredentials('fakeKeyId', 'fakeSecret'),
+    );
+  }
+}
+
+void main() {
+  group(
+      'AmplifyAuthCognitoDart plugin registers auth providers during configuration',
+      () {
+    late AmplifyAuthCognitoDart plugin;
+
+    setUp(() async {
+      plugin = AmplifyAuthCognitoDart(credentialStorage: MockSecureStorage());
+    });
+
+    test('registers CognitoIamAuthProvider', () async {
+      final testAuthRepo = AmplifyAuthProviderRepository();
+      await plugin.configure(
+        config: mockConfig,
+        authProviderRepo: testAuthRepo,
+      );
+      final authProvider = testAuthRepo.getAuthProvider(
+        APIAuthorizationType.iam.authProviderToken,
+      );
+      expect(authProvider, isA<CognitoIamAuthProvider>());
+    });
+  });
+
+  group('CognitoIamAuthProvider', () {
+    setUpAll(() async {
+      await Amplify.addPlugin(TestAmplifyAuth());
+    });
+
+    test('gets AWS credentials from Amplify.Auth.fetchAuthSession', () async {
+      final authProvider = CognitoIamAuthProvider();
+      final credentials = await authProvider.retrieve();
+      expect(credentials.accessKeyId, isA<String>());
+      expect(credentials.secretAccessKey, isA<String>());
+    });
+
+    test('signs a request when calling authorizeRequest', () async {
+      final authProvider = CognitoIamAuthProvider();
+      final authorizedRequest = await authProvider.authorizeRequest(
+        _generateTestRequest(),
+        options: const IamAuthProviderOptions(
+          region: 'us-east-1',
+          service: AWSService.appSync,
+        ),
+      );
+      // Note: not intended to be complete test of sigv4 algorithm.
+      expect(authorizedRequest.headers[AWSHeaders.authorization], isNotEmpty);
+      const userAgentHeader =
+          zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+      expect(
+        authorizedRequest.headers[AWSHeaders.host],
+        isNotEmpty,
+        skip: zIsWeb,
+      );
+      expect(
+        authorizedRequest.headers[userAgentHeader],
+        contains('aws-sigv4'),
+      );
+    });
+
+    test('throws when no options provided', () async {
+      final authProvider = CognitoIamAuthProvider();
+      await expectLater(
+        authProvider.authorizeRequest(_generateTestRequest()),
+        throwsA(isA<AuthException>()),
+      );
+    });
+  });
+}
diff --git a/packages/auth/amplify_auth_cognito_dart/test/plugin/delete_user_test.dart b/packages/auth/amplify_auth_cognito_dart/test/plugin/delete_user_test.dart
index b589b6b110..15e08de206 100644
--- a/packages/auth/amplify_auth_cognito_dart/test/plugin/delete_user_test.dart
+++ b/packages/auth/amplify_auth_cognito_dart/test/plugin/delete_user_test.dart
@@ -58,6 +58,8 @@ void main() {
   late StreamController<AuthHubEvent> hubEventsController;
   late Stream<AuthHubEvent> hubEvents;
 
+  final testAuthRepo = AmplifyAuthProviderRepository();
+
   final userDeletedEvent = isA<AuthHubEvent>().having(
     (event) => event.type,
     'type',
@@ -83,7 +85,10 @@ void main() {
 
     group('deleteUser', () {
       test('throws when signed out', () async {
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
         await expectLater(plugin.deleteUser(), throwsSignedOutException);
 
         expect(hubEvents, neverEmits(userDeletedEvent));
@@ -96,7 +101,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(() async {});
         stateMachine.addInstance<CognitoIdentityProviderClient>(mockIdp);
@@ -113,7 +121,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(() async {
           throw InternalErrorException();
diff --git a/packages/auth/amplify_auth_cognito_dart/test/plugin/sign_out_test.dart b/packages/auth/amplify_auth_cognito_dart/test/plugin/sign_out_test.dart
index 6c9f3fe3a2..fe14fc98be 100644
--- a/packages/auth/amplify_auth_cognito_dart/test/plugin/sign_out_test.dart
+++ b/packages/auth/amplify_auth_cognito_dart/test/plugin/sign_out_test.dart
@@ -69,6 +69,8 @@ void main() {
   late StreamController<AuthHubEvent> hubEventsController;
   late Stream<AuthHubEvent> hubEvents;
 
+  final testAuthRepo = AmplifyAuthProviderRepository();
+
   final emitsSignOutEvent = emitsThrough(
     isA<AuthHubEvent>().having(
       (event) => event.type,
@@ -112,14 +114,20 @@ void main() {
 
     group('signOut', () {
       test('completes when already signed out', () async {
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
         expect(plugin.signOut(), completes);
         expect(hubEvents, emitsSignOutEvent);
       });
 
       test('does not clear AWS creds when already signed out', () async {
         seedStorage(secureStorage, identityPoolKeys: identityPoolKeys);
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
         await expectLater(plugin.signOut(), completes);
         expect(hubEvents, emitsSignOutEvent);
 
@@ -144,7 +152,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(
           globalSignOut: () async => GlobalSignOutResponse(),
@@ -165,7 +176,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(
           globalSignOut:
@@ -194,7 +208,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(
           globalSignOut: () async => GlobalSignOutResponse(),
@@ -217,7 +234,10 @@ void main() {
 
       test('can sign out in user pool-only mode', () async {
         seedStorage(secureStorage, userPoolKeys: userPoolKeys);
-        await plugin.configure(config: userPoolOnlyConfig);
+        await plugin.configure(
+          config: userPoolOnlyConfig,
+          authProviderRepo: testAuthRepo,
+        );
         expect(plugin.signOut(), completes);
       });
 
@@ -229,7 +249,10 @@ void main() {
             identityPoolKeys: identityPoolKeys,
             hostedUiKeys: hostedUiKeys,
           );
-          await plugin.configure(config: mockConfig);
+          await plugin.configure(
+            config: mockConfig,
+            authProviderRepo: testAuthRepo,
+          );
 
           final mockIdp = MockCognitoIdpClient(
             globalSignOut: () async => GlobalSignOutResponse(),
@@ -250,7 +273,10 @@ void main() {
             identityPoolKeys: identityPoolKeys,
             hostedUiKeys: hostedUiKeys,
           );
-          await plugin.configure(config: mockConfig);
+          await plugin.configure(
+            config: mockConfig,
+            authProviderRepo: testAuthRepo,
+          );
 
           final mockIdp = MockCognitoIdpClient(
             globalSignOut:
@@ -279,7 +305,10 @@ void main() {
             identityPoolKeys: identityPoolKeys,
             hostedUiKeys: hostedUiKeys,
           );
-          await plugin.configure(config: mockConfig);
+          await plugin.configure(
+            config: mockConfig,
+            authProviderRepo: testAuthRepo,
+          );
 
           final mockIdp = MockCognitoIdpClient(
             globalSignOut: () async => GlobalSignOutResponse(),
@@ -321,7 +350,10 @@ void main() {
             ),
             HostedUiPlatform.token,
           );
-          await plugin.configure(config: mockConfig);
+          await plugin.configure(
+            config: mockConfig,
+            authProviderRepo: testAuthRepo,
+          );
 
           await expectLater(plugin.getUserPoolTokens(), completes);
           await expectLater(

From 7a98c5a58a1627075b90c5c669461adad9eca543 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Thu, 21 Jul 2022 12:50:37 -0800
Subject: [PATCH 07/33] feat(core,api): IAM auth mode for HTTP requests (REST
 and GQL) (#1893)

---
 .../api/auth/api_authorization_type.dart      |   2 +-
 .../types/common/amplify_auth_provider.dart   |  14 ++
 .../amplify_authorization_rest_client.dart    |  30 ++--
 .../amplify_api/lib/src/api_plugin_impl.dart  |  64 ++++---
 .../decorators/authorize_http_request.dart    | 110 ++++++++++++
 .../app_sync_api_key_auth_provider.dart       |  38 +++++
 packages/api/amplify_api/pubspec.yaml         |   1 +
 .../test/amplify_dart_rest_methods_test.dart  |   5 +-
 .../test/authorize_http_request_test.dart     | 159 ++++++++++++++++++
 .../amplify_api/test/dart_graphql_test.dart   |   2 +-
 .../test/plugin_configuration_test.dart       | 112 ++++++++++++
 packages/api/amplify_api/test/util.dart       |  53 ++++++
 12 files changed, 538 insertions(+), 52 deletions(-)
 create mode 100644 packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
 create mode 100644 packages/api/amplify_api/lib/src/graphql/app_sync_api_key_auth_provider.dart
 create mode 100644 packages/api/amplify_api/test/authorize_http_request_test.dart
 create mode 100644 packages/api/amplify_api/test/plugin_configuration_test.dart
 create mode 100644 packages/api/amplify_api/test/util.dart

diff --git a/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart b/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
index f15da13b9f..95b73a4cac 100644
--- a/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
+++ b/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
@@ -35,7 +35,7 @@ enum APIAuthorizationType<T extends AmplifyAuthProvider> {
   /// See also:
   /// - [API Key Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#api-key-authorization)
   @JsonValue('API_KEY')
-  apiKey(AmplifyAuthProviderToken<AmplifyAuthProvider>()),
+  apiKey(AmplifyAuthProviderToken<ApiKeyAmplifyAuthProvider>()),
 
   /// Use an IAM access/secret key credential pair to authorize access to an API.
   ///
diff --git a/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart b/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
index 30c00ff053..16707d6afd 100644
--- a/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
+++ b/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
@@ -34,6 +34,12 @@ class IamAuthProviderOptions extends AuthProviderOptions {
   const IamAuthProviderOptions({required this.region, required this.service});
 }
 
+class ApiKeyAuthProviderOptions extends AuthProviderOptions {
+  final String apiKey;
+
+  const ApiKeyAuthProviderOptions(this.apiKey);
+}
+
 abstract class AmplifyAuthProvider {
   Future<AWSBaseHttpRequest> authorizeRequest(
     AWSBaseHttpRequest request, {
@@ -50,6 +56,14 @@ abstract class AWSIamAmplifyAuthProvider extends AmplifyAuthProvider
   });
 }
 
+abstract class ApiKeyAmplifyAuthProvider extends AmplifyAuthProvider {
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant ApiKeyAuthProviderOptions? options,
+  });
+}
+
 abstract class TokenAmplifyAuthProvider extends AmplifyAuthProvider {
   Future<String> getLatestAuthToken();
 
diff --git a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
index 8a2d0678b5..a0b7aece44 100644
--- a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
+++ b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
@@ -18,15 +18,19 @@ import 'package:amplify_core/amplify_core.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
 
-const _xApiKey = 'X-Api-Key';
+import 'decorators/authorize_http_request.dart';
 
 /// Implementation of http [http.Client] that authorizes HTTP requests with
 /// Amplify.
 @internal
 class AmplifyAuthorizationRestClient extends http.BaseClient
     implements Closeable {
+  /// [AmplifyAuthProviderRepository] for any auth modes this client may use.
+  final AmplifyAuthProviderRepository authProviderRepo;
+
   /// Determines how requests with this client are authorized.
   final AWSApiConfig endpointConfig;
+
   final http.Client _baseClient;
   final bool _useDefaultBaseClient;
 
@@ -34,6 +38,7 @@ class AmplifyAuthorizationRestClient extends http.BaseClient
   /// client are authorized.
   AmplifyAuthorizationRestClient({
     required this.endpointConfig,
+    required this.authProviderRepo,
     http.Client? baseClient,
   })  : _useDefaultBaseClient = baseClient == null,
         _baseClient = baseClient ?? http.Client();
@@ -42,27 +47,14 @@ class AmplifyAuthorizationRestClient extends http.BaseClient
   /// header already set.
   @override
   Future<http.StreamedResponse> send(http.BaseRequest request) async =>
-      _baseClient.send(_authorizeRequest(request));
+      _baseClient.send(await authorizeHttpRequest(
+        request,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      ));
 
   @override
   void close() {
     if (_useDefaultBaseClient) _baseClient.close();
   }
-
-  http.BaseRequest _authorizeRequest(http.BaseRequest request) {
-    if (!request.headers.containsKey(AWSHeaders.authorization) &&
-        endpointConfig.authorizationType != APIAuthorizationType.none) {
-      // TODO(ragingsquirrel3): Use auth providers from core to transform the request.
-      final apiKey = endpointConfig.apiKey;
-      if (endpointConfig.authorizationType == APIAuthorizationType.apiKey) {
-        if (apiKey == null) {
-          throw const ApiException(
-              'Auth mode is API Key, but no API Key was found in config.');
-        }
-
-        request.headers.putIfAbsent(_xApiKey, () => apiKey);
-      }
-    }
-    return request;
-  }
 }
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index a5dfd58ce6..e353c70a31 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -26,6 +26,7 @@ import 'package:meta/meta.dart';
 
 import 'amplify_api_config.dart';
 import 'amplify_authorization_rest_client.dart';
+import 'graphql/app_sync_api_key_auth_provider.dart';
 import 'graphql/send_graphql_request.dart';
 import 'util.dart';
 
@@ -35,10 +36,11 @@ import 'util.dart';
 class AmplifyAPIDart extends AmplifyAPI {
   late final AWSApiPluginConfig _apiConfig;
   final http.Client? _baseHttpClient;
+  late final AmplifyAuthProviderRepository _authProviderRepo;
 
   /// A map of the keys from the Amplify API config to HTTP clients to use for
   /// requests to that endpoint.
-  final Map<String, AmplifyAuthorizationRestClient> _clientPool = {};
+  final Map<String, http.Client> _clientPool = {};
 
   /// The registered [APIAuthProvider] instances.
   final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};
@@ -65,6 +67,21 @@ class AmplifyAPIDart extends AmplifyAPI {
               'https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/flutter/#configure-api');
     }
     _apiConfig = apiConfig;
+    _authProviderRepo = authProviderRepo;
+    _registerApiPluginAuthProviders();
+  }
+
+  /// If an endpoint has an API key, ensure valid auth provider registered.
+  void _registerApiPluginAuthProviders() {
+    _apiConfig.endpoints.forEach((key, value) {
+      // Check the presence of apiKey (not auth type) because other modes might
+      // have a key if not the primary auth mode.
+      if (value.apiKey != null) {
+        _authProviderRepo.registerAuthProvider(
+            value.authorizationType.authProviderToken,
+            AppSyncApiKeyAuthProvider());
+      }
+    });
   }
 
   @override
@@ -89,32 +106,21 @@ class AmplifyAPIDart extends AmplifyAPI {
     }
   }
 
-  /// Returns the HTTP client to be used for GraphQL operations.
+  /// Returns the HTTP client to be used for REST/GraphQL operations.
   ///
-  /// Use [apiName] if there are multiple GraphQL endpoints.
+  /// Use [apiName] if there are multiple endpoints of the same type.
   @visibleForTesting
-  http.Client getGraphQLClient({String? apiName}) {
+  http.Client getHttpClient(EndpointType type, {String? apiName}) {
     final endpoint = _apiConfig.getEndpoint(
-      type: EndpointType.graphQL,
+      type: type,
       apiName: apiName,
     );
-    return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
-        endpointConfig: endpoint.config, baseClient: _baseHttpClient);
-  }
-
-  /// Returns the HTTP client to be used for REST operations.
-  ///
-  /// Use [apiName] if there are multiple REST endpoints.
-  @visibleForTesting
-  http.Client getRestClient({String? apiName}) {
-    final endpoint = _apiConfig.getEndpoint(
-      type: EndpointType.rest,
-      apiName: apiName,
-    );
-    return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
+    return _clientPool[endpoint.name] ??= AmplifyHttpClient(
+        baseClient: AmplifyAuthorizationRestClient(
       endpointConfig: endpoint.config,
       baseClient: _baseHttpClient,
-    );
+      authProviderRepo: _authProviderRepo,
+    ));
   }
 
   Uri _getGraphQLUri(String? apiName) {
@@ -160,7 +166,8 @@ class AmplifyAPIDart extends AmplifyAPI {
   @override
   CancelableOperation<GraphQLResponse<T>> query<T>(
       {required GraphQLRequest<T> request}) {
-    final graphQLClient = getGraphQLClient(apiName: request.apiName);
+    final graphQLClient =
+        getHttpClient(EndpointType.graphQL, apiName: request.apiName);
     final uri = _getGraphQLUri(request.apiName);
 
     final responseFuture = sendGraphQLRequest<T>(
@@ -171,7 +178,8 @@ class AmplifyAPIDart extends AmplifyAPI {
   @override
   CancelableOperation<GraphQLResponse<T>> mutate<T>(
       {required GraphQLRequest<T> request}) {
-    final graphQLClient = getGraphQLClient(apiName: request.apiName);
+    final graphQLClient =
+        getHttpClient(EndpointType.graphQL, apiName: request.apiName);
     final uri = _getGraphQLUri(request.apiName);
 
     final responseFuture = sendGraphQLRequest<T>(
@@ -190,7 +198,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(AWSStreamedHttpRequest.delete(
       uri,
       body: body ?? HttpPayload.empty(),
@@ -206,7 +214,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSHttpRequest.get(
         uri,
@@ -223,7 +231,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSHttpRequest.head(
         uri,
@@ -241,7 +249,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSStreamedHttpRequest.patch(
         uri,
@@ -260,7 +268,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSStreamedHttpRequest.post(
         uri,
@@ -279,7 +287,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSStreamedHttpRequest.put(
         uri,
diff --git a/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
new file mode 100644
index 0000000000..3cab4d7443
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
@@ -0,0 +1,110 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+/// Transforms an HTTP request according to auth providers that match the endpoint
+/// configuration.
+@internal
+Future<http.BaseRequest> authorizeHttpRequest(http.BaseRequest request,
+    {required AWSApiConfig endpointConfig,
+    required AmplifyAuthProviderRepository authProviderRepo}) async {
+  if (request.headers.containsKey(AWSHeaders.authorization)) {
+    return request;
+  }
+  final authType = endpointConfig.authorizationType;
+
+  switch (authType) {
+    case APIAuthorizationType.apiKey:
+      final authProvider = _validateAuthProvider(
+          authProviderRepo
+              .getAuthProvider(APIAuthorizationType.apiKey.authProviderToken),
+          authType);
+      final apiKey = endpointConfig.apiKey;
+      if (apiKey == null) {
+        throw const ApiException(
+            'Auth mode is API Key, but no API Key was found in config.');
+      }
+
+      final authorizedRequest = await authProvider.authorizeRequest(
+          _httpToAWSRequest(request),
+          options: ApiKeyAuthProviderOptions(apiKey));
+      return authorizedRequest.httpRequest;
+    case APIAuthorizationType.iam:
+      final authProvider = _validateAuthProvider(
+          authProviderRepo
+              .getAuthProvider(APIAuthorizationType.iam.authProviderToken),
+          authType);
+      final service = endpointConfig.endpointType == EndpointType.graphQL
+          ? AWSService.appSync
+          : AWSService.apiGatewayManagementApi; // resolves to "execute-api"
+
+      final authorizedRequest = await authProvider.authorizeRequest(
+        _httpToAWSRequest(request),
+        options: IamAuthProviderOptions(
+          region: endpointConfig.region,
+          service: service,
+        ),
+      );
+      return authorizedRequest.httpRequest;
+    case APIAuthorizationType.function:
+    case APIAuthorizationType.oidc:
+    case APIAuthorizationType.userPools:
+      throw UnimplementedError('${authType.name} not implemented.');
+    case APIAuthorizationType.none:
+      return request;
+  }
+}
+
+T _validateAuthProvider<T extends AmplifyAuthProvider>(
+    T? authProvider, APIAuthorizationType authType) {
+  if (authProvider == null) {
+    throw ApiException('No auth provider found for auth mode ${authType.name}.',
+        recoverySuggestion: 'Ensure auth plugin correctly configured.');
+  }
+  return authProvider;
+}
+
+AWSBaseHttpRequest _httpToAWSRequest(http.BaseRequest request) {
+  final method = AWSHttpMethod.fromString(request.method);
+  if (request is http.Request) {
+    return AWSHttpRequest(
+      method: method,
+      uri: request.url,
+      headers: {
+        AWSHeaders.contentType: 'application/x-amz-json-1.1',
+        ...request.headers,
+      },
+      body: request.bodyBytes,
+    );
+  } else if (request is http.StreamedRequest) {
+    return AWSStreamedHttpRequest(
+      method: method,
+      uri: request.url,
+      headers: {
+        AWSHeaders.contentType: 'application/x-amz-json-1.1',
+        ...request.headers,
+      },
+      body: request.finalize(),
+    );
+  } else {
+    throw UnimplementedError(
+      'Multipart HTTP requests are not supported.',
+    );
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/graphql/app_sync_api_key_auth_provider.dart b/packages/api/amplify_api/lib/src/graphql/app_sync_api_key_auth_provider.dart
new file mode 100644
index 0000000000..bdafe6dbed
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/graphql/app_sync_api_key_auth_provider.dart
@@ -0,0 +1,38 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:meta/meta.dart';
+
+/// "X-Api-Key", key used for API key header in API key auth mode.
+const xApiKey = 'X-Api-Key';
+
+/// [AmplifyAuthProvider] implementation that puts an API key in the header.
+@internal
+class AppSyncApiKeyAuthProvider extends ApiKeyAmplifyAuthProvider {
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    ApiKeyAuthProviderOptions? options,
+  }) async {
+    if (options == null) {
+      throw const ApiException(
+          'Called API key auth provider without passing a valid API key.');
+    }
+    request.headers.putIfAbsent(xApiKey, () => options.apiKey);
+    return request;
+  }
+}
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index 00041dcf57..c7f4848edb 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -27,6 +27,7 @@ dev_dependencies:
     path: ../../amplify_lints
   amplify_test:
     path: ../../amplify_test
+  aws_signature_v4: ^0.1.0 
   build_runner: ^2.0.0
   flutter_test:
     sdk: flutter
diff --git a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
index d8c5162377..8469354830 100644
--- a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
+++ b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
@@ -11,8 +11,6 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-import 'dart:convert';
-
 import 'package:amplify_api/amplify_api.dart';
 import 'package:amplify_api/src/api_plugin_impl.dart';
 import 'package:amplify_core/amplify_core.dart';
@@ -28,7 +26,8 @@ const _pathThatShouldFail = 'notHere';
 
 class MockAmplifyAPI extends AmplifyAPIDart {
   @override
-  http.Client getRestClient({String? apiName}) => MockClient((request) async {
+  http.Client getHttpClient(EndpointType type, {String? apiName}) =>
+      MockClient((request) async {
         if (request.body.isNotEmpty) {
           expect(request.headers['Content-Type'], 'application/json');
         }
diff --git a/packages/api/amplify_api/test/authorize_http_request_test.dart b/packages/api/amplify_api/test/authorize_http_request_test.dart
new file mode 100644
index 0000000000..2179a07ad8
--- /dev/null
+++ b/packages/api/amplify_api/test/authorize_http_request_test.dart
@@ -0,0 +1,159 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+import 'package:amplify_api/src/decorators/authorize_http_request.dart';
+import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'util.dart';
+
+const _region = 'us-east-1';
+const _gqlEndpoint =
+    'https://abc123.appsync-api.$_region.amazonaws.com/graphql';
+const _restEndpoint = 'https://xyz456.execute-api.$_region.amazonaws.com/test';
+
+http.Request _generateTestRequest(String url) {
+  return http.Request('GET', Uri.parse(url));
+}
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  final authProviderRepo = AmplifyAuthProviderRepository();
+
+  setUpAll(() {
+    authProviderRepo.registerAuthProvider(
+        APIAuthorizationType.apiKey.authProviderToken,
+        AppSyncApiKeyAuthProvider());
+    authProviderRepo.registerAuthProvider(
+        APIAuthorizationType.iam.authProviderToken, TestIamAuthProvider());
+  });
+
+  group('authorizeHttpRequest', () {
+    test('no-op for auth mode NONE', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.none,
+          endpoint: _restEndpoint,
+          endpointType: EndpointType.rest,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      expect(authorizedRequest.headers.containsKey(AWSHeaders.authorization),
+          isFalse);
+      expect(authorizedRequest, inputRequest);
+    });
+
+    test('no-op for request with Authorization header already set', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.userPools,
+          endpoint: _restEndpoint,
+          endpointType: EndpointType.rest,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      const testAuthValue = 'foo';
+      inputRequest.headers
+          .putIfAbsent(AWSHeaders.authorization, () => testAuthValue);
+
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      expect(
+          authorizedRequest.headers[AWSHeaders.authorization], testAuthValue);
+      expect(authorizedRequest, inputRequest);
+    });
+
+    test('authorizes request with IAM auth provider', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.iam,
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      validateSignedRequest(authorizedRequest);
+    });
+
+    test('authorizes request with API key', () async {
+      const testApiKey = 'abc-123-fake-key';
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.apiKey,
+          apiKey: testApiKey,
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      expect(
+        authorizedRequest.headers[xApiKey],
+        testApiKey,
+      );
+    });
+
+    test('throws when API key not in config', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.apiKey,
+          // no apiKey value provided
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      expectLater(
+          authorizeHttpRequest(
+            inputRequest,
+            endpointConfig: endpointConfig,
+            authProviderRepo: authProviderRepo,
+          ),
+          throwsA(isA<ApiException>()));
+    });
+
+    test('authorizes with Cognito User Pools auth mode', () {}, skip: true);
+
+    test('authorizes with OIDC auth mode', () {}, skip: true);
+
+    test('authorizes with lambda auth mode', () {}, skip: true);
+
+    test('throws when no auth provider found', () async {
+      final emptyAuthRepo = AmplifyAuthProviderRepository();
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.apiKey,
+          apiKey: 'abc-123-fake-key',
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      expectLater(
+          authorizeHttpRequest(
+            inputRequest,
+            endpointConfig: endpointConfig,
+            authProviderRepo: emptyAuthRepo,
+          ),
+          throwsA(isA<ApiException>()));
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/dart_graphql_test.dart b/packages/api/amplify_api/test/dart_graphql_test.dart
index bedd0092f2..4d9d8ec47f 100644
--- a/packages/api/amplify_api/test/dart_graphql_test.dart
+++ b/packages/api/amplify_api/test/dart_graphql_test.dart
@@ -91,7 +91,7 @@ class MockAmplifyAPI extends AmplifyAPIDart {
   }) : super(modelProvider: modelProvider);
 
   @override
-  http.Client getGraphQLClient({String? apiName}) =>
+  http.Client getHttpClient(EndpointType type, {String? apiName}) =>
       MockClient((request) async {
         if (request.body.contains('getBlog')) {
           return http.Response(json.encode(_expectedModelQueryResult), 200);
diff --git a/packages/api/amplify_api/test/plugin_configuration_test.dart b/packages/api/amplify_api/test/plugin_configuration_test.dart
new file mode 100644
index 0000000000..fcf3692114
--- /dev/null
+++ b/packages/api/amplify_api/test/plugin_configuration_test.dart
@@ -0,0 +1,112 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import 'dart:convert';
+
+import 'package:amplify_api/src/api_plugin_impl.dart';
+import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+
+import 'test_data/fake_amplify_configuration.dart';
+import 'util.dart';
+
+const _expectedQuerySuccessResponseBody = {
+  'data': {
+    'listBlogs': {
+      'items': [
+        {
+          'id': 'TEST_ID',
+          'name': 'Test App Blog',
+          'createdAt': '2022-06-28T17:36:52.460Z'
+        }
+      ]
+    }
+  }
+};
+
+/// Asserts user agent and API key present.
+final _mockGqlClient = MockClient((request) async {
+  const userAgentHeader =
+      zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+  expect(request.headers[userAgentHeader], contains('amplify-flutter'));
+  expect(request.headers[xApiKey], isA<String>());
+  return http.Response(json.encode(_expectedQuerySuccessResponseBody), 200);
+});
+
+/// Asserts user agent and signed.
+final _mockRestClient = MockClient((request) async {
+  const userAgentHeader =
+      zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+  expect(request.headers[userAgentHeader], contains('amplify-flutter'));
+  validateSignedRequest(request);
+  return http.Response('"Hello from Lambda!"', 200);
+});
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  final authProviderRepo = AmplifyAuthProviderRepository();
+  authProviderRepo.registerAuthProvider(
+      APIAuthorizationType.iam.authProviderToken, TestIamAuthProvider());
+  final config =
+      AmplifyConfig.fromJson(jsonDecode(amplifyconfig) as Map<String, Object?>);
+
+  group('AmplifyAPI plugin configuration', () {
+    test(
+        'should register an API key auth provider when the configuration has an API key',
+        () async {
+      final plugin = AmplifyAPIDart();
+      await plugin.configure(
+          authProviderRepo: authProviderRepo, config: config);
+      final apiKeyAuthProvider = authProviderRepo
+          .getAuthProvider(APIAuthorizationType.apiKey.authProviderToken);
+      expect(apiKeyAuthProvider, isA<AppSyncApiKeyAuthProvider>());
+    });
+
+    test(
+        'should configure an HTTP client for GraphQL that authorizes with auth providers and adds user-agent',
+        () async {
+      final plugin = AmplifyAPIDart(baseHttpClient: _mockGqlClient);
+      await plugin.configure(
+          authProviderRepo: authProviderRepo, config: config);
+
+      String graphQLDocument = '''query TestQuery {
+          listBlogs {
+            items {
+              id
+              name
+              createdAt
+            }
+          }
+        }''';
+      final request =
+          GraphQLRequest<String>(document: graphQLDocument, variables: {});
+      await plugin.query(request: request).value;
+      // no assertion here because assertion implemented in mock HTTP client
+    });
+
+    test(
+        'should configure an HTTP client for REST that authorizes with auth providers and adds user-agent',
+        () async {
+      final plugin = AmplifyAPIDart(baseHttpClient: _mockRestClient);
+      await plugin.configure(
+          authProviderRepo: authProviderRepo, config: config);
+
+      await plugin.get('/items').value;
+      // no assertion here because assertion implemented in mock HTTP client
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/util.dart b/packages/api/amplify_api/test/util.dart
new file mode 100644
index 0000000000..f3c2ef551e
--- /dev/null
+++ b/packages/api/amplify_api/test/util.dart
@@ -0,0 +1,53 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:aws_signature_v4/aws_signature_v4.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+
+class TestIamAuthProvider extends AWSIamAmplifyAuthProvider {
+  @override
+  Future<AWSCredentials> retrieve() async {
+    return const AWSCredentials(
+        'fake-access-key-123', 'fake-secret-access-key-456');
+  }
+
+  @override
+  Future<AWSSignedRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    IamAuthProviderOptions? options,
+  }) async {
+    final signer = AWSSigV4Signer(
+      credentialsProvider: AWSCredentialsProvider(await retrieve()),
+    );
+    final scope = AWSCredentialScope(
+      region: options!.region,
+      service: AWSService.appSync,
+    );
+    return signer.sign(
+      request,
+      credentialScope: scope,
+    );
+  }
+}
+
+void validateSignedRequest(http.BaseRequest request) {
+  const userAgentHeader =
+      zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+  expect(
+    request.headers[userAgentHeader],
+    contains('aws-sigv4'),
+  );
+}

From 5f2783b13a5abb34f0cb8fac8b800447b0ea00a2 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Thu, 21 Jul 2022 16:32:26 -0700
Subject: [PATCH 08/33] feat(api): .subscribe() for GraphQL

---
 .../integration_test/graphql_tests.dart       | 234 +++++++++---------
 .../amplify_api/lib/src/api_plugin_impl.dart  |  27 ++
 .../authorize_websocket_message.dart          |  68 +++++
 .../src/graphql/ws/websocket_connection.dart  | 186 ++++++++++++++
 .../lib/src/graphql/ws/websocket_message.dart | 202 +++++++++++++++
 .../websocket_message_stream_transformer.dart |  54 ++++
 packages/api/amplify_api/pubspec.yaml         |   2 +
 7 files changed, 659 insertions(+), 114 deletions(-)
 create mode 100644 packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
 create mode 100644 packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
 create mode 100644 packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
 create mode 100644 packages/api/amplify_api/lib/src/graphql/ws/websocket_message_stream_transformer.dart

diff --git a/packages/api/amplify_api/example/integration_test/graphql_tests.dart b/packages/api/amplify_api/example/integration_test/graphql_tests.dart
index f1a9a42362..20404fdea3 100644
--- a/packages/api/amplify_api/example/integration_test/graphql_tests.dart
+++ b/packages/api/amplify_api/example/integration_test/graphql_tests.dart
@@ -529,136 +529,142 @@ void main() {
       });
     });
 
-    group('subscriptions', () {
-      // Some local helper methods to help with establishing subscriptions and such.
-
-      // Wait for subscription established for given request.
-      Future<StreamSubscription<GraphQLResponse<T>>>
-          _getEstablishedSubscriptionOperation<T>(
-              GraphQLRequest<T> subscriptionRequest,
-              void Function(GraphQLResponse<T>) onData) async {
-        Completer<void> establishedCompleter = Completer();
-        final stream =
-            Amplify.API.subscribe<T>(subscriptionRequest, onEstablished: () {
-          establishedCompleter.complete();
-        });
-        final subscription = stream.listen(
-          onData,
-          onError: (Object e) => fail('Error in subscription stream: $e'),
-        );
-
-        await establishedCompleter.future
-            .timeout(const Duration(seconds: _subscriptionTimeoutInterval));
-        return subscription;
-      }
+    group(
+      'subscriptions',
+      () {
+        // Some local helper methods to help with establishing subscriptions and such.
+
+        // Wait for subscription established for given request.
+        Future<StreamSubscription<GraphQLResponse<T>>>
+            _getEstablishedSubscriptionOperation<T>(
+                GraphQLRequest<T> subscriptionRequest,
+                void Function(GraphQLResponse<T>) onData) async {
+          Completer<void> establishedCompleter = Completer();
+          final stream =
+              Amplify.API.subscribe<T>(subscriptionRequest, onEstablished: () {
+            establishedCompleter.complete();
+          });
+          final subscription = stream.listen(
+            onData,
+            onError: (Object e) => fail('Error in subscription stream: $e'),
+          );
+
+          await establishedCompleter.future
+              .timeout(const Duration(seconds: _subscriptionTimeoutInterval));
+          return subscription;
+        }
 
-      // Establish subscription for request, do the mutationFunction, then wait
-      // for the stream event, cancel the operation, return response from event.
-      Future<GraphQLResponse<T?>> _establishSubscriptionAndMutate<T>(
-          GraphQLRequest<T> subscriptionRequest,
-          Future<void> Function() mutationFunction) async {
-        Completer<GraphQLResponse<T?>> dataCompleter = Completer();
-        // With stream established, exec callback with stream events.
-        final subscription = await _getEstablishedSubscriptionOperation<T>(
-            subscriptionRequest, (event) {
-          if (event.hasErrors) {
-            fail('subscription errors: ${event.errors}');
-          }
-          dataCompleter.complete(event);
-        });
-        await mutationFunction();
-        final response = await dataCompleter.future
-            .timeout((const Duration(seconds: _subscriptionTimeoutInterval)));
+        // Establish subscription for request, do the mutationFunction, then wait
+        // for the stream event, cancel the operation, return response from event.
+        Future<GraphQLResponse<T?>> _establishSubscriptionAndMutate<T>(
+            GraphQLRequest<T> subscriptionRequest,
+            Future<void> Function() mutationFunction) async {
+          Completer<GraphQLResponse<T?>> dataCompleter = Completer();
+          // With stream established, exec callback with stream events.
+          final subscription = await _getEstablishedSubscriptionOperation<T>(
+              subscriptionRequest, (event) {
+            if (event.hasErrors) {
+              fail('subscription errors: ${event.errors}');
+            }
+            dataCompleter.complete(event);
+          });
+          await mutationFunction();
+          final response = await dataCompleter.future
+              .timeout((const Duration(seconds: _subscriptionTimeoutInterval)));
+
+          await subscription.cancel();
+          return response;
+        }
 
-        await subscription.cancel();
-        return response;
-      }
+        testWidgets(
+            'should emit event when onCreate subscription made with model helper',
+            (WidgetTester tester) async {
+          String name =
+              'Integration Test Blog - subscription create ${UUID.getUUID()}';
+          final subscriptionRequest =
+              ModelSubscriptions.onCreate(Blog.classType);
 
-      testWidgets(
-          'should emit event when onCreate subscription made with model helper',
-          (WidgetTester tester) async {
-        String name =
-            'Integration Test Blog - subscription create ${UUID.getUUID()}';
-        final subscriptionRequest = ModelSubscriptions.onCreate(Blog.classType);
+          final eventResponse = await _establishSubscriptionAndMutate(
+              subscriptionRequest, () => addBlog(name));
+          Blog? blogFromEvent = eventResponse.data;
 
-        final eventResponse = await _establishSubscriptionAndMutate(
-            subscriptionRequest, () => addBlog(name));
-        Blog? blogFromEvent = eventResponse.data;
+          expect(blogFromEvent?.name, equals(name));
+        });
 
-        expect(blogFromEvent?.name, equals(name));
-      });
+        testWidgets(
+            'should emit event when onUpdate subscription made with model helper',
+            (WidgetTester tester) async {
+          const originalName = 'Integration Test Blog - subscription update';
+          final updatedName =
+              'Integration Test Blog - subscription update, name now ${UUID.getUUID()}';
+          Blog blogToUpdate = await addBlog(originalName);
+
+          final subscriptionRequest =
+              ModelSubscriptions.onUpdate(Blog.classType);
+          final eventResponse =
+              await _establishSubscriptionAndMutate(subscriptionRequest, () {
+            blogToUpdate = blogToUpdate.copyWith(name: updatedName);
+            final updateReq = ModelMutations.update(blogToUpdate);
+            return Amplify.API.mutate(request: updateReq).response;
+          });
+          Blog? blogFromEvent = eventResponse.data;
+
+          expect(blogFromEvent?.name, equals(updatedName));
+        });
 
-      testWidgets(
-          'should emit event when onUpdate subscription made with model helper',
-          (WidgetTester tester) async {
-        const originalName = 'Integration Test Blog - subscription update';
-        final updatedName =
-            'Integration Test Blog - subscription update, name now ${UUID.getUUID()}';
-        Blog blogToUpdate = await addBlog(originalName);
-
-        final subscriptionRequest = ModelSubscriptions.onUpdate(Blog.classType);
-        final eventResponse =
-            await _establishSubscriptionAndMutate(subscriptionRequest, () {
-          blogToUpdate = blogToUpdate.copyWith(name: updatedName);
-          final updateReq = ModelMutations.update(blogToUpdate);
-          return Amplify.API.mutate(request: updateReq).response;
+        testWidgets(
+            'should emit event when onDelete subscription made with model helper',
+            (WidgetTester tester) async {
+          const name = 'Integration Test Blog - subscription delete';
+          Blog blogToDelete = await addBlog(name);
+
+          final subscriptionRequest =
+              ModelSubscriptions.onDelete(Blog.classType);
+          final eventResponse =
+              await _establishSubscriptionAndMutate(subscriptionRequest, () {
+            final deleteReq =
+                ModelMutations.deleteById(Blog.classType, blogToDelete.id);
+            return Amplify.API.mutate(request: deleteReq).response;
+          });
+          Blog? blogFromEvent = eventResponse.data;
+
+          expect(blogFromEvent?.name, equals(name));
         });
-        Blog? blogFromEvent = eventResponse.data;
 
-        expect(blogFromEvent?.name, equals(updatedName));
-      });
+        testWidgets('should cancel subscription', (WidgetTester tester) async {
+          const name = 'Integration Test Blog - subscription to cancel';
+          Blog blogToDelete = await addBlog(name);
 
-      testWidgets(
-          'should emit event when onDelete subscription made with model helper',
-          (WidgetTester tester) async {
-        const name = 'Integration Test Blog - subscription delete';
-        Blog blogToDelete = await addBlog(name);
+          final subReq = ModelSubscriptions.onDelete(Blog.classType);
+          final subscription =
+              await _getEstablishedSubscriptionOperation<Blog>(subReq, (_) {
+            fail('Subscription event triggered. Should be canceled.');
+          });
+          await subscription.cancel();
 
-        final subscriptionRequest = ModelSubscriptions.onDelete(Blog.classType);
-        final eventResponse =
-            await _establishSubscriptionAndMutate(subscriptionRequest, () {
+          // delete the blog, wait for update
           final deleteReq =
               ModelMutations.deleteById(Blog.classType, blogToDelete.id);
-          return Amplify.API.mutate(request: deleteReq).response;
+          await Amplify.API.mutate(request: deleteReq).response;
+          await Future<dynamic>.delayed(const Duration(seconds: 5));
         });
-        Blog? blogFromEvent = eventResponse.data;
 
-        expect(blogFromEvent?.name, equals(name));
-      });
+        testWidgets(
+            'should emit event when onCreate subscription made with model helper for post (model with parent).',
+            (WidgetTester tester) async {
+          String title =
+              'Integration Test post - subscription create ${UUID.getUUID()}';
+          final subscriptionRequest =
+              ModelSubscriptions.onCreate(Post.classType);
 
-      testWidgets('should cancel subscription', (WidgetTester tester) async {
-        const name = 'Integration Test Blog - subscription to cancel';
-        Blog blogToDelete = await addBlog(name);
+          final eventResponse = await _establishSubscriptionAndMutate(
+              subscriptionRequest,
+              () => addPostAndBlogWithModelHelper(title, 0));
+          Post? postFromEvent = eventResponse.data;
 
-        final subReq = ModelSubscriptions.onDelete(Blog.classType);
-        final subscription =
-            await _getEstablishedSubscriptionOperation<Blog>(subReq, (_) {
-          fail('Subscription event triggered. Should be canceled.');
+          expect(postFromEvent?.title, equals(title));
         });
-        await subscription.cancel();
-
-        // delete the blog, wait for update
-        final deleteReq =
-            ModelMutations.deleteById(Blog.classType, blogToDelete.id);
-        await Amplify.API.mutate(request: deleteReq).response;
-        await Future<dynamic>.delayed(const Duration(seconds: 5));
-      });
-
-      testWidgets(
-          'should emit event when onCreate subscription made with model helper for post (model with parent).',
-          (WidgetTester tester) async {
-        String title =
-            'Integration Test post - subscription create ${UUID.getUUID()}';
-        final subscriptionRequest = ModelSubscriptions.onCreate(Post.classType);
-
-        final eventResponse = await _establishSubscriptionAndMutate(
-            subscriptionRequest, () => addPostAndBlogWithModelHelper(title, 0));
-        Post? postFromEvent = eventResponse.data;
-
-        expect(postFromEvent?.title, equals(title));
-      });
-    },
-        skip:
-            'TODO(ragingsquirrel3): re-enable tests once subscriptions are implemented.');
+      },
+    );
   });
 }
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index e353c70a31..e7847a7303 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -17,6 +17,7 @@ library amplify_api;
 import 'dart:io';
 
 import 'package:amplify_api/amplify_api.dart';
+import 'package:amplify_api/src/graphql/ws/websocket_connection.dart';
 import 'package:amplify_api/src/native_api_plugin.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:async/async.dart';
@@ -42,6 +43,10 @@ class AmplifyAPIDart extends AmplifyAPI {
   /// requests to that endpoint.
   final Map<String, http.Client> _clientPool = {};
 
+  /// A map of the keys from the Amplify API config websocket connections to use
+  /// for that endpoint.
+  final Map<String, WebSocketConnection> _webSocketConnectionPool = {};
+
   /// The registered [APIAuthProvider] instances.
   final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};
 
@@ -123,6 +128,19 @@ class AmplifyAPIDart extends AmplifyAPI {
     ));
   }
 
+  /// Returns the websocket connection to use for a given endpoint.
+  ///
+  /// Use [apiName] if there are multiple endpoints.
+  @visibleForTesting
+  WebSocketConnection getWebsocketConnection({String? apiName}) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.graphQL,
+      apiName: apiName,
+    );
+    return _webSocketConnectionPool[endpoint.name] ??=
+        WebSocketConnection(endpoint.config, _authProviderRepo);
+  }
+
   Uri _getGraphQLUri(String? apiName) {
     final endpoint = _apiConfig.getEndpoint(
       type: EndpointType.graphQL,
@@ -187,6 +205,15 @@ class AmplifyAPIDart extends AmplifyAPI {
     return _makeCancelable<GraphQLResponse<T>>(responseFuture);
   }
 
+  @override
+  Stream<GraphQLResponse<T>> subscribe<T>(
+    GraphQLRequest<T> request, {
+    void Function()? onEstablished,
+  }) {
+    return getWebsocketConnection(apiName: request.apiName)
+        .subscribe(request, onEstablished);
+  }
+
   // ====== REST =======
 
   @override
diff --git a/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart b/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
new file mode 100644
index 0000000000..e72426201f
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
@@ -0,0 +1,68 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+import '../graphql/ws/websocket_message.dart';
+import 'authorize_http_request.dart';
+
+/// Takes input websocket message (connection or subscription establisher) and
+/// adds authorization headers from auth repo.
+@internal
+Future<WebSocketMessage> authorizeWebSocketMessage(
+  WebSocketMessage inputMessage,
+  AWSApiConfig config,
+  AmplifyAuthProviderRepository authRepo,
+) async {
+  final body = inputMessage.payload?.toJson()['data'];
+  if (inputMessage is WebSocketConnectionInitMessage) {
+    inputMessage.authorizationHeaders =
+        await _generateAuthorizationHeaders(config, authRepo: authRepo);
+  } else if (body is String) {
+    inputMessage.payload?.authorizationHeaders =
+        await _generateAuthorizationHeaders(config,
+            authRepo: authRepo, body: body);
+  }
+  return Future.value(inputMessage);
+}
+
+Future<Map<String, dynamic>> _generateAuthorizationHeaders(
+  AWSApiConfig config, {
+  required AmplifyAuthProviderRepository authRepo,
+  String body = '{}',
+}) async {
+  final endpointHost = Uri.parse(config.endpoint).host;
+  // Create canonical HTTP request to authorize.
+  final maybeConnect = body != '{}' ? '' : '/connect';
+  final canonicalHttpRequest =
+      http.Request('POST', Uri.parse('${config.endpoint}$maybeConnect'));
+  canonicalHttpRequest.headers.addAll({
+    AWSHeaders.accept: 'application/json, text/javascript',
+    AWSHeaders.contentEncoding: 'amz-1.0',
+    AWSHeaders.contentType: 'application/json; charset=UTF-8',
+  });
+  canonicalHttpRequest.body = body;
+
+  final authorizedHttpRequest = await authorizeHttpRequest(
+    canonicalHttpRequest,
+    endpointConfig: config,
+    authProviderRepo: authRepo,
+  );
+  return {
+    ...authorizedHttpRequest.headers,
+    AWSHeaders.host: endpointHost,
+  };
+}
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
new file mode 100644
index 0000000000..30df9d1b9f
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
@@ -0,0 +1,186 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:amplify_api/src/decorators/authorize_websocket_message.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import 'websocket_message.dart';
+import 'websocket_message_stream_transformer.dart';
+
+/// {@template websocket_connection}
+/// Manages connection with an AppSync backend and subscription routing.
+/// {@endtemplate}
+class WebSocketConnection implements Closeable {
+  static const webSocketProtocols = ['graphql-ws'];
+
+  final AmplifyAuthProviderRepository authProviderRepo;
+
+  final AWSApiConfig _config;
+  late final WebSocketChannel _channel;
+  late final StreamSubscription<WebSocketMessage> _subscription;
+  late final RestartableTimer _timeoutTimer;
+
+  // Add connection error variable to throw in `init`.
+
+  Future<void>? _initFuture;
+  final Completer<void> _connectionReady = Completer<void>();
+
+  /// Fires when the connection is ready to be listened to, i.e.
+  /// after the first `connection_ack` message.
+  Future<void> get ready => _connectionReady.future;
+
+  /// Re-broadcasts messages for child streams.
+  final StreamController<WebSocketMessage> _rebroadcastController =
+      StreamController<WebSocketMessage>.broadcast();
+
+  /// Incoming message stream for all events.
+  Stream<WebSocketMessage> get _messageStream => _rebroadcastController.stream;
+
+  /// {@macro websocket_connection}
+  WebSocketConnection(this._config, this.authProviderRepo);
+
+  /// Connects to the real time WebSocket.
+  Future<void> _connect() async {
+    // Generate a URI for the connection and all subscriptions.
+    // See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection
+    final connectionMessage = WebSocketConnectionInitMessage(_config);
+    final authorizedConnectionMessage = await authorizeWebSocketMessage(
+            connectionMessage, _config, authProviderRepo)
+        as WebSocketConnectionInitMessage;
+    final connectionUri = authorizedConnectionMessage.getConnectionUri();
+
+    _channel = WebSocketChannel.connect(
+      connectionUri,
+      protocols: webSocketProtocols,
+    );
+    _subscription = _channel.stream
+        .transform(const WebSocketMessageStreamTransformer())
+        .listen(_onData);
+  }
+
+  /// Closes the WebSocket connection.
+  @override
+  void close() {
+    _subscription.cancel();
+    _channel.sink.close();
+  }
+
+  /// Initializes the connection.
+  Future<void> init() {
+    return _initFuture ??= _init();
+  }
+
+  Future<void> _init() async {
+    await _connect();
+    if (_connectionReady.isCompleted) return;
+    send(MessageType.connectionInit);
+    return ready;
+  }
+
+  /// Subscribes to the given GraphQL request. Returns the subscription object,
+  /// or throws an [Exception] if there's an error.
+  Stream<GraphQLResponse<T>> subscribe<T>(
+    GraphQLRequest<T> request,
+    void Function()? onEstablished,
+  ) {
+    if (!_connectionReady.isCompleted) {
+      init();
+    }
+    final subRegistration = WebSocketMessage(
+      messageType: MessageType.start,
+      payload:
+          SubscriptionRegistrationPayload(request: request, config: _config),
+    );
+    final subscriptionId = subRegistration.id!;
+    return _messageStream
+        .where((msg) => msg.id == subscriptionId)
+        .transform(
+            WebSocketSubscriptionStreamTransformer(request, onEstablished))
+        .asBroadcastStream(
+          onListen: (_) => _send(subRegistration),
+          onCancel: (_) => _cancel(subscriptionId),
+        );
+  }
+
+  /// Cancels a subscription.
+  void _cancel(String subscriptionId) {
+    _send(WebSocketMessage(
+      id: subscriptionId,
+      messageType: MessageType.stop,
+    ));
+    // TODO(equartey): if this is the only susbscription, close the connection.
+  }
+
+  /// Sends a structured message over the WebSocket.
+  void send(MessageType type, {WebSocketMessagePayload? payload}) {
+    final message = WebSocketMessage(messageType: type, payload: payload);
+    _send(message);
+  }
+
+  /// Sends a structured message over the WebSocket.
+  Future<void> _send(WebSocketMessage message) async {
+    final authorizedMessage =
+        await authorizeWebSocketMessage(message, _config, authProviderRepo);
+    final authorizedJson = authorizedMessage.toJson();
+    final msgJson = json.encode(authorizedJson);
+    // print('Sent: $msgJson');
+    _channel.sink.add(msgJson);
+  }
+
+  /// Times out the connection (usually if a keep alive has not been received in time).
+  void _timeout(Duration timeoutDuration) {
+    _rebroadcastController.addError(
+      TimeoutException(
+        'Connection timeout',
+        timeoutDuration,
+      ),
+    );
+  }
+
+  /// Handles incoming data on the WebSocket.
+  void _onData(WebSocketMessage message) {
+    // print('Received: message $message');
+    switch (message.messageType) {
+      case MessageType.connectionAck:
+        final messageAck = message.payload as ConnectionAckMessagePayload;
+        final timeoutDuration = Duration(
+          milliseconds: messageAck.connectionTimeoutMs,
+        );
+        _timeoutTimer = RestartableTimer(
+          timeoutDuration,
+          () => _timeout(timeoutDuration),
+        );
+        _connectionReady.complete();
+        // print('Registered timer');
+        return;
+      case MessageType.connectionError:
+        final wsError = message.payload as WebSocketError?;
+        _connectionReady.completeError(
+          wsError ??
+              Exception(
+                'An unknown error occurred while connecting to the WebSocket',
+              ),
+        );
+        return;
+      case MessageType.keepAlive:
+        _timeoutTimer.reset();
+        // print('Reset timer');
+        return;
+      case MessageType.error:
+        // Only handle general messages, not subscription-specific ones
+        if (message.id != null) {
+          break;
+        }
+        final wsError = message.payload as WebSocketError;
+        _rebroadcastController.addError(wsError);
+        return;
+      default:
+        break;
+    }
+
+    // Re-broadcast unhandled messages
+    _rebroadcastController.add(message);
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart b/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
new file mode 100644
index 0000000000..97609e8de3
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
@@ -0,0 +1,202 @@
+import 'dart:convert';
+
+import 'package:amplify_core/amplify_core.dart';
+
+class MessageType {
+  final String type;
+
+  const MessageType._(this.type);
+
+  factory MessageType.fromJson(dynamic json) =>
+      values.firstWhere((el) => json == el.type);
+
+  static const List<MessageType> values = [
+    connectionInit,
+    connectionAck,
+    connectionError,
+    start,
+    startAck,
+    error,
+    data,
+    stop,
+    keepAlive,
+    complete,
+  ];
+
+  static const connectionInit = MessageType._('connection_init');
+  static const connectionAck = MessageType._('connection_ack');
+  static const connectionError = MessageType._('connection_error');
+  static const error = MessageType._('error');
+  static const start = MessageType._('start');
+  static const startAck = MessageType._('start_ack');
+  static const data = MessageType._('data');
+  static const stop = MessageType._('stop');
+  static const keepAlive = MessageType._('ka');
+  static const complete = MessageType._('complete');
+
+  @override
+  String toString() => type;
+}
+
+abstract class WebSocketMessagePayload {
+  Map<String, dynamic> authorizationHeaders = {};
+
+  WebSocketMessagePayload();
+
+  static const Map<MessageType, WebSocketMessagePayload Function(Map)>
+      _factories = {
+    MessageType.connectionAck: ConnectionAckMessagePayload.fromJson,
+    MessageType.data: SubscriptionDataPayload.fromJson,
+    MessageType.error: WebSocketError.fromJson,
+  };
+
+  static WebSocketMessagePayload? fromJson(Map json, MessageType type) {
+    return _factories[type]?.call(json);
+  }
+
+  Map<String, dynamic> toJson();
+
+  @override
+  String toString() => prettyPrintJson(toJson());
+}
+
+class ConnectionAckMessagePayload extends WebSocketMessagePayload {
+  final int connectionTimeoutMs;
+
+  ConnectionAckMessagePayload(this.connectionTimeoutMs);
+
+  static ConnectionAckMessagePayload fromJson(Map json) {
+    final connectionTimeoutMs = json['connectionTimeoutMs'] as int;
+    return ConnectionAckMessagePayload(connectionTimeoutMs);
+  }
+
+  @override
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'connectionTimeoutMs': connectionTimeoutMs,
+      };
+}
+
+class SubscriptionRegistrationPayload extends WebSocketMessagePayload {
+  final GraphQLRequest request;
+  final AWSApiConfig config;
+
+  SubscriptionRegistrationPayload({
+    required this.request,
+    required this.config,
+  });
+
+  @override
+  Map<String, dynamic> toJson() {
+    return <String, dynamic>{
+      'data': jsonEncode(
+          {'variables': request.variables, 'query': request.document}),
+      'extensions': <String, dynamic>{'authorization': authorizationHeaders}
+    };
+  }
+}
+
+class SubscriptionDataPayload extends WebSocketMessagePayload {
+  final Map<String, dynamic>? data;
+  final Map<String, dynamic>? errors;
+
+  SubscriptionDataPayload(this.data, this.errors);
+
+  static SubscriptionDataPayload fromJson(Map json) {
+    final data = json['data'] as Map?;
+    final errors = json['errors'] as Map?;
+    return SubscriptionDataPayload(
+      data?.cast<String, dynamic>(),
+      errors?.cast<String, dynamic>(),
+    );
+  }
+
+  @override
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'data': data,
+        'errors': errors,
+      };
+}
+
+class WebSocketError extends WebSocketMessagePayload implements Exception {
+  final List<Map> errors;
+
+  WebSocketError(this.errors);
+
+  static WebSocketError fromJson(Map json) {
+    final errors = json['errors'] as List?;
+    return WebSocketError(errors?.cast() ?? []);
+  }
+
+  @override
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        'errors': errors,
+      };
+}
+
+class WebSocketMessage {
+  final String? id;
+  final MessageType messageType;
+  final WebSocketMessagePayload? payload;
+
+  WebSocketMessage({
+    String? id,
+    required this.messageType,
+    this.payload,
+  }) : id = id ?? UUID.getUUID();
+
+  WebSocketMessage._({
+    this.id,
+    required this.messageType,
+    this.payload,
+  });
+
+  static WebSocketMessage fromJson(Map json) {
+    final id = json['id'] as String?;
+    final type = json['type'] as String;
+    final messageType = MessageType.fromJson(type);
+    final payloadMap = json['payload'] as Map?;
+    final payload = payloadMap == null
+        ? null
+        : WebSocketMessagePayload.fromJson(
+            payloadMap,
+            messageType,
+          );
+    return WebSocketMessage._(
+      id: id,
+      messageType: messageType,
+      payload: payload,
+    );
+  }
+
+  Map<String, dynamic> toJson() => <String, dynamic>{
+        if (id != null) 'id': id,
+        'type': messageType.type,
+        if (payload != null) 'payload': payload?.toJson(),
+      };
+
+  @override
+  String toString() {
+    return prettyPrintJson(this);
+  }
+}
+
+class WebSocketConnectionInitMessage extends WebSocketMessage {
+  final AWSApiConfig config;
+  Map<String, dynamic> authorizationHeaders = {};
+
+  WebSocketConnectionInitMessage(this.config)
+      : super(messageType: MessageType.connectionInit);
+
+  Uri getConnectionUri() {
+    final encodedAuthHeaders =
+        base64.encode(json.encode(authorizationHeaders).codeUnits);
+    final endpointUri = Uri.parse(
+        config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'));
+    return Uri(scheme: 'wss', host: endpointUri.host, path: 'graphql')
+        .replace(queryParameters: <String, String>{
+      'header': encodedAuthHeaders,
+      'payload':
+          base64.encode(utf8.encode(json.encode({}))) // always payload of '{}'
+    });
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message_stream_transformer.dart b/packages/api/amplify_api/lib/src/graphql/ws/websocket_message_stream_transformer.dart
new file mode 100644
index 0000000000..90afd0d429
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/graphql/ws/websocket_message_stream_transformer.dart
@@ -0,0 +1,54 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:amplify_api/src/util.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'websocket_message.dart';
+
+import '../graphql_response_decoder.dart';
+
+class WebSocketMessageStreamTransformer
+    extends StreamTransformerBase<dynamic, WebSocketMessage> {
+  const WebSocketMessageStreamTransformer();
+
+  @override
+  Stream<WebSocketMessage> bind(Stream<dynamic> stream) {
+    return stream.cast<String>().map<Map>((str) {
+      return json.decode(str) as Map;
+    }).map(WebSocketMessage.fromJson);
+  }
+}
+
+class WebSocketSubscriptionStreamTransformer<T>
+    extends StreamTransformerBase<WebSocketMessage, GraphQLResponse<T>> {
+  final GraphQLRequest<T> request;
+  final void Function()? onEstablished;
+
+  const WebSocketSubscriptionStreamTransformer(
+      this.request, this.onEstablished);
+
+  @override
+  Stream<GraphQLResponse<T>> bind(Stream<WebSocketMessage> stream) async* {
+    await for (var event in stream) {
+      switch (event.messageType) {
+        case MessageType.startAck:
+          onEstablished?.call();
+          break;
+        case MessageType.data:
+          final payload = event.payload as SubscriptionDataPayload;
+          final errors = deserializeGraphQLResponseErrors(payload.toJson());
+          yield GraphQLResponseDecoder.instance.decode<T>(
+              request: request,
+              data: json.encode(payload.data),
+              errors: errors);
+
+          break;
+        case MessageType.error:
+          final error = event.payload as WebSocketError;
+          throw error;
+        case MessageType.complete:
+          return;
+      }
+    }
+  }
+}
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index c7f4848edb..0c05cfb643 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -21,6 +21,8 @@ dependencies:
   http: ^0.13.4
   meta: ^1.7.0
   plugin_platform_interface: ^2.0.0
+  web_socket_channel: ^2.2.0
+
 
 dev_dependencies:
   amplify_lints: 

From 55931aa1a3ec603598d94bc36f80c9ff40baba8d Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Thu, 21 Jul 2022 16:42:22 -0700
Subject: [PATCH 09/33] change name

---
 packages/api/amplify_api/lib/src/api_plugin_impl.dart | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index e7847a7303..7a60cdda0e 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -132,7 +132,7 @@ class AmplifyAPIDart extends AmplifyAPI {
   ///
   /// Use [apiName] if there are multiple endpoints.
   @visibleForTesting
-  WebSocketConnection getWebsocketConnection({String? apiName}) {
+  WebSocketConnection getWebSocketConnection({String? apiName}) {
     final endpoint = _apiConfig.getEndpoint(
       type: EndpointType.graphQL,
       apiName: apiName,
@@ -210,7 +210,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     GraphQLRequest<T> request, {
     void Function()? onEstablished,
   }) {
-    return getWebsocketConnection(apiName: request.apiName)
+    return getWebSocketConnection(apiName: request.apiName)
         .subscribe(request, onEstablished);
   }
 

From b6ebdabe8884d5eebc2cd85dbd46f82ccef95e02 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Tue, 26 Jul 2022 15:34:40 -0700
Subject: [PATCH 10/33] add some unit tests

---
 .../authorize_websocket_message.dart          |  21 +--
 .../src/graphql/ws/websocket_connection.dart  |  76 ++++----
 .../lib/src/graphql/ws/websocket_message.dart |  27 +--
 .../test/ws/web_socket_connection_test.dart   | 171 ++++++++++++++++++
 4 files changed, 233 insertions(+), 62 deletions(-)
 create mode 100644 packages/api/amplify_api/test/ws/web_socket_connection_test.dart

diff --git a/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart b/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
index e72426201f..f4b1d699d1 100644
--- a/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
+++ b/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
@@ -22,24 +22,21 @@ import 'authorize_http_request.dart';
 /// Takes input websocket message (connection or subscription establisher) and
 /// adds authorization headers from auth repo.
 @internal
-Future<WebSocketMessage> authorizeWebSocketMessage(
-  WebSocketMessage inputMessage,
+Future<WebSocketSubscriptionRegistrationMessage> authorizeWebSocketMessage(
+  WebSocketSubscriptionRegistrationMessage inputMessage,
   AWSApiConfig config,
   AmplifyAuthProviderRepository authRepo,
 ) async {
   final body = inputMessage.payload?.toJson()['data'];
-  if (inputMessage is WebSocketConnectionInitMessage) {
-    inputMessage.authorizationHeaders =
-        await _generateAuthorizationHeaders(config, authRepo: authRepo);
-  } else if (body is String) {
-    inputMessage.payload?.authorizationHeaders =
-        await _generateAuthorizationHeaders(config,
-            authRepo: authRepo, body: body);
-  }
-  return Future.value(inputMessage);
+
+  final payload = inputMessage.payload as SubscriptionRegistrationPayload?;
+  payload?.authorizationHeaders = await generateAuthorizationHeaders(config,
+      authRepo: authRepo, body: body as String);
+  return inputMessage;
 }
 
-Future<Map<String, dynamic>> _generateAuthorizationHeaders(
+@internal
+Future<Map<String, String>> generateAuthorizationHeaders(
   AWSApiConfig config, {
   required AmplifyAuthProviderRepository authRepo,
   String body = '{}',
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
index 30df9d1b9f..9eeebe95ee 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
@@ -4,6 +4,7 @@ import 'dart:convert';
 import 'package:amplify_api/src/decorators/authorize_websocket_message.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:async/async.dart';
+import 'package:flutter/material.dart';
 import 'package:web_socket_channel/web_socket_channel.dart';
 
 import 'websocket_message.dart';
@@ -41,23 +42,22 @@ class WebSocketConnection implements Closeable {
   /// {@macro websocket_connection}
   WebSocketConnection(this._config, this.authProviderRepo);
 
-  /// Connects to the real time WebSocket.
-  Future<void> _connect() async {
-    // Generate a URI for the connection and all subscriptions.
-    // See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection
-    final connectionMessage = WebSocketConnectionInitMessage(_config);
-    final authorizedConnectionMessage = await authorizeWebSocketMessage(
-            connectionMessage, _config, authProviderRepo)
-        as WebSocketConnectionInitMessage;
-    final connectionUri = authorizedConnectionMessage.getConnectionUri();
+  @visibleForTesting
+  StreamSubscription<WebSocketMessage> getStreamSubscription(
+      Stream<dynamic> stream) {
+    return stream
+        .transform(const WebSocketMessageStreamTransformer())
+        .listen(_onData);
+  }
 
+  /// Connects to the real time WebSocket.
+  @visibleForTesting
+  Future<void> connect(Uri connectionUri) async {
     _channel = WebSocketChannel.connect(
       connectionUri,
       protocols: webSocketProtocols,
     );
-    _subscription = _channel.stream
-        .transform(const WebSocketMessageStreamTransformer())
-        .listen(_onData);
+    _subscription = getStreamSubscription(_channel.stream);
   }
 
   /// Closes the WebSocket connection.
@@ -73,9 +73,13 @@ class WebSocketConnection implements Closeable {
   }
 
   Future<void> _init() async {
-    await _connect();
+    // Generate a URI for the connection and all subscriptions.
+    // See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection
+    final connectionUri = await _getConnectionUri(_config, authProviderRepo);
+    await connect(connectionUri);
+
     if (_connectionReady.isCompleted) return;
-    send(MessageType.connectionInit);
+    send(WebSocketConnectionInitMessage());
     return ready;
   }
 
@@ -88,8 +92,7 @@ class WebSocketConnection implements Closeable {
     if (!_connectionReady.isCompleted) {
       init();
     }
-    final subRegistration = WebSocketMessage(
-      messageType: MessageType.start,
+    final subRegistration = WebSocketSubscriptionRegistrationMessage(
       payload:
           SubscriptionRegistrationPayload(request: request, config: _config),
     );
@@ -99,14 +102,14 @@ class WebSocketConnection implements Closeable {
         .transform(
             WebSocketSubscriptionStreamTransformer(request, onEstablished))
         .asBroadcastStream(
-          onListen: (_) => _send(subRegistration),
+          onListen: (_) => send(subRegistration),
           onCancel: (_) => _cancel(subscriptionId),
         );
   }
 
   /// Cancels a subscription.
   void _cancel(String subscriptionId) {
-    _send(WebSocketMessage(
+    send(WebSocketMessage(
       id: subscriptionId,
       messageType: MessageType.stop,
     ));
@@ -114,18 +117,14 @@ class WebSocketConnection implements Closeable {
   }
 
   /// Sends a structured message over the WebSocket.
-  void send(MessageType type, {WebSocketMessagePayload? payload}) {
-    final message = WebSocketMessage(messageType: type, payload: payload);
-    _send(message);
-  }
-
-  /// Sends a structured message over the WebSocket.
-  Future<void> _send(WebSocketMessage message) async {
-    final authorizedMessage =
-        await authorizeWebSocketMessage(message, _config, authProviderRepo);
-    final authorizedJson = authorizedMessage.toJson();
-    final msgJson = json.encode(authorizedJson);
-    // print('Sent: $msgJson');
+  @visibleForTesting
+  Future<void> send(WebSocketMessage message) async {
+    var authorizedMessage = message;
+    if (message is WebSocketSubscriptionRegistrationMessage) {
+      authorizedMessage =
+          await authorizeWebSocketMessage(message, _config, authProviderRepo);
+    }
+    final msgJson = json.encode(authorizedMessage.toJson());
     _channel.sink.add(msgJson);
   }
 
@@ -141,7 +140,6 @@ class WebSocketConnection implements Closeable {
 
   /// Handles incoming data on the WebSocket.
   void _onData(WebSocketMessage message) {
-    // print('Received: message $message');
     switch (message.messageType) {
       case MessageType.connectionAck:
         final messageAck = message.payload as ConnectionAckMessagePayload;
@@ -184,3 +182,19 @@ class WebSocketConnection implements Closeable {
     _rebroadcastController.add(message);
   }
 }
+
+Future<Uri> _getConnectionUri(
+    AWSApiConfig config, AmplifyAuthProviderRepository authRepo) async {
+  const body = '{}';
+  final authorizationHeaders = await generateAuthorizationHeaders(config,
+      authRepo: authRepo, body: body);
+  final encodedAuthHeaders =
+      base64.encode(json.encode(authorizationHeaders).codeUnits);
+  final endpointUri = Uri.parse(
+      config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'));
+  return Uri(scheme: 'wss', host: endpointUri.host, path: 'graphql')
+      .replace(queryParameters: <String, String>{
+    'header': encodedAuthHeaders,
+    'payload': base64.encode(utf8.encode(body)) // always payload of '{}'
+  });
+}
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart b/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
index 97609e8de3..3729f7cb6e 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
@@ -39,8 +39,6 @@ class MessageType {
 }
 
 abstract class WebSocketMessagePayload {
-  Map<String, dynamic> authorizationHeaders = {};
-
   WebSocketMessagePayload();
 
   static const Map<MessageType, WebSocketMessagePayload Function(Map)>
@@ -79,6 +77,7 @@ class ConnectionAckMessagePayload extends WebSocketMessagePayload {
 class SubscriptionRegistrationPayload extends WebSocketMessagePayload {
   final GraphQLRequest request;
   final AWSApiConfig config;
+  Map<String, dynamic> authorizationHeaders = {};
 
   SubscriptionRegistrationPayload({
     required this.request,
@@ -142,7 +141,7 @@ class WebSocketMessage {
     String? id,
     required this.messageType,
     this.payload,
-  }) : id = id ?? UUID.getUUID();
+  }) : id = id ?? uuid();
 
   WebSocketMessage._({
     this.id,
@@ -181,22 +180,12 @@ class WebSocketMessage {
 }
 
 class WebSocketConnectionInitMessage extends WebSocketMessage {
-  final AWSApiConfig config;
-  Map<String, dynamic> authorizationHeaders = {};
-
-  WebSocketConnectionInitMessage(this.config)
+  WebSocketConnectionInitMessage()
       : super(messageType: MessageType.connectionInit);
+}
 
-  Uri getConnectionUri() {
-    final encodedAuthHeaders =
-        base64.encode(json.encode(authorizationHeaders).codeUnits);
-    final endpointUri = Uri.parse(
-        config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'));
-    return Uri(scheme: 'wss', host: endpointUri.host, path: 'graphql')
-        .replace(queryParameters: <String, String>{
-      'header': encodedAuthHeaders,
-      'payload':
-          base64.encode(utf8.encode(json.encode({}))) // always payload of '{}'
-    });
-  }
+class WebSocketSubscriptionRegistrationMessage extends WebSocketMessage {
+  WebSocketSubscriptionRegistrationMessage(
+      {required SubscriptionRegistrationPayload payload})
+      : super(messageType: MessageType.start, payload: payload);
 }
diff --git a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
new file mode 100644
index 0000000000..61524efe39
--- /dev/null
+++ b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
@@ -0,0 +1,171 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:amplify_api/src/api_plugin_impl.dart';
+import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
+import 'package:amplify_api/src/graphql/ws/websocket_message.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:web_socket_channel/web_socket_channel.dart';
+
+import '../util.dart';
+
+import 'package:amplify_api/src/graphql/ws/websocket_connection.dart';
+
+/// Extension of [WebSocketConnection] that stores messages internally instead
+/// of sending them.
+class MockWebSocketConnection extends WebSocketConnection {
+  /// Instead of actually connecting, just set the URI here so it can be inspected
+  /// for testing.
+  Uri? connectedUri;
+
+  /// Instead of sending messages, they are pushed to end of list so they can be
+  /// inspected for testing.
+  final List<WebSocketMessage> sentMessages = [];
+
+  MockWebSocketConnection(super.config, super.authProviderRepo);
+
+  WebSocketMessage? get lastSentMessage => sentMessages.lastOrNull;
+
+  final messageStream = StreamController<dynamic>();
+
+  @override
+  Future<void> connect(Uri connectionUri) async {
+    connectedUri = connectionUri;
+
+    // mock some message responses (acks) from server
+    final broadcast = messageStream.stream.asBroadcastStream();
+    broadcast.listen((event) {
+      final eventJson = json.decode(event as String);
+      final messageFromEvent = WebSocketMessage.fromJson(eventJson as Map);
+
+      // connection_init, respond with connection_ack
+      WebSocketMessage? mockAckMsg;
+      if (messageFromEvent.messageType == MessageType.connectionInit) {
+        mockAckMsg = WebSocketMessage(
+          messageType: MessageType.connectionAck,
+          payload: ConnectionAckMessagePayload(10000),
+        );
+        // start, respond with start_ack
+      } else if (messageFromEvent.messageType == MessageType.start) {
+        mockAckMsg = WebSocketMessage(
+          messageType: MessageType.startAck,
+          id: messageFromEvent.id,
+        );
+      }
+      if (mockAckMsg != null) {
+        final messageStr = json.encode(mockAckMsg);
+        messageStream.add(messageStr);
+      }
+    });
+
+    // ensures connected to _onDone events in parent class
+    getStreamSubscription(broadcast);
+  }
+
+  @override
+  Future<void> send(WebSocketMessage message) async {
+    sentMessages.add(message);
+
+    final messageStr = json.encode(message.toJson());
+    messageStream.add(messageStr);
+  }
+}
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  final authProviderRepo = AmplifyAuthProviderRepository();
+  authProviderRepo.registerAuthProvider(
+      APIAuthorizationType.apiKey.authProviderToken,
+      AppSyncApiKeyAuthProvider());
+
+  const endpointType = EndpointType.graphQL;
+  const endpoint = 'https://abc123.appsync-api.us-east-1.amazonaws.com/graphql';
+  const region = 'us-east-1';
+  const authorizationType = APIAuthorizationType.apiKey;
+  const apiKey = 'abc-123';
+
+  const config = AWSApiConfig(
+      endpointType: endpointType,
+      endpoint: endpoint,
+      region: region,
+      authorizationType: authorizationType,
+      apiKey: apiKey);
+
+  late MockWebSocketConnection connection;
+
+  const graphQLDocument = '''subscription MySubscription {
+    onCreateBlog {
+      id
+      name
+      createdAt
+    }
+  }''';
+  final subscriptionRequest = GraphQLRequest<String>(document: graphQLDocument);
+
+  setUp(() {
+    connection = MockWebSocketConnection(config, authProviderRepo);
+  });
+
+  group('WebSocketConnection', () {
+    test(
+        'init() should connect with authorized query params in URI and send connection init message',
+        () async {
+      await connection.init();
+      expectLater(connection.ready, completes);
+      const expectedConnectionUri =
+          'wss://abc123.appsync-realtime-api.us-east-1.amazonaws.com/graphql?header=eyJDb250ZW50LVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PVVURi04IiwiWC1BcGktS2V5IjoiYWJjLTEyMyIsIkFjY2VwdCI6ImFwcGxpY2F0aW9uL2pzb24sIHRleHQvamF2YXNjcmlwdCIsIkNvbnRlbnQtRW5jb2RpbmciOiJhbXotMS4wIiwiSG9zdCI6ImFiYzEyMy5hcHBzeW5jLWFwaS51cy1lYXN0LTEuYW1hem9uYXdzLmNvbSJ9&payload=e30%3D';
+      expect(connection.connectedUri.toString(), expectedConnectionUri);
+      expect(
+          connection.lastSentMessage?.messageType, MessageType.connectionInit);
+    });
+
+    test('subscribe() should initialize the connection and call onEstablished',
+        () async {
+      Completer<void> establishedCompleter = Completer();
+      connection.subscribe(subscriptionRequest, () {
+        establishedCompleter.complete();
+      }).listen((event) {});
+
+      expectLater(connection.ready, completes);
+      expectLater(establishedCompleter.future, completes);
+    });
+
+    test(
+        'subscribe() should send SubscriptionRegistrationMessage with authorized payload',
+        () async {
+      connection.init();
+      await connection.ready;
+      Completer<void> establishedCompleter = Completer();
+      connection.subscribe(subscriptionRequest, () {
+        establishedCompleter.complete();
+      }).listen((event) {});
+      await establishedCompleter.future;
+
+      final lastMessage = connection.lastSentMessage;
+      expect(lastMessage?.messageType, MessageType.start);
+      final payloadJson = lastMessage?.payload?.toJson();
+      print(payloadJson);
+
+      // TODO assert payload authorized
+    });
+
+    test('subscribe() should return a subscription stream', () {});
+  });
+}

From dcaeba1a317c6cfa2a5fd29e604ec83ca96a0284 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Tue, 26 Jul 2022 16:35:18 -0700
Subject: [PATCH 11/33] refactor a little

---
 .../authorize_websocket_message.dart          |  65 -----------
 .../src/decorators/websocket_auth_utils.dart  | 109 ++++++++++++++++++
 .../src/graphql/ws/websocket_connection.dart  |  54 +++------
 .../lib/src/graphql/ws/websocket_message.dart |   7 +-
 .../test/ws/web_socket_connection_test.dart   |  22 ++--
 5 files changed, 147 insertions(+), 110 deletions(-)
 delete mode 100644 packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
 create mode 100644 packages/api/amplify_api/lib/src/decorators/websocket_auth_utils.dart

diff --git a/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart b/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
deleted file mode 100644
index f4b1d699d1..0000000000
--- a/packages/api/amplify_api/lib/src/decorators/authorize_websocket_message.dart
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import 'package:amplify_core/amplify_core.dart';
-import 'package:http/http.dart' as http;
-import 'package:meta/meta.dart';
-
-import '../graphql/ws/websocket_message.dart';
-import 'authorize_http_request.dart';
-
-/// Takes input websocket message (connection or subscription establisher) and
-/// adds authorization headers from auth repo.
-@internal
-Future<WebSocketSubscriptionRegistrationMessage> authorizeWebSocketMessage(
-  WebSocketSubscriptionRegistrationMessage inputMessage,
-  AWSApiConfig config,
-  AmplifyAuthProviderRepository authRepo,
-) async {
-  final body = inputMessage.payload?.toJson()['data'];
-
-  final payload = inputMessage.payload as SubscriptionRegistrationPayload?;
-  payload?.authorizationHeaders = await generateAuthorizationHeaders(config,
-      authRepo: authRepo, body: body as String);
-  return inputMessage;
-}
-
-@internal
-Future<Map<String, String>> generateAuthorizationHeaders(
-  AWSApiConfig config, {
-  required AmplifyAuthProviderRepository authRepo,
-  String body = '{}',
-}) async {
-  final endpointHost = Uri.parse(config.endpoint).host;
-  // Create canonical HTTP request to authorize.
-  final maybeConnect = body != '{}' ? '' : '/connect';
-  final canonicalHttpRequest =
-      http.Request('POST', Uri.parse('${config.endpoint}$maybeConnect'));
-  canonicalHttpRequest.headers.addAll({
-    AWSHeaders.accept: 'application/json, text/javascript',
-    AWSHeaders.contentEncoding: 'amz-1.0',
-    AWSHeaders.contentType: 'application/json; charset=UTF-8',
-  });
-  canonicalHttpRequest.body = body;
-
-  final authorizedHttpRequest = await authorizeHttpRequest(
-    canonicalHttpRequest,
-    endpointConfig: config,
-    authProviderRepo: authRepo,
-  );
-  return {
-    ...authorizedHttpRequest.headers,
-    AWSHeaders.host: endpointHost,
-  };
-}
diff --git a/packages/api/amplify_api/lib/src/decorators/websocket_auth_utils.dart b/packages/api/amplify_api/lib/src/decorators/websocket_auth_utils.dart
new file mode 100644
index 0000000000..58092aa224
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/decorators/websocket_auth_utils.dart
@@ -0,0 +1,109 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:convert';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+import '../graphql/ws/websocket_message.dart';
+import 'authorize_http_request.dart';
+
+/// Generate a URI for the connection and all subscriptions.
+///
+/// See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection=
+@internal
+Future<Uri> generateConnectionUri(
+    AWSApiConfig config, AmplifyAuthProviderRepository authRepo) async {
+  const body = '{}';
+  final authorizationHeaders = await _generateAuthorizationHeaders(config,
+      authRepo: authRepo, body: body);
+  final encodedAuthHeaders =
+      base64.encode(json.encode(authorizationHeaders).codeUnits);
+  final endpointUri = Uri.parse(
+      config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'));
+  return Uri(scheme: 'wss', host: endpointUri.host, path: 'graphql')
+      .replace(queryParameters: <String, String>{
+    'header': encodedAuthHeaders,
+    'payload': base64.encode(utf8.encode(body)) // always payload of '{}'
+  });
+}
+
+/// Generate websocket message with authorized payload to register subscription.
+///
+/// See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#subscription-registration-message
+@internal
+Future<WebSocketSubscriptionRegistrationMessage>
+    generateSubscriptionRegistrationMessage(
+  AWSApiConfig config, {
+  required String id,
+  required AmplifyAuthProviderRepository authRepo,
+  required GraphQLRequest request,
+}) async {
+  final body =
+      jsonEncode({'variables': request.variables, 'query': request.document});
+  final authorizationHeaders = await _generateAuthorizationHeaders(config,
+      authRepo: authRepo, body: body);
+
+  return WebSocketSubscriptionRegistrationMessage(
+    id: id,
+    payload: SubscriptionRegistrationPayload(
+      request: request,
+      config: config,
+      authorizationHeaders: authorizationHeaders,
+    ),
+  );
+}
+
+/// For either connection URI or subscription registration, authorization headers
+/// are formatted correctly to be either encoded into URI query params or subscription
+/// registration payload headers.
+///
+/// If body is "{}" then headers are formatted like connection URI. Any other string
+/// for body will be formatted as subscription registration. This is done by creating
+/// a canonical HTTP request that is authorized but never sent. The headers from
+/// the HTTP request are reformatted and returned. This logic applies for all auth
+/// modes as determined by [authRepo] parameter.
+Future<Map<String, String>> _generateAuthorizationHeaders(
+  AWSApiConfig config, {
+  required AmplifyAuthProviderRepository authRepo,
+  required String body,
+}) async {
+  final endpointHost = Uri.parse(config.endpoint).host;
+  // Create canonical HTTP request to authorize.
+  //
+  // The canonical request URL is a little different depending on if connection_init
+  // or start (subscription registration).
+  final maybeConnect = body != '{}' ? '' : '/connect';
+  final canonicalHttpRequest =
+      http.Request('POST', Uri.parse('${config.endpoint}$maybeConnect'));
+  canonicalHttpRequest.headers.addAll({
+    AWSHeaders.accept: 'application/json, text/javascript',
+    AWSHeaders.contentEncoding: 'amz-1.0',
+    AWSHeaders.contentType: 'application/json; charset=UTF-8',
+  });
+  canonicalHttpRequest.body = body;
+  final authorizedHttpRequest = await authorizeHttpRequest(
+    canonicalHttpRequest,
+    endpointConfig: config,
+    authProviderRepo: authRepo,
+  );
+
+  // Take authorized HTTP headers as map with "host" value added.
+  return {
+    ...authorizedHttpRequest.headers,
+    AWSHeaders.host: endpointHost,
+  };
+}
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
index 9eeebe95ee..cc2263da45 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
@@ -1,7 +1,7 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'package:amplify_api/src/decorators/authorize_websocket_message.dart';
+import 'package:amplify_api/src/decorators/websocket_auth_utils.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:async/async.dart';
 import 'package:flutter/material.dart';
@@ -23,7 +23,7 @@ class WebSocketConnection implements Closeable {
   late final StreamSubscription<WebSocketMessage> _subscription;
   late final RestartableTimer _timeoutTimer;
 
-  // Add connection error variable to throw in `init`.
+  // TODO: Add connection error variable to throw in `init`.
 
   Future<void>? _initFuture;
   final Completer<void> _connectionReady = Completer<void>();
@@ -42,6 +42,7 @@ class WebSocketConnection implements Closeable {
   /// {@macro websocket_connection}
   WebSocketConnection(this._config, this.authProviderRepo);
 
+  /// Connects stream to _onData handler.
   @visibleForTesting
   StreamSubscription<WebSocketMessage> getStreamSubscription(
       Stream<dynamic> stream) {
@@ -73,9 +74,8 @@ class WebSocketConnection implements Closeable {
   }
 
   Future<void> _init() async {
-    // Generate a URI for the connection and all subscriptions.
-    // See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection
-    final connectionUri = await _getConnectionUri(_config, authProviderRepo);
+    final connectionUri =
+        await generateConnectionUri(_config, authProviderRepo);
     await connect(connectionUri);
 
     if (_connectionReady.isCompleted) return;
@@ -92,17 +92,22 @@ class WebSocketConnection implements Closeable {
     if (!_connectionReady.isCompleted) {
       init();
     }
-    final subRegistration = WebSocketSubscriptionRegistrationMessage(
-      payload:
-          SubscriptionRegistrationPayload(request: request, config: _config),
-    );
-    final subscriptionId = subRegistration.id!;
+
+    final subscriptionId = uuid();
     return _messageStream
         .where((msg) => msg.id == subscriptionId)
         .transform(
             WebSocketSubscriptionStreamTransformer(request, onEstablished))
         .asBroadcastStream(
-          onListen: (_) => send(subRegistration),
+          onListen: (_) async {
+            final subscriptionRegistrationMessage =
+                await generateSubscriptionRegistrationMessage(_config,
+                    id: subscriptionId,
+                    authRepo: authProviderRepo,
+                    request: request);
+
+            send(subscriptionRegistrationMessage);
+          },
           onCancel: (_) => _cancel(subscriptionId),
         );
   }
@@ -113,18 +118,13 @@ class WebSocketConnection implements Closeable {
       id: subscriptionId,
       messageType: MessageType.stop,
     ));
-    // TODO(equartey): if this is the only susbscription, close the connection.
+    // TODO(equartey): if this is the only subscription, close the connection.
   }
 
   /// Sends a structured message over the WebSocket.
   @visibleForTesting
-  Future<void> send(WebSocketMessage message) async {
-    var authorizedMessage = message;
-    if (message is WebSocketSubscriptionRegistrationMessage) {
-      authorizedMessage =
-          await authorizeWebSocketMessage(message, _config, authProviderRepo);
-    }
-    final msgJson = json.encode(authorizedMessage.toJson());
+  void send(WebSocketMessage message) {
+    final msgJson = json.encode(message.toJson());
     _channel.sink.add(msgJson);
   }
 
@@ -182,19 +182,3 @@ class WebSocketConnection implements Closeable {
     _rebroadcastController.add(message);
   }
 }
-
-Future<Uri> _getConnectionUri(
-    AWSApiConfig config, AmplifyAuthProviderRepository authRepo) async {
-  const body = '{}';
-  final authorizationHeaders = await generateAuthorizationHeaders(config,
-      authRepo: authRepo, body: body);
-  final encodedAuthHeaders =
-      base64.encode(json.encode(authorizationHeaders).codeUnits);
-  final endpointUri = Uri.parse(
-      config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'));
-  return Uri(scheme: 'wss', host: endpointUri.host, path: 'graphql')
-      .replace(queryParameters: <String, String>{
-    'header': encodedAuthHeaders,
-    'payload': base64.encode(utf8.encode(body)) // always payload of '{}'
-  });
-}
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart b/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
index 3729f7cb6e..2522969550 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
@@ -77,11 +77,12 @@ class ConnectionAckMessagePayload extends WebSocketMessagePayload {
 class SubscriptionRegistrationPayload extends WebSocketMessagePayload {
   final GraphQLRequest request;
   final AWSApiConfig config;
-  Map<String, dynamic> authorizationHeaders = {};
+  final Map<String, dynamic> authorizationHeaders;
 
   SubscriptionRegistrationPayload({
     required this.request,
     required this.config,
+    required this.authorizationHeaders,
   });
 
   @override
@@ -186,6 +187,6 @@ class WebSocketConnectionInitMessage extends WebSocketMessage {
 
 class WebSocketSubscriptionRegistrationMessage extends WebSocketMessage {
   WebSocketSubscriptionRegistrationMessage(
-      {required SubscriptionRegistrationPayload payload})
-      : super(messageType: MessageType.start, payload: payload);
+      {required String id, required SubscriptionRegistrationPayload payload})
+      : super(messageType: MessageType.start, payload: payload, id: id);
 }
diff --git a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
index 61524efe39..be0f1f0d91 100644
--- a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
+++ b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
@@ -78,10 +78,10 @@ class MockWebSocketConnection extends WebSocketConnection {
     getStreamSubscription(broadcast);
   }
 
+  /// Pushes message in sentMessages and adds to stream (to support mocking result).
   @override
-  Future<void> send(WebSocketMessage message) async {
+  void send(WebSocketMessage message) {
     sentMessages.add(message);
-
     final messageStr = json.encode(message.toJson());
     messageStream.add(messageStr);
   }
@@ -148,7 +148,7 @@ void main() {
     });
 
     test(
-        'subscribe() should send SubscriptionRegistrationMessage with authorized payload',
+        'subscribe() should send SubscriptionRegistrationMessage with authorized payload correctly serialized',
         () async {
       connection.init();
       await connection.ready;
@@ -161,11 +161,19 @@ void main() {
       final lastMessage = connection.lastSentMessage;
       expect(lastMessage?.messageType, MessageType.start);
       final payloadJson = lastMessage?.payload?.toJson();
-      print(payloadJson);
-
-      // TODO assert payload authorized
+      final apiKeyFromPayload =
+          payloadJson?['extensions']['authorization'][xApiKey];
+      expect(apiKeyFromPayload, apiKey);
     });
 
-    test('subscribe() should return a subscription stream', () {});
+    // test('subscribe() should return a subscription stream', () async {
+    //   connection.init();
+    //   await connection.ready;
+    //   Completer<void> establishedCompleter = Completer();
+    //   final subscription = connection.subscribe(subscriptionRequest, () {
+    //     establishedCompleter.complete();
+    //   }).listen((event) {});
+    //   await establishedCompleter.future;
+    // });
   });
 }

From 5715cb5f0b45aa572d7558278811b289589d6078 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Wed, 27 Jul 2022 09:31:45 -0700
Subject: [PATCH 12/33] some renames

---
 .../amplify_api/lib/src/api_plugin_impl.dart  |  2 +-
 ..._utils.dart => web_socket_auth_utils.dart} |  4 +-
 ...ection.dart => web_socket_connection.dart} | 78 ++++++++++++-------
 ...eb_socket_message_stream_transformer.dart} | 16 +++-
 ...ket_message.dart => web_socket_types.dart} | 55 ++++++++++---
 .../test/ws/web_socket_connection_test.dart   |  6 +-
 6 files changed, 118 insertions(+), 43 deletions(-)
 rename packages/api/amplify_api/lib/src/decorators/{websocket_auth_utils.dart => web_socket_auth_utils.dart} (97%)
 rename packages/api/amplify_api/lib/src/graphql/ws/{websocket_connection.dart => web_socket_connection.dart} (66%)
 rename packages/api/amplify_api/lib/src/graphql/ws/{websocket_message_stream_transformer.dart => web_socket_message_stream_transformer.dart} (70%)
 rename packages/api/amplify_api/lib/src/graphql/ws/{websocket_message.dart => web_socket_types.dart} (75%)

diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index 7a60cdda0e..0a859f4613 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -17,7 +17,7 @@ library amplify_api;
 import 'dart:io';
 
 import 'package:amplify_api/amplify_api.dart';
-import 'package:amplify_api/src/graphql/ws/websocket_connection.dart';
+import 'package:amplify_api/src/graphql/ws/web_socket_connection.dart';
 import 'package:amplify_api/src/native_api_plugin.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:async/async.dart';
diff --git a/packages/api/amplify_api/lib/src/decorators/websocket_auth_utils.dart b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
similarity index 97%
rename from packages/api/amplify_api/lib/src/decorators/websocket_auth_utils.dart
rename to packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
index 58092aa224..fb19c1ce2b 100644
--- a/packages/api/amplify_api/lib/src/decorators/websocket_auth_utils.dart
+++ b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
@@ -18,7 +18,7 @@ import 'package:amplify_core/amplify_core.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
 
-import '../graphql/ws/websocket_message.dart';
+import '../graphql/ws/web_socket_types.dart';
 import 'authorize_http_request.dart';
 
 /// Generate a URI for the connection and all subscriptions.
@@ -82,7 +82,7 @@ Future<Map<String, String>> _generateAuthorizationHeaders(
   required String body,
 }) async {
   final endpointHost = Uri.parse(config.endpoint).host;
-  // Create canonical HTTP request to authorize.
+  // Create canonical HTTP request to authorize but never send.
   //
   // The canonical request URL is a little different depending on if connection_init
   // or start (subscription registration).
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
similarity index 66%
rename from packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
rename to packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
index cc2263da45..98ccaeaea8 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/websocket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
@@ -1,30 +1,58 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import 'dart:async';
 import 'dart:convert';
 
-import 'package:amplify_api/src/decorators/websocket_auth_utils.dart';
+import 'package:amplify_api/src/decorators/web_socket_auth_utils.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:async/async.dart';
-import 'package:flutter/material.dart';
+import 'package:meta/meta.dart';
 import 'package:web_socket_channel/web_socket_channel.dart';
 
-import 'websocket_message.dart';
-import 'websocket_message_stream_transformer.dart';
+import 'web_socket_message_stream_transformer.dart';
+import 'web_socket_types.dart';
 
-/// {@template websocket_connection}
+/// {@template amplify_api.web_socket_connection}
 /// Manages connection with an AppSync backend and subscription routing.
 /// {@endtemplate}
+@internal
 class WebSocketConnection implements Closeable {
+  /// Allowed protocols for this connection.
   static const webSocketProtocols = ['graphql-ws'];
 
-  final AmplifyAuthProviderRepository authProviderRepo;
-
+  // Config and auth repo together determine how to authorize connection URLs
+  // and subscription registration messages.
+  final AmplifyAuthProviderRepository _authProviderRepo;
   final AWSApiConfig _config;
+
+  // Manages all incoming messages from server. Primarily handles messages related
+  // to the entire connection. E.g. connection_ack, connection_error, ka, error.
+  // Other events (for single subscriptions) rebroadcast to _rebroadcastController.
   late final WebSocketChannel _channel;
   late final StreamSubscription<WebSocketMessage> _subscription;
   late final RestartableTimer _timeoutTimer;
 
+  // Re-broadcasts incoming messages for child streams (single GraphQL subscriptions).
+  // start_ack, data, error
+  final StreamController<WebSocketMessage> _rebroadcastController =
+      StreamController<WebSocketMessage>.broadcast();
+  Stream<WebSocketMessage> get _messageStream => _rebroadcastController.stream;
+
   // TODO: Add connection error variable to throw in `init`.
 
+  // Futures to manage initial connection state.
   Future<void>? _initFuture;
   final Completer<void> _connectionReady = Completer<void>();
 
@@ -32,17 +60,10 @@ class WebSocketConnection implements Closeable {
   /// after the first `connection_ack` message.
   Future<void> get ready => _connectionReady.future;
 
-  /// Re-broadcasts messages for child streams.
-  final StreamController<WebSocketMessage> _rebroadcastController =
-      StreamController<WebSocketMessage>.broadcast();
-
-  /// Incoming message stream for all events.
-  Stream<WebSocketMessage> get _messageStream => _rebroadcastController.stream;
-
-  /// {@macro websocket_connection}
-  WebSocketConnection(this._config, this.authProviderRepo);
+  /// {@macro amplify_api.web_socket_connection}
+  WebSocketConnection(this._config, this._authProviderRepo);
 
-  /// Connects stream to _onData handler.
+  /// Connects _subscription stream to _onData handler.
   @visibleForTesting
   StreamSubscription<WebSocketMessage> getStreamSubscription(
       Stream<dynamic> stream) {
@@ -51,7 +72,8 @@ class WebSocketConnection implements Closeable {
         .listen(_onData);
   }
 
-  /// Connects to the real time WebSocket.
+  /// Connects WebSocket channel to _subscription stream but does not send connection
+  /// init message.
   @visibleForTesting
   Future<void> connect(Uri connectionUri) async {
     _channel = WebSocketChannel.connect(
@@ -69,13 +91,16 @@ class WebSocketConnection implements Closeable {
   }
 
   /// Initializes the connection.
+  ///
+  /// Connects to WebSocket, sends connection message and resolves future once
+  /// connection_ack message received from server.
   Future<void> init() {
     return _initFuture ??= _init();
   }
 
   Future<void> _init() async {
     final connectionUri =
-        await generateConnectionUri(_config, authProviderRepo);
+        await generateConnectionUri(_config, _authProviderRepo);
     await connect(connectionUri);
 
     if (_connectionReady.isCompleted) return;
@@ -100,10 +125,11 @@ class WebSocketConnection implements Closeable {
             WebSocketSubscriptionStreamTransformer(request, onEstablished))
         .asBroadcastStream(
           onListen: (_) async {
+            // Callout: need to reconsider sending start message onListen.
             final subscriptionRegistrationMessage =
                 await generateSubscriptionRegistrationMessage(_config,
                     id: subscriptionId,
-                    authRepo: authProviderRepo,
+                    authRepo: _authProviderRepo,
                     request: request);
 
             send(subscriptionRegistrationMessage);
@@ -114,14 +140,11 @@ class WebSocketConnection implements Closeable {
 
   /// Cancels a subscription.
   void _cancel(String subscriptionId) {
-    send(WebSocketMessage(
-      id: subscriptionId,
-      messageType: MessageType.stop,
-    ));
+    send(WebSocketStopMessage(id: subscriptionId));
     // TODO(equartey): if this is the only subscription, close the connection.
   }
 
-  /// Sends a structured message over the WebSocket.
+  /// Serializes a message as JSON string and sends over WebSocket channel.
   @visibleForTesting
   void send(WebSocketMessage message) {
     final msgJson = json.encode(message.toJson());
@@ -139,6 +162,9 @@ class WebSocketConnection implements Closeable {
   }
 
   /// Handles incoming data on the WebSocket.
+  ///
+  /// Here, handle connection-wide messages and pass subscription events to
+  /// `_rebroadcastController`.
   void _onData(WebSocketMessage message) {
     switch (message.messageType) {
       case MessageType.connectionAck:
@@ -178,7 +204,7 @@ class WebSocketConnection implements Closeable {
         break;
     }
 
-    // Re-broadcast unhandled messages
+    // Re-broadcast other message types related to single subscriptions.
     _rebroadcastController.add(message);
   }
 }
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message_stream_transformer.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
similarity index 70%
rename from packages/api/amplify_api/lib/src/graphql/ws/websocket_message_stream_transformer.dart
rename to packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
index 90afd0d429..ea6c195d67 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message_stream_transformer.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
@@ -1,11 +1,25 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import 'dart:async';
 import 'dart:convert';
 
 import 'package:amplify_api/src/util.dart';
 import 'package:amplify_core/amplify_core.dart';
-import 'websocket_message.dart';
 
 import '../graphql_response_decoder.dart';
+import 'web_socket_types.dart';
 
 class WebSocketMessageStreamTransformer
     extends StreamTransformerBase<dynamic, WebSocketMessage> {
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
similarity index 75%
rename from packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
rename to packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
index 2522969550..1db9caf8f7 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/websocket_message.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
@@ -1,7 +1,25 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// ignore_for_file: public_member_api_docs
+
 import 'dart:convert';
 
 import 'package:amplify_core/amplify_core.dart';
+import 'package:meta/meta.dart';
 
+@internal
 class MessageType {
   final String type;
 
@@ -38,8 +56,9 @@ class MessageType {
   String toString() => type;
 }
 
+@internal
 abstract class WebSocketMessagePayload {
-  WebSocketMessagePayload();
+  const WebSocketMessagePayload();
 
   static const Map<MessageType, WebSocketMessagePayload Function(Map)>
       _factories = {
@@ -58,10 +77,11 @@ abstract class WebSocketMessagePayload {
   String toString() => prettyPrintJson(toJson());
 }
 
+@internal
 class ConnectionAckMessagePayload extends WebSocketMessagePayload {
   final int connectionTimeoutMs;
 
-  ConnectionAckMessagePayload(this.connectionTimeoutMs);
+  const ConnectionAckMessagePayload(this.connectionTimeoutMs);
 
   static ConnectionAckMessagePayload fromJson(Map json) {
     final connectionTimeoutMs = json['connectionTimeoutMs'] as int;
@@ -74,27 +94,31 @@ class ConnectionAckMessagePayload extends WebSocketMessagePayload {
       };
 }
 
+@internal
 class SubscriptionRegistrationPayload extends WebSocketMessagePayload {
   final GraphQLRequest request;
   final AWSApiConfig config;
-  final Map<String, dynamic> authorizationHeaders;
+  final Map<String, String> authorizationHeaders;
 
-  SubscriptionRegistrationPayload({
+  const SubscriptionRegistrationPayload({
     required this.request,
     required this.config,
     required this.authorizationHeaders,
   });
 
   @override
-  Map<String, dynamic> toJson() {
-    return <String, dynamic>{
+  Map<String, Object> toJson() {
+    return <String, Object>{
       'data': jsonEncode(
           {'variables': request.variables, 'query': request.document}),
-      'extensions': <String, dynamic>{'authorization': authorizationHeaders}
+      'extensions': <String, Map<String, String>>{
+        'authorization': authorizationHeaders
+      }
     };
   }
 }
 
+@internal
 class SubscriptionDataPayload extends WebSocketMessagePayload {
   final Map<String, dynamic>? data;
   final Map<String, dynamic>? errors;
@@ -117,6 +141,7 @@ class SubscriptionDataPayload extends WebSocketMessagePayload {
       };
 }
 
+@internal
 class WebSocketError extends WebSocketMessagePayload implements Exception {
   final List<Map> errors;
 
@@ -133,6 +158,7 @@ class WebSocketError extends WebSocketMessagePayload implements Exception {
       };
 }
 
+@internal
 class WebSocketMessage {
   final String? id;
   final MessageType messageType;
@@ -180,13 +206,22 @@ class WebSocketMessage {
   }
 }
 
+@internal
 class WebSocketConnectionInitMessage extends WebSocketMessage {
   WebSocketConnectionInitMessage()
       : super(messageType: MessageType.connectionInit);
 }
 
+@internal
 class WebSocketSubscriptionRegistrationMessage extends WebSocketMessage {
-  WebSocketSubscriptionRegistrationMessage(
-      {required String id, required SubscriptionRegistrationPayload payload})
-      : super(messageType: MessageType.start, payload: payload, id: id);
+  WebSocketSubscriptionRegistrationMessage({
+    required String id,
+    required SubscriptionRegistrationPayload payload,
+  }) : super(messageType: MessageType.start, payload: payload, id: id);
+}
+
+@internal
+class WebSocketStopMessage extends WebSocketMessage {
+  WebSocketStopMessage({required String id})
+      : super(messageType: MessageType.stop, id: id);
 }
diff --git a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
index be0f1f0d91..a18a63e582 100644
--- a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
+++ b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
@@ -17,7 +17,7 @@ import 'dart:convert';
 
 import 'package:amplify_api/src/api_plugin_impl.dart';
 import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
-import 'package:amplify_api/src/graphql/ws/websocket_message.dart';
+import 'package:amplify_api/src/graphql/ws/web_socket_types.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:collection/collection.dart';
 import 'package:flutter_test/flutter_test.dart';
@@ -25,7 +25,7 @@ import 'package:web_socket_channel/web_socket_channel.dart';
 
 import '../util.dart';
 
-import 'package:amplify_api/src/graphql/ws/websocket_connection.dart';
+import 'package:amplify_api/src/graphql/ws/web_socket_connection.dart';
 
 /// Extension of [WebSocketConnection] that stores messages internally instead
 /// of sending them.
@@ -59,7 +59,7 @@ class MockWebSocketConnection extends WebSocketConnection {
       if (messageFromEvent.messageType == MessageType.connectionInit) {
         mockAckMsg = WebSocketMessage(
           messageType: MessageType.connectionAck,
-          payload: ConnectionAckMessagePayload(10000),
+          payload: const ConnectionAckMessagePayload(10000),
         );
         // start, respond with start_ack
       } else if (messageFromEvent.messageType == MessageType.start) {

From db28cab2d1558a511a76f83381255050b5876bfe Mon Sep 17 00:00:00 2001
From: Elijah Quartey <Equartey@users.noreply.github.com>
Date: Fri, 29 Jul 2022 09:31:47 -0500
Subject: [PATCH 13/33] feat(api): GraphQL Custom Request Headers (#1938)

---
 .../lib/src/types/api/graphql/graphql_request.dart           | 5 +++++
 .../amplify_api/lib/src/graphql/send_graphql_request.dart    | 3 ++-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/packages/amplify_core/lib/src/types/api/graphql/graphql_request.dart b/packages/amplify_core/lib/src/types/api/graphql/graphql_request.dart
index ff77c1713c..26778fdee7 100644
--- a/packages/amplify_core/lib/src/types/api/graphql/graphql_request.dart
+++ b/packages/amplify_core/lib/src/types/api/graphql/graphql_request.dart
@@ -22,6 +22,9 @@ class GraphQLRequest<T> {
   /// Only required if your backend has multiple GraphQL endpoints in the amplifyconfiguration.dart file. This parameter is then needed to specify which one to use for this request.
   final String? apiName;
 
+  /// A map of Strings to dynamically use for custom headers in the http request.
+  final Map<String, String>? headers;
+
   /// The body of the request, starting with the operation type and operation name.
   ///
   /// See https://graphql.org/learn/queries/#operation-name for examples and more information.
@@ -57,12 +60,14 @@ class GraphQLRequest<T> {
       {this.apiName,
       required this.document,
       this.variables = const <String, dynamic>{},
+      this.headers,
       this.decodePath,
       this.modelType});
 
   Map<String, dynamic> serializeAsMap() => <String, dynamic>{
         'document': document,
         'variables': variables,
+        'headers': headers,
         'cancelToken': id,
         if (apiName != null) 'apiName': apiName,
       };
diff --git a/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart b/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
index 6eab7deadd..3ba0a36c7d 100644
--- a/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
+++ b/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
@@ -31,7 +31,8 @@ Future<GraphQLResponse<T>> sendGraphQLRequest<T>({
 }) async {
   try {
     final body = {'variables': request.variables, 'query': request.document};
-    final graphQLResponse = await client.post(uri, body: json.encode(body));
+    final graphQLResponse = await client.post(uri,
+        body: json.encode(body), headers: request.headers);
 
     final responseBody = json.decode(graphQLResponse.body);
 

From c8297a6463a1a7c6ccf49bec733d5ec36bab2b49 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Mon, 8 Aug 2022 08:58:01 -0800
Subject: [PATCH 14/33] feat(auth,api): cognito user pools auth provider & auth
 mode for API HTTP requests (#1913)

---
 .../decorators/authorize_http_request.dart    |   9 +-
 .../test/authorize_http_request_test.dart     |  34 ++-
 packages/api/amplify_api/test/util.dart       |   9 +
 .../lib/src/auth_plugin_impl.dart             |  14 +-
 .../cognito_user_pools_auth_provider.dart     |  37 +++
 .../test/plugin/auth_providers_test.dart      | 210 ++++++++++++++----
 6 files changed, 255 insertions(+), 58 deletions(-)
 create mode 100644 packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart

diff --git a/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
index 3cab4d7443..24a343895e 100644
--- a/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
+++ b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
@@ -64,8 +64,15 @@ Future<http.BaseRequest> authorizeHttpRequest(http.BaseRequest request,
       return authorizedRequest.httpRequest;
     case APIAuthorizationType.function:
     case APIAuthorizationType.oidc:
-    case APIAuthorizationType.userPools:
       throw UnimplementedError('${authType.name} not implemented.');
+    case APIAuthorizationType.userPools:
+      final authProvider = _validateAuthProvider(
+        authProviderRepo.getAuthProvider(authType.authProviderToken),
+        authType,
+      );
+      final authorizedRequest =
+          await authProvider.authorizeRequest(_httpToAWSRequest(request));
+      return authorizedRequest.httpRequest;
     case APIAuthorizationType.none:
       return request;
   }
diff --git a/packages/api/amplify_api/test/authorize_http_request_test.dart b/packages/api/amplify_api/test/authorize_http_request_test.dart
index 2179a07ad8..3f1ad3754d 100644
--- a/packages/api/amplify_api/test/authorize_http_request_test.dart
+++ b/packages/api/amplify_api/test/authorize_http_request_test.dart
@@ -33,11 +33,19 @@ void main() {
   final authProviderRepo = AmplifyAuthProviderRepository();
 
   setUpAll(() {
-    authProviderRepo.registerAuthProvider(
+    authProviderRepo
+      ..registerAuthProvider(
         APIAuthorizationType.apiKey.authProviderToken,
-        AppSyncApiKeyAuthProvider());
-    authProviderRepo.registerAuthProvider(
-        APIAuthorizationType.iam.authProviderToken, TestIamAuthProvider());
+        AppSyncApiKeyAuthProvider(),
+      )
+      ..registerAuthProvider(
+        APIAuthorizationType.iam.authProviderToken,
+        TestIamAuthProvider(),
+      )
+      ..registerAuthProvider(
+        APIAuthorizationType.userPools.authProviderToken,
+        TestTokenAuthProvider(),
+      );
   });
 
   group('authorizeHttpRequest', () {
@@ -132,7 +140,23 @@ void main() {
           throwsA(isA<ApiException>()));
     });
 
-    test('authorizes with Cognito User Pools auth mode', () {}, skip: true);
+    test('authorizes with Cognito User Pools auth mode', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.userPools,
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      expect(
+        authorizedRequest.headers[AWSHeaders.authorization],
+        testAccessToken,
+      );
+    });
 
     test('authorizes with OIDC auth mode', () {}, skip: true);
 
diff --git a/packages/api/amplify_api/test/util.dart b/packages/api/amplify_api/test/util.dart
index f3c2ef551e..cd06f8c13c 100644
--- a/packages/api/amplify_api/test/util.dart
+++ b/packages/api/amplify_api/test/util.dart
@@ -17,6 +17,8 @@ import 'package:aws_signature_v4/aws_signature_v4.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:http/http.dart' as http;
 
+const testAccessToken = 'test-access-token-123';
+
 class TestIamAuthProvider extends AWSIamAmplifyAuthProvider {
   @override
   Future<AWSCredentials> retrieve() async {
@@ -43,6 +45,13 @@ class TestIamAuthProvider extends AWSIamAmplifyAuthProvider {
   }
 }
 
+class TestTokenAuthProvider extends TokenAmplifyAuthProvider {
+  @override
+  Future<String> getLatestAuthToken() async {
+    return testAccessToken;
+  }
+}
+
 void validateSignedRequest(http.BaseRequest request) {
   const userAgentHeader =
       zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
index bec1774a51..89ee79adce 100644
--- a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
+++ b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
@@ -51,6 +51,7 @@ import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart
 import 'package:amplify_auth_cognito_dart/src/sdk/sdk_bridge.dart';
 import 'package:amplify_auth_cognito_dart/src/state/state.dart';
 import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart';
+import 'package:amplify_auth_cognito_dart/src/util/cognito_user_pools_auth_provider.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:amplify_secure_storage_dart/amplify_secure_storage_dart.dart';
 import 'package:built_collection/built_collection.dart';
@@ -180,10 +181,15 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
 
     // Register auth providers to provide auth functionality to other plugins
     // without requiring other plugins to call `Amplify.Auth...` directly.
-    authProviderRepo.registerAuthProvider(
-      APIAuthorizationType.iam.authProviderToken,
-      CognitoIamAuthProvider(),
-    );
+    authProviderRepo
+      ..registerAuthProvider(
+        APIAuthorizationType.iam.authProviderToken,
+        CognitoIamAuthProvider(),
+      )
+      ..registerAuthProvider(
+        APIAuthorizationType.userPools.authProviderToken,
+        CognitoUserPoolsAuthProvider(),
+      );
 
     if (_stateMachine.getOrCreate(AuthStateMachine.type).currentState.type !=
         AuthStateType.notConfigured) {
diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart
new file mode 100644
index 0000000000..edde7c3bca
--- /dev/null
+++ b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart
@@ -0,0 +1,37 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:meta/meta.dart';
+
+/// [AmplifyAuthProvider] implementation that adds access token to request headers.
+@internal
+class CognitoUserPoolsAuthProvider extends TokenAmplifyAuthProvider {
+  /// Get access token from `Amplify.Auth.fetchAuthSession()`.
+  @override
+  Future<String> getLatestAuthToken() async {
+    final authSession =
+        await Amplify.Auth.fetchAuthSession() as CognitoAuthSession;
+    final token = authSession.userPoolTokens?.accessToken.raw;
+    if (token == null) {
+      throw const AuthException(
+        'Unable to fetch access token while authorizing with Cognito User Pools.',
+      );
+    }
+    return token;
+  }
+}
diff --git a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
index acb126fa66..de1d20b496 100644
--- a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
+++ b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
@@ -15,7 +15,9 @@ import 'dart:async';
 
 import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart'
     hide InternalErrorException;
+import 'package:amplify_auth_cognito_dart/src/credentials/cognito_keys.dart';
 import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart';
+import 'package:amplify_auth_cognito_dart/src/util/cognito_user_pools_auth_provider.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:test/test.dart';
 
@@ -29,84 +31,196 @@ AWSHttpRequest _generateTestRequest() {
   );
 }
 
-/// Returns dummy AWS credentials.
-class TestAmplifyAuth extends AmplifyAuthCognitoDart {
+/// Mock implementation of user pool only error when trying to get credentials.
+class TestAmplifyAuthUserPoolOnly extends AmplifyAuthCognitoDart {
   @override
   Future<AuthSession> fetchAuthSession({
     required AuthSessionRequest request,
   }) async {
-    return const CognitoAuthSession(
+    final options = request.options as CognitoSessionOptions?;
+    final getAWSCredentials = options?.getAWSCredentials;
+    if (getAWSCredentials != null && getAWSCredentials) {
+      throw const InvalidAccountTypeException.noIdentityPool(
+        recoverySuggestion:
+            'Register an identity pool using the CLI or set getAWSCredentials '
+            'to false',
+      );
+    }
+    return CognitoAuthSession(
       isSignedIn: true,
-      credentials: AWSCredentials('fakeKeyId', 'fakeSecret'),
+      userPoolTokens: CognitoUserPoolTokens(
+        accessToken: accessToken,
+        idToken: idToken,
+        refreshToken: refreshToken,
+      ),
     );
   }
 }
 
 void main() {
+  late AmplifyAuthCognitoDart plugin;
+  late AmplifyAuthProviderRepository testAuthRepo;
+
+  setUpAll(() async {
+    testAuthRepo = AmplifyAuthProviderRepository();
+    final secureStorage = MockSecureStorage();
+    final stateMachine = CognitoAuthStateMachine()..addInstance(secureStorage);
+    plugin = AmplifyAuthCognitoDart(credentialStorage: secureStorage)
+      ..stateMachine = stateMachine;
+
+    seedStorage(
+      secureStorage,
+      userPoolKeys: CognitoUserPoolKeys(userPoolConfig),
+      identityPoolKeys: CognitoIdentityPoolKeys(identityPoolConfig),
+    );
+
+    await plugin.configure(
+      config: mockConfig,
+      authProviderRepo: testAuthRepo,
+    );
+  });
+
   group(
       'AmplifyAuthCognitoDart plugin registers auth providers during configuration',
       () {
-    late AmplifyAuthCognitoDart plugin;
-
-    setUp(() async {
-      plugin = AmplifyAuthCognitoDart(credentialStorage: MockSecureStorage());
-    });
-
     test('registers CognitoIamAuthProvider', () async {
-      final testAuthRepo = AmplifyAuthProviderRepository();
-      await plugin.configure(
-        config: mockConfig,
-        authProviderRepo: testAuthRepo,
-      );
       final authProvider = testAuthRepo.getAuthProvider(
         APIAuthorizationType.iam.authProviderToken,
       );
       expect(authProvider, isA<CognitoIamAuthProvider>());
     });
-  });
-
-  group('CognitoIamAuthProvider', () {
-    setUpAll(() async {
-      await Amplify.addPlugin(TestAmplifyAuth());
-    });
 
-    test('gets AWS credentials from Amplify.Auth.fetchAuthSession', () async {
-      final authProvider = CognitoIamAuthProvider();
-      final credentials = await authProvider.retrieve();
-      expect(credentials.accessKeyId, isA<String>());
-      expect(credentials.secretAccessKey, isA<String>());
+    test('registers CognitoUserPoolsAuthProvider', () async {
+      final authProvider = testAuthRepo.getAuthProvider(
+        APIAuthorizationType.userPools.authProviderToken,
+      );
+      expect(authProvider, isA<CognitoUserPoolsAuthProvider>());
     });
+  });
 
-    test('signs a request when calling authorizeRequest', () async {
+  group('no auth plugin added', () {
+    test('CognitoIamAuthProvider throws when trying to authorize a request',
+        () async {
       final authProvider = CognitoIamAuthProvider();
-      final authorizedRequest = await authProvider.authorizeRequest(
-        _generateTestRequest(),
-        options: const IamAuthProviderOptions(
-          region: 'us-east-1',
-          service: AWSService.appSync,
+      await expectLater(
+        authProvider.authorizeRequest(
+          _generateTestRequest(),
+          options: const IamAuthProviderOptions(
+            region: 'us-east-1',
+            service: AWSService.appSync,
+          ),
         ),
-      );
-      // Note: not intended to be complete test of sigv4 algorithm.
-      expect(authorizedRequest.headers[AWSHeaders.authorization], isNotEmpty);
-      const userAgentHeader =
-          zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
-      expect(
-        authorizedRequest.headers[AWSHeaders.host],
-        isNotEmpty,
-        skip: zIsWeb,
-      );
-      expect(
-        authorizedRequest.headers[userAgentHeader],
-        contains('aws-sigv4'),
+        throwsA(isA<AmplifyException>()),
       );
     });
 
-    test('throws when no options provided', () async {
-      final authProvider = CognitoIamAuthProvider();
+    test('CognitoUserPoolsAuthProvider throws when trying to authorize request',
+        () async {
+      final authProvider = CognitoUserPoolsAuthProvider();
       await expectLater(
         authProvider.authorizeRequest(_generateTestRequest()),
-        throwsA(isA<AuthException>()),
+        throwsA(isA<AmplifyException>()),
       );
     });
   });
+
+  group('auth providers defined in auth plugin', () {
+    setUpAll(() async {
+      await Amplify.reset();
+      await Amplify.addPlugin(plugin);
+    });
+
+    group('CognitoIamAuthProvider', () {
+      test('gets AWS credentials from Amplify.Auth.fetchAuthSession', () async {
+        final authProvider = CognitoIamAuthProvider();
+        final credentials = await authProvider.retrieve();
+        expect(credentials.accessKeyId, isA<String>());
+        expect(credentials.secretAccessKey, isA<String>());
+      });
+
+      test('signs a request when calling authorizeRequest', () async {
+        final authProvider = CognitoIamAuthProvider();
+        final authorizedRequest = await authProvider.authorizeRequest(
+          _generateTestRequest(),
+          options: const IamAuthProviderOptions(
+            region: 'us-east-1',
+            service: AWSService.appSync,
+          ),
+        );
+        // Note: not intended to be complete test of sigv4 algorithm.
+        expect(authorizedRequest.headers[AWSHeaders.authorization], isNotEmpty);
+        const userAgentHeader =
+            zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+        expect(
+          authorizedRequest.headers[AWSHeaders.host],
+          isNotEmpty,
+          skip: zIsWeb,
+        );
+        expect(
+          authorizedRequest.headers[userAgentHeader],
+          contains('aws-sigv4'),
+        );
+      });
+
+      test('throws when no options provided', () async {
+        final authProvider = CognitoIamAuthProvider();
+        await expectLater(
+          authProvider.authorizeRequest(_generateTestRequest()),
+          throwsA(isA<AuthException>()),
+        );
+      });
+    });
+
+    group('CognitoUserPoolsAuthProvider', () {
+      test('gets raw access token from Amplify.Auth.fetchAuthSession',
+          () async {
+        final authProvider = CognitoUserPoolsAuthProvider();
+        final token = await authProvider.getLatestAuthToken();
+        expect(token, accessToken.raw);
+      });
+
+      test('adds access token to header when calling authorizeRequest',
+          () async {
+        final authProvider = CognitoUserPoolsAuthProvider();
+        final authorizedRequest = await authProvider.authorizeRequest(
+          _generateTestRequest(),
+        );
+        expect(
+          authorizedRequest.headers[AWSHeaders.authorization],
+          accessToken.raw,
+        );
+      });
+    });
+  });
+
+  group('auth providers with user pool-only configuration', () {
+    setUpAll(() async {
+      await Amplify.reset();
+      await Amplify.addPlugin(TestAmplifyAuthUserPoolOnly());
+    });
+
+    group('CognitoIamAuthProvider', () {
+      test('throws when trying to retrieve credentials', () async {
+        final authProvider = CognitoIamAuthProvider();
+        await expectLater(
+          authProvider.retrieve(),
+          throwsA(isA<InvalidAccountTypeException>()),
+        );
+      });
+    });
+
+    group('CognitoUserPoolsAuthProvider', () {
+      test('adds access token to header when calling authorizeRequest',
+          () async {
+        final authProvider = CognitoUserPoolsAuthProvider();
+        final authorizedRequest = await authProvider.authorizeRequest(
+          _generateTestRequest(),
+        );
+        expect(
+          authorizedRequest.headers[AWSHeaders.authorization],
+          accessToken.raw,
+        );
+      });
+    });
+  });
 }

From c464e6e4e90284af48c45601ec51e1910015be5a Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Wed, 10 Aug 2022 13:13:33 -0700
Subject: [PATCH 15/33] change disconnect

---
 .../example/lib/graphql_api_view.dart         | 20 ++++++---
 .../src/graphql/ws/web_socket_connection.dart | 44 ++++++++++---------
 .../lib/src/graphql/ws/web_socket_types.dart  |  2 +
 3 files changed, 39 insertions(+), 27 deletions(-)

diff --git a/packages/api/amplify_api/example/lib/graphql_api_view.dart b/packages/api/amplify_api/example/lib/graphql_api_view.dart
index 6644dad380..fa0f2f345f 100644
--- a/packages/api/amplify_api/example/lib/graphql_api_view.dart
+++ b/packages/api/amplify_api/example/lib/graphql_api_view.dart
@@ -45,13 +45,19 @@ class _GraphQLApiViewState extends State<GraphQLApiView> {
       onEstablished: () => print('Subscription established'),
     );
 
-    try {
-      await for (var event in operation) {
-        print('Subscription event data received: ${event.data}');
-      }
-    } on Exception catch (e) {
-      print('Error in subscription stream: $e');
-    }
+    final streamSubscription = operation.listen(
+      (event) {
+        final result = 'Subscription event data received: ${event.data}';
+        print(result);
+        setState(() {
+          _result = result;
+        });
+      },
+      onError: (Object error) => print(
+        'Error in GraphQL subscription: $error',
+      ),
+    );
+    _unsubscribe = streamSubscription.cancel;
   }
 
   Future<void> query() async {
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
index 98ccaeaea8..eb0fc99e2f 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
@@ -19,11 +19,15 @@ import 'package:amplify_api/src/decorators/web_socket_auth_utils.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:async/async.dart';
 import 'package:meta/meta.dart';
+import 'package:web_socket_channel/status.dart' as status;
 import 'package:web_socket_channel/web_socket_channel.dart';
 
 import 'web_socket_message_stream_transformer.dart';
 import 'web_socket_types.dart';
 
+/// 1001, going away
+const _defaultCloseStatus = status.goingAway;
+
 /// {@template amplify_api.web_socket_connection}
 /// Manages connection with an AppSync backend and subscription routing.
 /// {@endtemplate}
@@ -53,7 +57,7 @@ class WebSocketConnection implements Closeable {
   // TODO: Add connection error variable to throw in `init`.
 
   // Futures to manage initial connection state.
-  Future<void>? _initFuture;
+  final _initMemo = AsyncMemoizer<void>();
   final Completer<void> _connectionReady = Completer<void>();
 
   /// Fires when the connection is ready to be listened to, i.e.
@@ -85,18 +89,21 @@ class WebSocketConnection implements Closeable {
 
   /// Closes the WebSocket connection.
   @override
-  void close() {
+  void close([int closeStatus = _defaultCloseStatus]) {
+    final reason =
+        closeStatus == _defaultCloseStatus ? 'client closed' : 'unknown';
     _subscription.cancel();
-    _channel.sink.close();
+    _channel.sink.close(closeStatus, reason);
+    _rebroadcastController.close();
+    _timeoutTimer.cancel();
   }
 
   /// Initializes the connection.
   ///
   /// Connects to WebSocket, sends connection message and resolves future once
-  /// connection_ack message received from server.
-  Future<void> init() {
-    return _initFuture ??= _init();
-  }
+  /// connection_ack message received from server. If the connection was previously
+  /// established then will return previously completed future.
+  Future<void> init() => _initMemo.runOnce(_init);
 
   Future<void> _init() async {
     final connectionUri =
@@ -105,6 +112,7 @@ class WebSocketConnection implements Closeable {
 
     if (_connectionReady.isCompleted) return;
     send(WebSocketConnectionInitMessage());
+
     return ready;
   }
 
@@ -119,23 +127,19 @@ class WebSocketConnection implements Closeable {
     }
 
     final subscriptionId = uuid();
+
+    generateSubscriptionRegistrationMessage(
+      _config,
+      id: subscriptionId,
+      authRepo: _authProviderRepo,
+      request: request,
+    ).then(send);
+
     return _messageStream
         .where((msg) => msg.id == subscriptionId)
         .transform(
             WebSocketSubscriptionStreamTransformer(request, onEstablished))
-        .asBroadcastStream(
-          onListen: (_) async {
-            // Callout: need to reconsider sending start message onListen.
-            final subscriptionRegistrationMessage =
-                await generateSubscriptionRegistrationMessage(_config,
-                    id: subscriptionId,
-                    authRepo: _authProviderRepo,
-                    request: request);
-
-            send(subscriptionRegistrationMessage);
-          },
-          onCancel: (_) => _cancel(subscriptionId),
-        );
+        .asBroadcastStream(onCancel: (_) => _cancel(subscriptionId));
   }
 
   /// Cancels a subscription.
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
index 1db9caf8f7..0a754122bd 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
@@ -56,6 +56,7 @@ class MessageType {
   String toString() => type;
 }
 
+@immutable
 @internal
 abstract class WebSocketMessagePayload {
   const WebSocketMessagePayload();
@@ -158,6 +159,7 @@ class WebSocketError extends WebSocketMessagePayload implements Exception {
       };
 }
 
+@immutable
 @internal
 class WebSocketMessage {
   final String? id;

From d93e00c5e456ea22ad1ba1fec2d5bcafa883799b Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Wed, 10 Aug 2022 14:12:12 -0700
Subject: [PATCH 16/33] add logger

---
 .../amplify_api/lib/src/api_plugin_impl.dart  |  6 ++++-
 .../src/graphql/ws/web_socket_connection.dart | 23 ++++++++++++++-----
 ...web_socket_message_stream_transformer.dart | 19 +++++++++++----
 .../test/ws/web_socket_connection_test.dart   |  4 +++-
 4 files changed, 40 insertions(+), 12 deletions(-)

diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index 0a859f4613..a6a574b5e2 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -38,6 +38,7 @@ class AmplifyAPIDart extends AmplifyAPI {
   late final AWSApiPluginConfig _apiConfig;
   final http.Client? _baseHttpClient;
   late final AmplifyAuthProviderRepository _authProviderRepo;
+  final _logger = AmplifyLogger.category(Category.api);
 
   /// A map of the keys from the Amplify API config to HTTP clients to use for
   /// requests to that endpoint.
@@ -138,7 +139,10 @@ class AmplifyAPIDart extends AmplifyAPI {
       apiName: apiName,
     );
     return _webSocketConnectionPool[endpoint.name] ??=
-        WebSocketConnection(endpoint.config, _authProviderRepo);
+        WebSocketConnection(endpoint.config, _authProviderRepo,
+            logger: _logger.createChild(
+              'webSocketConnection${endpoint.name}',
+            ));
   }
 
   Uri _getGraphQLUri(String? apiName) {
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
index eb0fc99e2f..9a1f2354d3 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
@@ -35,6 +35,7 @@ const _defaultCloseStatus = status.goingAway;
 class WebSocketConnection implements Closeable {
   /// Allowed protocols for this connection.
   static const webSocketProtocols = ['graphql-ws'];
+  final AmplifyLogger _logger;
 
   // Config and auth repo together determine how to authorize connection URLs
   // and subscription registration messages.
@@ -65,7 +66,9 @@ class WebSocketConnection implements Closeable {
   Future<void> get ready => _connectionReady.future;
 
   /// {@macro amplify_api.web_socket_connection}
-  WebSocketConnection(this._config, this._authProviderRepo);
+  WebSocketConnection(this._config, this._authProviderRepo,
+      {required AmplifyLogger logger})
+      : _logger = logger;
 
   /// Connects _subscription stream to _onData handler.
   @visibleForTesting
@@ -126,8 +129,8 @@ class WebSocketConnection implements Closeable {
       init();
     }
 
+    // Generate and send an authorized subscription registration message.
     final subscriptionId = uuid();
-
     generateSubscriptionRegistrationMessage(
       _config,
       id: subscriptionId,
@@ -135,15 +138,21 @@ class WebSocketConnection implements Closeable {
       request: request,
     ).then(send);
 
+    // Filter incoming messages that have the subscription ID and return as new
+    // stream with messages converted to GraphQLResponse<T>.
     return _messageStream
         .where((msg) => msg.id == subscriptionId)
-        .transform(
-            WebSocketSubscriptionStreamTransformer(request, onEstablished))
+        .transform(WebSocketSubscriptionStreamTransformer(
+          request,
+          onEstablished,
+          logger: _logger,
+        ))
         .asBroadcastStream(onCancel: (_) => _cancel(subscriptionId));
   }
 
   /// Cancels a subscription.
   void _cancel(String subscriptionId) {
+    _logger.info('Attempting to cancel Operation $subscriptionId');
     send(WebSocketStopMessage(id: subscriptionId));
     // TODO(equartey): if this is the only subscription, close the connection.
   }
@@ -170,6 +179,8 @@ class WebSocketConnection implements Closeable {
   /// Here, handle connection-wide messages and pass subscription events to
   /// `_rebroadcastController`.
   void _onData(WebSocketMessage message) {
+    _logger.verbose('websocket received message: $message');
+
     switch (message.messageType) {
       case MessageType.connectionAck:
         final messageAck = message.payload as ConnectionAckMessagePayload;
@@ -181,7 +192,7 @@ class WebSocketConnection implements Closeable {
           () => _timeout(timeoutDuration),
         );
         _connectionReady.complete();
-        // print('Registered timer');
+        _logger.verbose('Connection established. Registered timer');
         return;
       case MessageType.connectionError:
         final wsError = message.payload as WebSocketError?;
@@ -194,7 +205,7 @@ class WebSocketConnection implements Closeable {
         return;
       case MessageType.keepAlive:
         _timeoutTimer.reset();
-        // print('Reset timer');
+        _logger.verbose('Reset timer');
         return;
       case MessageType.error:
         // Only handle general messages, not subscription-specific ones
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
index ea6c195d67..72d9c7160a 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
@@ -12,15 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// ignore_for_file: public_member_api_docs
+
 import 'dart:async';
 import 'dart:convert';
 
 import 'package:amplify_api/src/util.dart';
 import 'package:amplify_core/amplify_core.dart';
+import 'package:meta/meta.dart';
 
 import '../graphql_response_decoder.dart';
 import 'web_socket_types.dart';
 
+@internal
 class WebSocketMessageStreamTransformer
     extends StreamTransformerBase<dynamic, WebSocketMessage> {
   const WebSocketMessageStreamTransformer();
@@ -33,13 +37,18 @@ class WebSocketMessageStreamTransformer
   }
 }
 
+@internal
 class WebSocketSubscriptionStreamTransformer<T>
     extends StreamTransformerBase<WebSocketMessage, GraphQLResponse<T>> {
   final GraphQLRequest<T> request;
+  final AmplifyLogger logger;
   final void Function()? onEstablished;
 
   const WebSocketSubscriptionStreamTransformer(
-      this.request, this.onEstablished);
+    this.request,
+    this.onEstablished, {
+    required this.logger,
+  });
 
   @override
   Stream<GraphQLResponse<T>> bind(Stream<WebSocketMessage> stream) async* {
@@ -52,15 +61,17 @@ class WebSocketSubscriptionStreamTransformer<T>
           final payload = event.payload as SubscriptionDataPayload;
           final errors = deserializeGraphQLResponseErrors(payload.toJson());
           yield GraphQLResponseDecoder.instance.decode<T>(
-              request: request,
-              data: json.encode(payload.data),
-              errors: errors);
+            request: request,
+            data: json.encode(payload.data),
+            errors: errors,
+          );
 
           break;
         case MessageType.error:
           final error = event.payload as WebSocketError;
           throw error;
         case MessageType.complete:
+          logger.info('Cancel succeeded for Operation: ${event.id}');
           return;
       }
     }
diff --git a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
index a18a63e582..8b62409dd2 100644
--- a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
+++ b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
@@ -38,7 +38,9 @@ class MockWebSocketConnection extends WebSocketConnection {
   /// inspected for testing.
   final List<WebSocketMessage> sentMessages = [];
 
-  MockWebSocketConnection(super.config, super.authProviderRepo);
+  MockWebSocketConnection(
+      AWSApiConfig config, AmplifyAuthProviderRepository authProviderRepo)
+      : super(config, authProviderRepo, logger: AmplifyLogger());
 
   WebSocketMessage? get lastSentMessage => sentMessages.lastOrNull;
 

From ac5348a21d43245012c7d6300bb33b5ee3fc92e3 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Thu, 11 Aug 2022 12:50:27 -0700
Subject: [PATCH 17/33] throw error during connection

---
 .../src/decorators/web_socket_auth_utils.dart | 11 +++-
 .../src/graphql/ws/web_socket_connection.dart | 63 ++++++++++---------
 2 files changed, 40 insertions(+), 34 deletions(-)

diff --git a/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
index fb19c1ce2b..9e56f26305 100644
--- a/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
+++ b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
@@ -21,6 +21,11 @@ import 'package:meta/meta.dart';
 import '../graphql/ws/web_socket_types.dart';
 import 'authorize_http_request.dart';
 
+// Constants for header values as noted in https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html.
+const _acceptHeaderValue = 'application/json, text/javascript';
+const _contentEncodingHeaderValue = 'amz-1.0';
+const _contentTypeHeaderValue = 'application/json; charset=UTF-8';
+
 /// Generate a URI for the connection and all subscriptions.
 ///
 /// See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection=
@@ -90,9 +95,9 @@ Future<Map<String, String>> _generateAuthorizationHeaders(
   final canonicalHttpRequest =
       http.Request('POST', Uri.parse('${config.endpoint}$maybeConnect'));
   canonicalHttpRequest.headers.addAll({
-    AWSHeaders.accept: 'application/json, text/javascript',
-    AWSHeaders.contentEncoding: 'amz-1.0',
-    AWSHeaders.contentType: 'application/json; charset=UTF-8',
+    AWSHeaders.accept: _acceptHeaderValue,
+    AWSHeaders.contentEncoding: _contentEncodingHeaderValue,
+    AWSHeaders.contentType: _contentTypeHeaderValue,
   });
   canonicalHttpRequest.body = body;
   final authorizedHttpRequest = await authorizeHttpRequest(
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
index 9a1f2354d3..4de3d9272c 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
@@ -28,7 +28,7 @@ import 'web_socket_types.dart';
 /// 1001, going away
 const _defaultCloseStatus = status.goingAway;
 
-/// {@template amplify_api.web_socket_connection}
+/// {@template amplify_api.ws.web_socket_connection}
 /// Manages connection with an AppSync backend and subscription routing.
 /// {@endtemplate}
 @internal
@@ -45,9 +45,9 @@ class WebSocketConnection implements Closeable {
   // Manages all incoming messages from server. Primarily handles messages related
   // to the entire connection. E.g. connection_ack, connection_error, ka, error.
   // Other events (for single subscriptions) rebroadcast to _rebroadcastController.
-  late final WebSocketChannel _channel;
-  late final StreamSubscription<WebSocketMessage> _subscription;
-  late final RestartableTimer _timeoutTimer;
+  WebSocketChannel? _channel;
+  StreamSubscription<WebSocketMessage>? _subscription;
+  RestartableTimer? _timeoutTimer;
 
   // Re-broadcasts incoming messages for child streams (single GraphQL subscriptions).
   // start_ack, data, error
@@ -55,17 +55,15 @@ class WebSocketConnection implements Closeable {
       StreamController<WebSocketMessage>.broadcast();
   Stream<WebSocketMessage> get _messageStream => _rebroadcastController.stream;
 
-  // TODO: Add connection error variable to throw in `init`.
+  // Manage initial connection state.
+  var _initMemo = AsyncMemoizer<void>();
+  Completer<void> _connectionReady = Completer<void>();
 
-  // Futures to manage initial connection state.
-  final _initMemo = AsyncMemoizer<void>();
-  final Completer<void> _connectionReady = Completer<void>();
-
-  /// Fires when the connection is ready to be listened to, i.e.
-  /// after the first `connection_ack` message.
+  /// Fires when the connection is ready to be listened to after the first
+  /// `connection_ack` message.
   Future<void> get ready => _connectionReady.future;
 
-  /// {@macro amplify_api.web_socket_connection}
+  /// {@macro amplify_api.ws.web_socket_connection}
   WebSocketConnection(this._config, this._authProviderRepo,
       {required AmplifyLogger logger})
       : _logger = logger;
@@ -76,7 +74,10 @@ class WebSocketConnection implements Closeable {
       Stream<dynamic> stream) {
     return stream
         .transform(const WebSocketMessageStreamTransformer())
-        .listen(_onData);
+        .listen(_onData, onError: (Object e) {
+      _connectionError(ApiException('Connection failed.',
+          underlyingException: e.toString()));
+    });
   }
 
   /// Connects WebSocket channel to _subscription stream but does not send connection
@@ -87,18 +88,26 @@ class WebSocketConnection implements Closeable {
       connectionUri,
       protocols: webSocketProtocols,
     );
-    _subscription = getStreamSubscription(_channel.stream);
+    _subscription = getStreamSubscription(_channel!.stream);
+  }
+
+  void _connectionError(ApiException exception) {
+    _connectionReady.completeError(_connectionError);
+    _channel?.sink.close();
+    // Reset connection init memo so it can be re-attempted.
+    _initMemo = AsyncMemoizer<void>();
+    _connectionReady = Completer<void>();
   }
 
-  /// Closes the WebSocket connection.
+  /// Closes the WebSocket connection and cleans up local variables.
   @override
   void close([int closeStatus = _defaultCloseStatus]) {
     final reason =
         closeStatus == _defaultCloseStatus ? 'client closed' : 'unknown';
-    _subscription.cancel();
-    _channel.sink.close(closeStatus, reason);
+    _subscription?.cancel();
+    _channel?.sink.close(closeStatus, reason);
     _rebroadcastController.close();
-    _timeoutTimer.cancel();
+    _timeoutTimer?.cancel();
   }
 
   /// Initializes the connection.
@@ -113,7 +122,6 @@ class WebSocketConnection implements Closeable {
         await generateConnectionUri(_config, _authProviderRepo);
     await connect(connectionUri);
 
-    if (_connectionReady.isCompleted) return;
     send(WebSocketConnectionInitMessage());
 
     return ready;
@@ -125,9 +133,7 @@ class WebSocketConnection implements Closeable {
     GraphQLRequest<T> request,
     void Function()? onEstablished,
   ) {
-    if (!_connectionReady.isCompleted) {
-      init();
-    }
+    init(); // no-op if already connected
 
     // Generate and send an authorized subscription registration message.
     final subscriptionId = uuid();
@@ -161,7 +167,7 @@ class WebSocketConnection implements Closeable {
   @visibleForTesting
   void send(WebSocketMessage message) {
     final msgJson = json.encode(message.toJson());
-    _channel.sink.add(msgJson);
+    _channel?.sink.add(msgJson);
   }
 
   /// Times out the connection (usually if a keep alive has not been received in time).
@@ -195,16 +201,11 @@ class WebSocketConnection implements Closeable {
         _logger.verbose('Connection established. Registered timer');
         return;
       case MessageType.connectionError:
-        final wsError = message.payload as WebSocketError?;
-        _connectionReady.completeError(
-          wsError ??
-              Exception(
-                'An unknown error occurred while connecting to the WebSocket',
-              ),
-        );
+        _connectionError(const ApiException(
+            'Error occurred while connecting to the websocket'));
         return;
       case MessageType.keepAlive:
-        _timeoutTimer.reset();
+        _timeoutTimer?.reset();
         _logger.verbose('Reset timer');
         return;
       case MessageType.error:

From 2bc90247b48d92695e5fd9178136daeed8241fb3 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Mon, 15 Aug 2022 10:44:37 -0700
Subject: [PATCH 18/33] expand unit tests

---
 .../amplify_api/lib/src/api_plugin_impl.dart  |  12 +-
 .../src/decorators/web_socket_auth_utils.dart |  18 ++-
 .../src/graphql/ws/web_socket_connection.dart |  40 ++++--
 ...web_socket_message_stream_transformer.dart |   4 +-
 .../lib/src/graphql/ws/web_socket_types.dart  |   6 +-
 .../amplify_api/test/dart_graphql_test.dart   |  73 +++++++++-
 packages/api/amplify_api/test/util.dart       | 115 +++++++++++++++
 .../test/ws/web_socket_auth_utils_test.dart   |  85 +++++++++++
 .../test/ws/web_socket_connection_test.dart   | 136 +++++-------------
 9 files changed, 355 insertions(+), 134 deletions(-)
 create mode 100644 packages/api/amplify_api/test/ws/web_socket_auth_utils_test.dart

diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index a6a574b5e2..66e0d0ca91 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -138,11 +138,13 @@ class AmplifyAPIDart extends AmplifyAPI {
       type: EndpointType.graphQL,
       apiName: apiName,
     );
-    return _webSocketConnectionPool[endpoint.name] ??=
-        WebSocketConnection(endpoint.config, _authProviderRepo,
-            logger: _logger.createChild(
-              'webSocketConnection${endpoint.name}',
-            ));
+    return _webSocketConnectionPool[endpoint.name] ??= WebSocketConnection(
+      endpoint.config,
+      _authProviderRepo,
+      logger: _logger.createChild(
+        'webSocketConnection${endpoint.name}',
+      ),
+    );
   }
 
   Uri _getGraphQLUri(String? apiName) {
diff --git a/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
index 9e56f26305..f685c3821f 100644
--- a/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
+++ b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
@@ -26,23 +26,29 @@ const _acceptHeaderValue = 'application/json, text/javascript';
 const _contentEncodingHeaderValue = 'amz-1.0';
 const _contentTypeHeaderValue = 'application/json; charset=UTF-8';
 
+// AppSync expects "{}" encoded in the URI as the payload during handshake.
+const _emptyBody = '{}';
+
 /// Generate a URI for the connection and all subscriptions.
 ///
 /// See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection=
 @internal
 Future<Uri> generateConnectionUri(
     AWSApiConfig config, AmplifyAuthProviderRepository authRepo) async {
-  const body = '{}';
-  final authorizationHeaders = await _generateAuthorizationHeaders(config,
-      authRepo: authRepo, body: body);
+  final authorizationHeaders = await _generateAuthorizationHeaders(
+    config,
+    authRepo: authRepo,
+    body: _emptyBody,
+  );
   final encodedAuthHeaders =
       base64.encode(json.encode(authorizationHeaders).codeUnits);
   final endpointUri = Uri.parse(
-      config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'));
+    config.endpoint.replaceFirst('appsync-api', 'appsync-realtime-api'),
+  );
   return Uri(scheme: 'wss', host: endpointUri.host, path: 'graphql')
       .replace(queryParameters: <String, String>{
     'header': encodedAuthHeaders,
-    'payload': base64.encode(utf8.encode(body)) // always payload of '{}'
+    'payload': base64.encode(utf8.encode(_emptyBody)),
   });
 }
 
@@ -91,7 +97,7 @@ Future<Map<String, String>> _generateAuthorizationHeaders(
   //
   // The canonical request URL is a little different depending on if connection_init
   // or start (subscription registration).
-  final maybeConnect = body != '{}' ? '' : '/connect';
+  final maybeConnect = body != _emptyBody ? '' : '/connect';
   final canonicalHttpRequest =
       http.Request('POST', Uri.parse('${config.endpoint}$maybeConnect'));
   canonicalHttpRequest.headers.addAll({
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
index 4de3d9272c..683a10afb7 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
@@ -94,7 +94,11 @@ class WebSocketConnection implements Closeable {
   void _connectionError(ApiException exception) {
     _connectionReady.completeError(_connectionError);
     _channel?.sink.close();
-    // Reset connection init memo so it can be re-attempted.
+    _resetConnectionInit();
+  }
+
+  // Reset connection init variables so it can be re-attempted.
+  void _resetConnectionInit() {
     _initMemo = AsyncMemoizer<void>();
     _connectionReady = Completer<void>();
   }
@@ -102,12 +106,15 @@ class WebSocketConnection implements Closeable {
   /// Closes the WebSocket connection and cleans up local variables.
   @override
   void close([int closeStatus = _defaultCloseStatus]) {
+    _logger.verbose('Closing web socket connection.');
     final reason =
         closeStatus == _defaultCloseStatus ? 'client closed' : 'unknown';
     _subscription?.cancel();
+    _channel?.sink.done.whenComplete(() => _channel = null);
     _channel?.sink.close(closeStatus, reason);
     _rebroadcastController.close();
     _timeoutTimer?.cancel();
+    _resetConnectionInit();
   }
 
   /// Initializes the connection.
@@ -133,16 +140,18 @@ class WebSocketConnection implements Closeable {
     GraphQLRequest<T> request,
     void Function()? onEstablished,
   ) {
-    init(); // no-op if already connected
-
-    // Generate and send an authorized subscription registration message.
     final subscriptionId = uuid();
-    generateSubscriptionRegistrationMessage(
-      _config,
-      id: subscriptionId,
-      authRepo: _authProviderRepo,
-      request: request,
-    ).then(send);
+
+    // init is no-op if already connected
+    init().then((_) {
+      // Generate and send an authorized subscription registration message.
+      generateSubscriptionRegistrationMessage(
+        _config,
+        id: subscriptionId,
+        authRepo: _authProviderRepo,
+        request: request,
+      ).then(send);
+    });
 
     // Filter incoming messages that have the subscription ID and return as new
     // stream with messages converted to GraphQLResponse<T>.
@@ -167,7 +176,11 @@ class WebSocketConnection implements Closeable {
   @visibleForTesting
   void send(WebSocketMessage message) {
     final msgJson = json.encode(message.toJson());
-    _channel?.sink.add(msgJson);
+    if (_channel == null) {
+      throw ApiException(
+          'Web socket not connected. Cannot send message $message');
+    }
+    _channel!.sink.add(msgJson);
   }
 
   /// Times out the connection (usually if a keep alive has not been received in time).
@@ -185,7 +198,7 @@ class WebSocketConnection implements Closeable {
   /// Here, handle connection-wide messages and pass subscription events to
   /// `_rebroadcastController`.
   void _onData(WebSocketMessage message) {
-    _logger.verbose('websocket received message: $message');
+    _logger.verbose('websocket received message: ${prettyPrintJson(message)}');
 
     switch (message.messageType) {
       case MessageType.connectionAck:
@@ -221,6 +234,7 @@ class WebSocketConnection implements Closeable {
     }
 
     // Re-broadcast other message types related to single subscriptions.
-    _rebroadcastController.add(message);
+
+    if (!_rebroadcastController.isClosed) _rebroadcastController.add(message);
   }
 }
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
index 72d9c7160a..f9837c7272 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
@@ -31,8 +31,8 @@ class WebSocketMessageStreamTransformer
 
   @override
   Stream<WebSocketMessage> bind(Stream<dynamic> stream) {
-    return stream.cast<String>().map<Map>((str) {
-      return json.decode(str) as Map;
+    return stream.cast<String>().map<Map<String, Object?>>((str) {
+      return json.decode(str) as Map<String, Object?>;
     }).map(WebSocketMessage.fromJson);
   }
 }
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
index 0a754122bd..961a433fe4 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
@@ -124,7 +124,7 @@ class SubscriptionDataPayload extends WebSocketMessagePayload {
   final Map<String, dynamic>? data;
   final Map<String, dynamic>? errors;
 
-  SubscriptionDataPayload(this.data, this.errors);
+  const SubscriptionDataPayload(this.data, this.errors);
 
   static SubscriptionDataPayload fromJson(Map json) {
     final data = json['data'] as Map?;
@@ -146,7 +146,7 @@ class SubscriptionDataPayload extends WebSocketMessagePayload {
 class WebSocketError extends WebSocketMessagePayload implements Exception {
   final List<Map> errors;
 
-  WebSocketError(this.errors);
+  const WebSocketError(this.errors);
 
   static WebSocketError fromJson(Map json) {
     final errors = json['errors'] as List?;
@@ -172,7 +172,7 @@ class WebSocketMessage {
     this.payload,
   }) : id = id ?? uuid();
 
-  WebSocketMessage._({
+  const WebSocketMessage._({
     this.id,
     required this.messageType,
     this.payload,
diff --git a/packages/api/amplify_api/test/dart_graphql_test.dart b/packages/api/amplify_api/test/dart_graphql_test.dart
index 4d9d8ec47f..f1d7919bef 100644
--- a/packages/api/amplify_api/test/dart_graphql_test.dart
+++ b/packages/api/amplify_api/test/dart_graphql_test.dart
@@ -12,10 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import 'dart:async';
 import 'dart:convert';
 
 import 'package:amplify_api/amplify_api.dart';
 import 'package:amplify_api/src/api_plugin_impl.dart';
+import 'package:amplify_api/src/graphql/ws/web_socket_connection.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:amplify_test/test_models/ModelProvider.dart';
 import 'package:collection/collection.dart';
@@ -24,6 +26,7 @@ import 'package:http/http.dart' as http;
 import 'package:http/testing.dart';
 
 import 'test_data/fake_amplify_configuration.dart';
+import 'util.dart';
 
 final _deepEquals = const DeepCollectionEquality().equals;
 
@@ -107,6 +110,10 @@ class MockAmplifyAPI extends AmplifyAPIDart {
         return http.Response(
             json.encode(_expectedQuerySuccessResponseBody), 200);
       });
+
+  @override
+  WebSocketConnection getWebSocketConnection({String? apiName}) =>
+      MockWebSocketConnection(testApiKeyConfig, getTestAuthProviderRepo());
 }
 
 void main() {
@@ -127,7 +134,10 @@ void main() {
             }
           }
         } ''';
-      final req = GraphQLRequest(document: graphQLDocument, variables: {});
+      final req = GraphQLRequest<String>(
+        document: graphQLDocument,
+        variables: {},
+      );
 
       final operation = Amplify.API.query(request: req);
       final res = await operation.value;
@@ -147,8 +157,10 @@ void main() {
           }
         } ''';
       final graphQLVariables = {'name': 'Test Blog 1'};
-      final req = GraphQLRequest(
-          document: graphQLDocument, variables: graphQLVariables);
+      final req = GraphQLRequest<String>(
+        document: graphQLDocument,
+        variables: graphQLVariables,
+      );
 
       final operation = Amplify.API.mutate(request: req);
       final res = await operation.value;
@@ -158,6 +170,33 @@ void main() {
       expect(res.data, equals(expected));
       expect(res.errors, equals(null));
     });
+
+    test('subscribe() should return a subscription stream', () async {
+      Completer<void> establishedCompleter = Completer();
+      Completer<String> dataCompleter = Completer();
+      const graphQLDocument = '''subscription MySubscription {
+        onCreateBlog {
+          id
+          name
+          createdAt
+        }
+      }''';
+      final subscriptionRequest =
+          GraphQLRequest<String>(document: graphQLDocument);
+      final subscription = Amplify.API.subscribe(
+        subscriptionRequest,
+        onEstablished: () => establishedCompleter.complete(),
+      );
+
+      final streamSub = subscription.listen(
+        (event) => dataCompleter.complete(event.data),
+      );
+      await expectLater(establishedCompleter.future, completes);
+
+      final subscriptionData = await dataCompleter.future;
+      expect(subscriptionData, json.encode(mockSubscriptionData));
+      streamSub.cancel();
+    });
   });
   group('Model Helpers', () {
     const blogSelectionSet =
@@ -184,12 +223,34 @@ void main() {
       expect(res.data?.id, _modelQueryId);
       expect(res.errors, equals(null));
     });
+
+    test('subscribe() should decode model data', () async {
+      Completer<void> establishedCompleter = Completer();
+      Completer<Post> dataCompleter = Completer();
+      final subscriptionRequest = ModelSubscriptions.onCreate(Post.classType);
+      final subscription = Amplify.API.subscribe(
+        subscriptionRequest,
+        onEstablished: () => establishedCompleter.complete(),
+      );
+
+      final streamSub = subscription.listen(
+        (event) => dataCompleter.complete(event.data),
+      );
+      await expectLater(establishedCompleter.future, completes);
+
+      final subscriptionData = await dataCompleter.future;
+      expect(subscriptionData, isA<Post>());
+      streamSub.cancel();
+    });
   });
 
   group('Error Handling', () {
     test('response errors are decoded', () async {
       String graphQLDocument = ''' TestError ''';
-      final req = GraphQLRequest(document: graphQLDocument, variables: {});
+      final req = GraphQLRequest<String>(
+        document: graphQLDocument,
+        variables: {},
+      );
 
       final operation = Amplify.API.query(request: req);
       final res = await operation.value;
@@ -209,7 +270,7 @@ void main() {
     });
 
     test('canceled query request should never resolve', () async {
-      final req = GraphQLRequest(document: '', variables: {});
+      final req = GraphQLRequest<String>(document: '', variables: {});
       final operation = Amplify.API.query(request: req);
       operation.cancel();
       operation.then((p0) => fail('Request should have been cancelled.'));
@@ -218,7 +279,7 @@ void main() {
     });
 
     test('canceled mutation request should never resolve', () async {
-      final req = GraphQLRequest(document: '', variables: {});
+      final req = GraphQLRequest<String>(document: '', variables: {});
       final operation = Amplify.API.mutate(request: req);
       operation.cancel();
       operation.then((p0) => fail('Request should have been cancelled.'));
diff --git a/packages/api/amplify_api/test/util.dart b/packages/api/amplify_api/test/util.dart
index cd06f8c13c..7da7c56c1b 100644
--- a/packages/api/amplify_api/test/util.dart
+++ b/packages/api/amplify_api/test/util.dart
@@ -12,8 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
+import 'package:amplify_api/src/graphql/ws/web_socket_connection.dart';
+import 'package:amplify_api/src/graphql/ws/web_socket_types.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:aws_signature_v4/aws_signature_v4.dart';
+import 'package:collection/collection.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:http/http.dart' as http;
 
@@ -60,3 +67,111 @@ void validateSignedRequest(http.BaseRequest request) {
     contains('aws-sigv4'),
   );
 }
+
+const testApiKeyConfig = AWSApiConfig(
+  endpointType: EndpointType.graphQL,
+  endpoint: 'https://abc123.appsync-api.us-east-1.amazonaws.com/graphql',
+  region: 'us-east-1',
+  authorizationType: APIAuthorizationType.apiKey,
+  apiKey: 'abc-123',
+);
+
+const expectedApiKeyWebSocketConnectionUrl =
+    'wss://abc123.appsync-realtime-api.us-east-1.amazonaws.com/graphql?header=eyJDb250ZW50LVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PVVURi04IiwiWC1BcGktS2V5IjoiYWJjLTEyMyIsIkFjY2VwdCI6ImFwcGxpY2F0aW9uL2pzb24sIHRleHQvamF2YXNjcmlwdCIsIkNvbnRlbnQtRW5jb2RpbmciOiJhbXotMS4wIiwiSG9zdCI6ImFiYzEyMy5hcHBzeW5jLWFwaS51cy1lYXN0LTEuYW1hem9uYXdzLmNvbSJ9&payload=e30%3D';
+
+AmplifyAuthProviderRepository getTestAuthProviderRepo() {
+  final testAuthProviderRepo = AmplifyAuthProviderRepository();
+  testAuthProviderRepo.registerAuthProvider(
+    APIAuthorizationType.apiKey.authProviderToken,
+    AppSyncApiKeyAuthProvider(),
+  );
+
+  return testAuthProviderRepo;
+}
+
+const mockSubscriptionData = {
+  'onCreatePost': {
+    'id': '49d54440-cb80-4f20-964b-91c142761e82',
+    'title':
+        'Integration Test post - subscription create aa779f0d-0c92-4677-af32-e43f71b3eb55',
+    'rating': 0,
+    'created': null,
+    'createdAt': '2022-08-15T18:22:15.410Z',
+    'updatedAt': '2022-08-15T18:22:15.410Z',
+    'blog': {
+      'id': '164bd1f1-544c-40cb-a656-a7563b046e71',
+      'name': 'Integration Test Blog with a post - create',
+      'createdAt': '2022-08-15T18:22:15.164Z',
+      'file': null,
+      'files': null,
+      'updatedAt': '2022-08-15T18:22:15.164Z'
+    }
+  }
+};
+
+/// Extension of [WebSocketConnection] that stores messages internally instead
+/// of sending them.
+class MockWebSocketConnection extends WebSocketConnection {
+  /// Instead of actually connecting, just set the URI here so it can be inspected
+  /// for testing.
+  Uri? connectedUri;
+
+  /// Instead of sending messages, they are pushed to end of list so they can be
+  /// inspected for testing.
+  final List<WebSocketMessage> sentMessages = [];
+
+  MockWebSocketConnection(
+      AWSApiConfig config, AmplifyAuthProviderRepository authProviderRepo)
+      : super(config, authProviderRepo, logger: AmplifyLogger());
+
+  WebSocketMessage? get lastSentMessage => sentMessages.lastOrNull;
+
+  final messageStream = StreamController<dynamic>();
+
+  @override
+  Future<void> connect(Uri connectionUri) async {
+    connectedUri = connectionUri;
+
+    // mock some message responses (acks and mock data) from server
+    final broadcast = messageStream.stream.asBroadcastStream();
+    broadcast.listen((event) {
+      final eventJson = json.decode(event as String);
+      final messageFromEvent = WebSocketMessage.fromJson(eventJson as Map);
+
+      // connection_init, respond with connection_ack
+      final mockResponseMessages = <WebSocketMessage>[];
+      if (messageFromEvent.messageType == MessageType.connectionInit) {
+        mockResponseMessages.add(WebSocketMessage(
+          messageType: MessageType.connectionAck,
+          payload: const ConnectionAckMessagePayload(10000),
+        ));
+        // start, respond with start_ack and mock data
+      } else if (messageFromEvent.messageType == MessageType.start) {
+        mockResponseMessages.add(WebSocketMessage(
+          messageType: MessageType.startAck,
+          id: messageFromEvent.id,
+        ));
+        mockResponseMessages.add(WebSocketMessage(
+          messageType: MessageType.data,
+          id: messageFromEvent.id,
+          payload: const SubscriptionDataPayload(mockSubscriptionData, null),
+        ));
+      }
+
+      for (var mockMessage in mockResponseMessages) {
+        messageStream.add(json.encode(mockMessage));
+      }
+    });
+
+    // ensures connected to _onDone events in parent class
+    getStreamSubscription(broadcast);
+  }
+
+  /// Pushes message in sentMessages and adds to stream (to support mocking result).
+  @override
+  void send(WebSocketMessage message) {
+    sentMessages.add(message);
+    final messageStr = json.encode(message.toJson());
+    messageStream.add(messageStr);
+  }
+}
diff --git a/packages/api/amplify_api/test/ws/web_socket_auth_utils_test.dart b/packages/api/amplify_api/test/ws/web_socket_auth_utils_test.dart
new file mode 100644
index 0000000000..19cb61a647
--- /dev/null
+++ b/packages/api/amplify_api/test/ws/web_socket_auth_utils_test.dart
@@ -0,0 +1,85 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:amplify_api/src/decorators/web_socket_auth_utils.dart';
+import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
+import 'package:amplify_api/src/graphql/ws/web_socket_types.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import '../util.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  final authProviderRepo = AmplifyAuthProviderRepository();
+  authProviderRepo.registerAuthProvider(
+      APIAuthorizationType.apiKey.authProviderToken,
+      AppSyncApiKeyAuthProvider());
+
+  const graphQLDocument = '''subscription MySubscription {
+    onCreateBlog {
+      id
+      name
+      createdAt
+    }
+  }''';
+  final subscriptionRequest = GraphQLRequest<String>(document: graphQLDocument);
+
+  void _assertBasicSubscriptionPayloadHeaders(
+      SubscriptionRegistrationPayload payload) {
+    expect(
+      payload.authorizationHeaders[AWSHeaders.contentType],
+      'application/json; charset=UTF-8',
+    );
+    expect(
+      payload.authorizationHeaders[AWSHeaders.accept],
+      'application/json, text/javascript',
+    );
+    expect(
+      payload.authorizationHeaders[AWSHeaders.host],
+      'abc123.appsync-api.us-east-1.amazonaws.com',
+    );
+  }
+
+  group('generateConnectionUri', () {
+    test('should generate authorized connection URI', () async {
+      final actualConnectionUri =
+          await generateConnectionUri(testApiKeyConfig, authProviderRepo);
+      expect(
+        actualConnectionUri.toString(),
+        expectedApiKeyWebSocketConnectionUrl,
+      );
+    });
+  });
+
+  group('generateSubscriptionRegistrationMessage', () {
+    test('should generate an authorized message', () async {
+      final authorizedMessage = await generateSubscriptionRegistrationMessage(
+        testApiKeyConfig,
+        id: 'abc123',
+        authRepo: authProviderRepo,
+        request: subscriptionRequest,
+      );
+      final payload =
+          authorizedMessage.payload as SubscriptionRegistrationPayload;
+
+      _assertBasicSubscriptionPayloadHeaders(payload);
+      expect(
+        payload.authorizationHeaders[xApiKey],
+        testApiKeyConfig.apiKey,
+      );
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
index 8b62409dd2..81e0d87d6e 100644
--- a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
+++ b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
@@ -15,101 +15,16 @@
 import 'dart:async';
 import 'dart:convert';
 
-import 'package:amplify_api/src/api_plugin_impl.dart';
 import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
 import 'package:amplify_api/src/graphql/ws/web_socket_types.dart';
 import 'package:amplify_core/amplify_core.dart';
-import 'package:collection/collection.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:web_socket_channel/web_socket_channel.dart';
 
 import '../util.dart';
 
-import 'package:amplify_api/src/graphql/ws/web_socket_connection.dart';
-
-/// Extension of [WebSocketConnection] that stores messages internally instead
-/// of sending them.
-class MockWebSocketConnection extends WebSocketConnection {
-  /// Instead of actually connecting, just set the URI here so it can be inspected
-  /// for testing.
-  Uri? connectedUri;
-
-  /// Instead of sending messages, they are pushed to end of list so they can be
-  /// inspected for testing.
-  final List<WebSocketMessage> sentMessages = [];
-
-  MockWebSocketConnection(
-      AWSApiConfig config, AmplifyAuthProviderRepository authProviderRepo)
-      : super(config, authProviderRepo, logger: AmplifyLogger());
-
-  WebSocketMessage? get lastSentMessage => sentMessages.lastOrNull;
-
-  final messageStream = StreamController<dynamic>();
-
-  @override
-  Future<void> connect(Uri connectionUri) async {
-    connectedUri = connectionUri;
-
-    // mock some message responses (acks) from server
-    final broadcast = messageStream.stream.asBroadcastStream();
-    broadcast.listen((event) {
-      final eventJson = json.decode(event as String);
-      final messageFromEvent = WebSocketMessage.fromJson(eventJson as Map);
-
-      // connection_init, respond with connection_ack
-      WebSocketMessage? mockAckMsg;
-      if (messageFromEvent.messageType == MessageType.connectionInit) {
-        mockAckMsg = WebSocketMessage(
-          messageType: MessageType.connectionAck,
-          payload: const ConnectionAckMessagePayload(10000),
-        );
-        // start, respond with start_ack
-      } else if (messageFromEvent.messageType == MessageType.start) {
-        mockAckMsg = WebSocketMessage(
-          messageType: MessageType.startAck,
-          id: messageFromEvent.id,
-        );
-      }
-      if (mockAckMsg != null) {
-        final messageStr = json.encode(mockAckMsg);
-        messageStream.add(messageStr);
-      }
-    });
-
-    // ensures connected to _onDone events in parent class
-    getStreamSubscription(broadcast);
-  }
-
-  /// Pushes message in sentMessages and adds to stream (to support mocking result).
-  @override
-  void send(WebSocketMessage message) {
-    sentMessages.add(message);
-    final messageStr = json.encode(message.toJson());
-    messageStream.add(messageStr);
-  }
-}
-
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
 
-  final authProviderRepo = AmplifyAuthProviderRepository();
-  authProviderRepo.registerAuthProvider(
-      APIAuthorizationType.apiKey.authProviderToken,
-      AppSyncApiKeyAuthProvider());
-
-  const endpointType = EndpointType.graphQL;
-  const endpoint = 'https://abc123.appsync-api.us-east-1.amazonaws.com/graphql';
-  const region = 'us-east-1';
-  const authorizationType = APIAuthorizationType.apiKey;
-  const apiKey = 'abc-123';
-
-  const config = AWSApiConfig(
-      endpointType: endpointType,
-      endpoint: endpoint,
-      region: region,
-      authorizationType: authorizationType,
-      apiKey: apiKey);
-
   late MockWebSocketConnection connection;
 
   const graphQLDocument = '''subscription MySubscription {
@@ -122,7 +37,10 @@ void main() {
   final subscriptionRequest = GraphQLRequest<String>(document: graphQLDocument);
 
   setUp(() {
-    connection = MockWebSocketConnection(config, authProviderRepo);
+    connection = MockWebSocketConnection(
+      testApiKeyConfig,
+      getTestAuthProviderRepo(),
+    );
   });
 
   group('WebSocketConnection', () {
@@ -131,9 +49,10 @@ void main() {
         () async {
       await connection.init();
       expectLater(connection.ready, completes);
-      const expectedConnectionUri =
-          'wss://abc123.appsync-realtime-api.us-east-1.amazonaws.com/graphql?header=eyJDb250ZW50LVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uOyBjaGFyc2V0PVVURi04IiwiWC1BcGktS2V5IjoiYWJjLTEyMyIsIkFjY2VwdCI6ImFwcGxpY2F0aW9uL2pzb24sIHRleHQvamF2YXNjcmlwdCIsIkNvbnRlbnQtRW5jb2RpbmciOiJhbXotMS4wIiwiSG9zdCI6ImFiYzEyMy5hcHBzeW5jLWFwaS51cy1lYXN0LTEuYW1hem9uYXdzLmNvbSJ9&payload=e30%3D';
-      expect(connection.connectedUri.toString(), expectedConnectionUri);
+      expect(
+        connection.connectedUri.toString(),
+        expectedApiKeyWebSocketConnectionUrl,
+      );
       expect(
           connection.lastSentMessage?.messageType, MessageType.connectionInit);
     });
@@ -165,17 +84,36 @@ void main() {
       final payloadJson = lastMessage?.payload?.toJson();
       final apiKeyFromPayload =
           payloadJson?['extensions']['authorization'][xApiKey];
-      expect(apiKeyFromPayload, apiKey);
+      expect(apiKeyFromPayload, testApiKeyConfig.apiKey);
     });
 
-    // test('subscribe() should return a subscription stream', () async {
-    //   connection.init();
-    //   await connection.ready;
-    //   Completer<void> establishedCompleter = Completer();
-    //   final subscription = connection.subscribe(subscriptionRequest, () {
-    //     establishedCompleter.complete();
-    //   }).listen((event) {});
-    //   await establishedCompleter.future;
-    // });
+    test('subscribe() should return a subscription stream', () async {
+      Completer<void> establishedCompleter = Completer();
+      Completer<String> dataCompleter = Completer();
+      final subscription = connection.subscribe(
+        subscriptionRequest,
+        () => establishedCompleter.complete(),
+      );
+
+      final streamSub = subscription.listen(
+        (event) => dataCompleter.complete(event.data),
+      );
+      await expectLater(establishedCompleter.future, completes);
+
+      final subscriptionData = await dataCompleter.future;
+      expect(subscriptionData, json.encode(mockSubscriptionData));
+      streamSub.cancel();
+    });
+
+    test('cancel() should send a stop message', () async {
+      Completer<void> establishedCompleter = Completer();
+      final subscription = connection.subscribe(subscriptionRequest, () {
+        establishedCompleter.complete();
+      });
+      final streamSub = subscription.listen((event) {});
+      await establishedCompleter.future;
+      streamSub.cancel();
+      expect(connection.lastSentMessage?.messageType, MessageType.stop);
+    });
   });
 }

From bdf706fffbe1fe8c96ecbe3f7b36397265ae7cc7 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Wed, 15 Jun 2022 11:35:34 -0800
Subject: [PATCH 19/33] chore!(api): migrate API category type definitions
 (#1640)

---
 .../src/category/amplify_api_category.dart    | 134 +++++----
 .../lib/src/category/amplify_categories.dart  |   1 +
 .../plugin/amplify_api_plugin_interface.dart  |  74 +++--
 .../lib/src/types/api/api_types.dart          |   2 +-
 .../types/api/exceptions/api_exception.dart   |   9 -
 .../types/api/graphql/graphql_operation.dart  |  15 +-
 .../lib/src/types/api/rest/http_payload.dart  |  82 ++++++
 .../src/types/api/rest/rest_exception.dart    |  14 +-
 .../src/types/api/rest/rest_operation.dart    |  20 +-
 .../lib/src/types/api/rest/rest_response.dart |  59 ----
 packages/api/amplify_api/.gitignore           |  40 ++-
 .../example/lib/graphql_api_view.dart         |   3 +-
 .../api/amplify_api/example/lib/main.dart     |   2 +-
 .../example/lib/rest_api_view.dart            |  65 ++---
 packages/api/amplify_api/example/pubspec.yaml |   1 +
 .../lib/src/method_channel_api.dart           | 229 +++++++++++----
 packages/api/amplify_api/pubspec.yaml         |   3 +-
 .../test/amplify_rest_api_methods_test.dart   | 270 ++++++++----------
 .../example/lib/main.dart                     |  13 +-
 19 files changed, 611 insertions(+), 425 deletions(-)
 create mode 100644 packages/amplify_core/lib/src/types/api/rest/http_payload.dart
 delete mode 100644 packages/amplify_core/lib/src/types/api/rest/rest_response.dart

diff --git a/packages/amplify_core/lib/src/category/amplify_api_category.dart b/packages/amplify_core/lib/src/category/amplify_api_category.dart
index 99406d31fc..7d9692a725 100644
--- a/packages/amplify_core/lib/src/category/amplify_api_category.dart
+++ b/packages/amplify_core/lib/src/category/amplify_api_category.dart
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License").
  * You may not use this file except in compliance with the License.
@@ -21,17 +21,13 @@ class APICategory extends AmplifyCategory<APIPluginInterface> {
   Category get category => Category.api;
 
   // ====== GraphQL =======
-  GraphQLOperation<T> query<T>({required GraphQLRequest<T> request}) {
-    return plugins.length == 1
-        ? plugins[0].query(request: request)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<GraphQLResponse<T>> query<T>(
+          {required GraphQLRequest<T> request}) =>
+      defaultPlugin.query(request: request);
 
-  GraphQLOperation<T> mutate<T>({required GraphQLRequest<T> request}) {
-    return plugins.length == 1
-        ? plugins[0].mutate(request: request)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<GraphQLResponse<T>> mutate<T>(
+          {required GraphQLRequest<T> request}) =>
+      defaultPlugin.mutate(request: request);
 
   /// Subscribes to the given [request] and returns the stream of response events.
   /// An optional [onEstablished] callback can be used to be alerted when the
@@ -42,52 +38,88 @@ class APICategory extends AmplifyCategory<APIPluginInterface> {
   Stream<GraphQLResponse<T>> subscribe<T>(
     GraphQLRequest<T> request, {
     void Function()? onEstablished,
-  }) {
-    return plugins.length == 1
-        ? plugins[0].subscribe(request, onEstablished: onEstablished)
-        : throw _pluginNotAddedException('Api');
-  }
+  }) =>
+      defaultPlugin.subscribe(request, onEstablished: onEstablished);
 
   // ====== RestAPI ======
-  void cancelRequest(String cancelToken) {
-    return plugins.length == 1
-        ? plugins[0].cancelRequest(cancelToken)
-        : throw _pluginNotAddedException('Api');
-  }
 
-  RestOperation get({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].get(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> delete(
+    String path, {
+    Map<String, String>? headers,
+    HttpPayload? body,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.delete(
+        path,
+        headers: headers,
+        body: body,
+        apiName: apiName,
+      );
 
-  RestOperation put({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].put(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> get(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.get(
+        path,
+        headers: headers,
+        apiName: apiName,
+      );
 
-  RestOperation post({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].post(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> head(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.head(
+        path,
+        headers: headers,
+        apiName: apiName,
+      );
 
-  RestOperation delete({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].delete(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> patch(
+    String path, {
+    Map<String, String>? headers,
+    HttpPayload? body,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.patch(
+        path,
+        headers: headers,
+        body: body,
+        apiName: apiName,
+      );
 
-  RestOperation head({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].head(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> post(
+    String path, {
+    Map<String, String>? headers,
+    HttpPayload? body,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.post(
+        path,
+        headers: headers,
+        body: body,
+        apiName: apiName,
+      );
 
-  RestOperation patch({required RestOptions restOptions}) {
-    return plugins.length == 1
-        ? plugins[0].patch(restOptions: restOptions)
-        : throw _pluginNotAddedException('Api');
-  }
+  CancelableOperation<AWSStreamedHttpResponse> put(
+    String path, {
+    Map<String, String>? headers,
+    HttpPayload? body,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) =>
+      defaultPlugin.put(
+        path,
+        headers: headers,
+        body: body,
+        apiName: apiName,
+      );
 }
diff --git a/packages/amplify_core/lib/src/category/amplify_categories.dart b/packages/amplify_core/lib/src/category/amplify_categories.dart
index 969ea3ebc7..4c014d05dc 100644
--- a/packages/amplify_core/lib/src/category/amplify_categories.dart
+++ b/packages/amplify_core/lib/src/category/amplify_categories.dart
@@ -18,6 +18,7 @@ library amplify_interface;
 import 'dart:async';
 
 import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
 import 'package:collection/collection.dart';
 import 'package:meta/meta.dart';
 
diff --git a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart
index d318db6e13..5169acb091 100644
--- a/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart
+++ b/packages/amplify_core/lib/src/plugin/amplify_api_plugin_interface.dart
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License").
  * You may not use this file except in compliance with the License.
@@ -14,6 +14,7 @@
  */
 
 import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
 import 'package:meta/meta.dart';
 
 abstract class APIPluginInterface extends AmplifyPluginInterface {
@@ -25,11 +26,13 @@ abstract class APIPluginInterface extends AmplifyPluginInterface {
   ModelProviderInterface? get modelProvider => throw UnimplementedError();
 
   // ====== GraphQL =======
-  GraphQLOperation<T> query<T>({required GraphQLRequest<T> request}) {
+  CancelableOperation<GraphQLResponse<T>> query<T>(
+      {required GraphQLRequest<T> request}) {
     throw UnimplementedError('query() has not been implemented.');
   }
 
-  GraphQLOperation<T> mutate<T>({required GraphQLRequest<T> request}) {
+  CancelableOperation<GraphQLResponse<T>> mutate<T>(
+      {required GraphQLRequest<T> request}) {
     throw UnimplementedError('mutate() has not been implemented.');
   }
 
@@ -50,31 +53,64 @@ abstract class APIPluginInterface extends AmplifyPluginInterface {
   void registerAuthProvider(APIAuthProvider authProvider);
 
   // ====== RestAPI ======
-  void cancelRequest(String cancelToken) {
-    throw UnimplementedError('cancelRequest has not been implemented.');
-  }
-
-  RestOperation get({required RestOptions restOptions}) {
-    throw UnimplementedError('get has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> delete(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('delete() has not been implemented');
   }
 
-  RestOperation put({required RestOptions restOptions}) {
-    throw UnimplementedError('put has not been implemented.');
+  /// Uses Amplify configuration to authorize request to [path] and returns
+  /// [CancelableOperation] which resolves to standard HTTP
+  /// [Response](https://pub.dev/documentation/http/latest/http/Response-class.html).
+  CancelableOperation<AWSStreamedHttpResponse> get(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('get() has not been implemented');
   }
 
-  RestOperation post({required RestOptions restOptions}) {
-    throw UnimplementedError('post has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> head(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('head() has not been implemented');
   }
 
-  RestOperation delete({required RestOptions restOptions}) {
-    throw UnimplementedError('delete has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> patch(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('patch() has not been implemented');
   }
 
-  RestOperation head({required RestOptions restOptions}) {
-    throw UnimplementedError('head has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> post(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('post() has not been implemented');
   }
 
-  RestOperation patch({required RestOptions restOptions}) {
-    throw UnimplementedError('patch has not been implemented.');
+  CancelableOperation<AWSStreamedHttpResponse> put(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    throw UnimplementedError('put() has not been implemented');
   }
 }
diff --git a/packages/amplify_core/lib/src/types/api/api_types.dart b/packages/amplify_core/lib/src/types/api/api_types.dart
index 3e69a1dc4b..299fd03412 100644
--- a/packages/amplify_core/lib/src/types/api/api_types.dart
+++ b/packages/amplify_core/lib/src/types/api/api_types.dart
@@ -27,10 +27,10 @@ export 'graphql/graphql_response.dart';
 export 'graphql/graphql_response_error.dart';
 export 'graphql/graphql_subscription_operation.dart';
 
+export 'rest/http_payload.dart';
 export 'rest/rest_exception.dart';
 export 'rest/rest_operation.dart';
 export 'rest/rest_options.dart';
-export 'rest/rest_response.dart';
 
 export 'types/pagination/paginated_model_type.dart';
 export 'types/pagination/paginated_result.dart';
diff --git a/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart b/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart
index 9f9d833110..2ec1bf37ac 100644
--- a/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart
+++ b/packages/amplify_core/lib/src/types/api/exceptions/api_exception.dart
@@ -19,18 +19,11 @@ import 'package:amplify_core/amplify_core.dart';
 /// Exception thrown from the API Category.
 /// {@endtemplate}
 class ApiException extends AmplifyException {
-  /// HTTP status of response, only available if error
-  @Deprecated(
-      'Use RestException instead to retrieve the HTTP response. Existing uses of '
-      'ApiException for handling REST errors can be safely replaced with RestException')
-  final int? httpStatusCode;
-
   /// {@macro api_exception}
   const ApiException(
     String message, {
     String? recoverySuggestion,
     String? underlyingException,
-    this.httpStatusCode,
   }) : super(
           message,
           recoverySuggestion: recoverySuggestion,
@@ -40,7 +33,6 @@ class ApiException extends AmplifyException {
   /// Constructor for down casting an AmplifyException to this exception
   ApiException._private(
     AmplifyException exception,
-    this.httpStatusCode,
   ) : super(
           exception.message,
           recoverySuggestion: exception.recoverySuggestion,
@@ -57,7 +49,6 @@ class ApiException extends AmplifyException {
     }
     return ApiException._private(
       AmplifyException.fromMap(serializedException),
-      statusCode,
     );
   }
 }
diff --git a/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart b/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart
index 94035a8997..b9f72dbd37 100644
--- a/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart
+++ b/packages/amplify_core/lib/src/types/api/graphql/graphql_operation.dart
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License").
  * You may not use this file except in compliance with the License.
@@ -13,11 +13,14 @@
  * permissions and limitations under the License.
  */
 
-import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
 
-class GraphQLOperation<T> {
-  final Function cancel;
-  final Future<GraphQLResponse<T>> response;
+import 'graphql_response.dart';
 
-  const GraphQLOperation({required this.response, required this.cancel});
+/// Allows callers to synchronously get the unstreamed response with decoded body.
+extension GraphQLOperation<T> on CancelableOperation<GraphQLResponse<T>> {
+  @Deprecated('use .value instead')
+  Future<GraphQLResponse<T>> get response {
+    return value;
+  }
 }
diff --git a/packages/amplify_core/lib/src/types/api/rest/http_payload.dart b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart
new file mode 100644
index 0000000000..eb657d7543
--- /dev/null
+++ b/packages/amplify_core/lib/src/types/api/rest/http_payload.dart
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:async/async.dart';
+
+/// {@template amplify_core.http_payload}
+/// An HTTP request's payload.
+/// {@endtemplate}
+class HttpPayload extends StreamView<List<int>> {
+  String contentType = 'text/plain';
+
+  /// {@macro amplify_core.http_payload}
+  factory HttpPayload([Object? body]) {
+    if (body == null) {
+      return HttpPayload.empty();
+    }
+    if (body is String) {
+      return HttpPayload.string(body);
+    }
+    if (body is List<int>) {
+      return HttpPayload.bytes(body);
+    }
+    if (body is Stream<List<int>>) {
+      return HttpPayload.streaming(body);
+    }
+    if (body is Map<String, String>) {
+      return HttpPayload.formFields(body);
+    }
+    throw ArgumentError('Invalid HTTP payload type: ${body.runtimeType}');
+  }
+
+  /// An empty HTTP body.
+  HttpPayload.empty() : super(const Stream.empty());
+
+  /// A UTF-8 encoded HTTP body.
+  HttpPayload.string(String body, {Encoding encoding = utf8})
+      : super(LazyStream(() => Stream.value(encoding.encode(body))));
+
+  /// A byte buffer HTTP body.
+  HttpPayload.bytes(List<int> body) : super(Stream.value(body));
+
+  /// A form-encoded body of `key=value` pairs.
+  HttpPayload.formFields(Map<String, String> body, {Encoding encoding = utf8})
+      : contentType = 'application/x-www-form-urlencoded',
+        super(LazyStream(() => Stream.value(
+            encoding.encode(_mapToQuery(body, encoding: encoding)))));
+
+  /// Encodes body as a JSON string and sets Content-Type to 'application/json'
+  HttpPayload.json(Object body, {Encoding encoding = utf8})
+      : contentType = 'application/json',
+        super(
+            LazyStream(() => Stream.value(encoding.encode(json.encode(body)))));
+
+  /// A streaming HTTP body.
+  HttpPayload.streaming(Stream<List<int>> body) : super(body);
+}
+
+/// Converts a [Map] from parameter names to values to a URL query string.
+///
+///     _mapToQuery({"foo": "bar", "baz": "bang"});
+///     //=> "foo=bar&baz=bang"
+///
+/// Similar util at https://github.com/dart-lang/http/blob/06649afbb5847dbb0293816ba8348766b116e419/pkgs/http/lib/src/utils.dart#L15
+String _mapToQuery(Map<String, String> map, {required Encoding encoding}) => map
+    .entries
+    .map((e) =>
+        '${Uri.encodeQueryComponent(e.key, encoding: encoding)}=${Uri.encodeQueryComponent(e.value, encoding: encoding)}')
+    .join('&');
diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart
index fe6a6a8ee5..1f6dc18c2e 100644
--- a/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart
+++ b/packages/amplify_core/lib/src/types/api/rest/rest_exception.dart
@@ -19,16 +19,8 @@ import 'package:amplify_core/amplify_core.dart';
 /// An HTTP error encountered during a REST API call, i.e. for calls returning
 /// non-2xx status codes.
 /// {@endtemplate}
-class RestException extends ApiException {
-  /// The HTTP response from the server.
-  final RestResponse response;
-
+@Deprecated('BREAKING CHANGE: No longer thrown for non-200 responses.')
+abstract class RestException extends ApiException {
   /// {@macro rest_exception}
-  RestException(this.response)
-      : super(response.body, httpStatusCode: response.statusCode);
-
-  @override
-  String toString() {
-    return 'RestException{response=$response}';
-  }
+  const RestException() : super('REST exception.');
 }
diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart
index eb84a0ea42..a24ad39ad2 100644
--- a/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart
+++ b/packages/amplify_core/lib/src/types/api/rest/rest_operation.dart
@@ -1,5 +1,5 @@
 /*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License").
  * You may not use this file except in compliance with the License.
@@ -13,11 +13,17 @@
  * permissions and limitations under the License.
  */
 
-import 'rest_response.dart';
+import 'package:async/async.dart';
+import 'package:aws_common/aws_common.dart';
 
-class RestOperation {
-  final Function cancel;
-  final Future<RestResponse> response;
-
-  const RestOperation({required this.response, required this.cancel});
+/// Allows callers to synchronously get unstreamed response with the decoded body.
+extension RestOperation on CancelableOperation<AWSStreamedHttpResponse> {
+  Future<AWSHttpResponse> get response async {
+    final value = await this.value;
+    return AWSHttpResponse(
+      body: await value.bodyBytes,
+      statusCode: value.statusCode,
+      headers: value.headers,
+    );
+  }
 }
diff --git a/packages/amplify_core/lib/src/types/api/rest/rest_response.dart b/packages/amplify_core/lib/src/types/api/rest/rest_response.dart
deleted file mode 100644
index f93a2079e4..0000000000
--- a/packages/amplify_core/lib/src/types/api/rest/rest_response.dart
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License").
- * You may not use this file except in compliance with the License.
- * A copy of the License is located at
- *
- *  http://aws.amazon.com/apache2.0
- *
- * or in the "license" file accompanying this file. This file is distributed
- * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing
- * permissions and limitations under the License.
- */
-
-import 'dart:convert';
-import 'dart:typed_data';
-
-import 'package:amplify_core/amplify_core.dart';
-import 'package:meta/meta.dart';
-
-/// {@template rest_response}
-/// An HTTP response from a REST API call.
-/// {@endtemplate}
-@immutable
-class RestResponse with AWSEquatable<RestResponse> {
-  /// The response status code.
-  final int statusCode;
-
-  /// The response headers.
-  ///
-  /// Will be `null` if unavailable from the platform.
-  final Map<String, String>? headers;
-
-  /// The response body bytes.
-  final Uint8List data;
-
-  /// The decoded body (using UTF-8).
-  ///
-  /// For custom processing, use [data] to get the raw body bytes.
-  late final String body;
-
-  /// {@macro rest_response}
-  RestResponse({
-    required Uint8List? data,
-    required this.headers,
-    required this.statusCode,
-  }) : data = data ?? Uint8List(0) {
-    body = utf8.decode(this.data, allowMalformed: true);
-  }
-
-  @override
-  List<Object?> get props => [statusCode, headers, data];
-
-  @override
-  String toString() {
-    return 'RestResponse{statusCode=$statusCode, headers=$headers, body=$body}';
-  }
-}
diff --git a/packages/api/amplify_api/.gitignore b/packages/api/amplify_api/.gitignore
index e9dc58d3d6..6bb69a50e0 100644
--- a/packages/api/amplify_api/.gitignore
+++ b/packages/api/amplify_api/.gitignore
@@ -1,7 +1,43 @@
+# See https://dart.dev/guides/libraries/private-files
+
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
 .DS_Store
-.dart_tool/
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
 
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
 .packages
+.pub-cache/
 .pub/
-
 build/
+
+# Code coverage
+coverage/
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
diff --git a/packages/api/amplify_api/example/lib/graphql_api_view.dart b/packages/api/amplify_api/example/lib/graphql_api_view.dart
index 53a218efcd..6644dad380 100644
--- a/packages/api/amplify_api/example/lib/graphql_api_view.dart
+++ b/packages/api/amplify_api/example/lib/graphql_api_view.dart
@@ -14,6 +14,7 @@
  */
 
 import 'package:amplify_flutter/amplify_flutter.dart';
+import 'package:async/async.dart';
 import 'package:flutter/material.dart';
 
 class GraphQLApiView extends StatefulWidget {
@@ -29,7 +30,7 @@ class GraphQLApiView extends StatefulWidget {
 class _GraphQLApiViewState extends State<GraphQLApiView> {
   String _result = '';
   void Function()? _unsubscribe;
-  late GraphQLOperation _lastOperation;
+  late CancelableOperation _lastOperation;
 
   Future<void> subscribe() async {
     String graphQLDocument = '''subscription MySubscription {
diff --git a/packages/api/amplify_api/example/lib/main.dart b/packages/api/amplify_api/example/lib/main.dart
index 5c044e7aec..6e5dbf862d 100644
--- a/packages/api/amplify_api/example/lib/main.dart
+++ b/packages/api/amplify_api/example/lib/main.dart
@@ -44,7 +44,7 @@ class _MyAppState extends State<MyApp> {
   }
 
   void _configureAmplify() async {
-    Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]);
+    await Amplify.addPlugins([AmplifyAuthCognito(), AmplifyAPI()]);
 
     try {
       await Amplify.configure(amplifyconfig);
diff --git a/packages/api/amplify_api/example/lib/rest_api_view.dart b/packages/api/amplify_api/example/lib/rest_api_view.dart
index aeca89c97f..68f8a414f1 100644
--- a/packages/api/amplify_api/example/lib/rest_api_view.dart
+++ b/packages/api/amplify_api/example/lib/rest_api_view.dart
@@ -13,9 +13,8 @@
  * permissions and limitations under the License.
  */
 
-import 'dart:convert';
-
 import 'package:amplify_flutter/amplify_flutter.dart';
+import 'package:async/async.dart';
 import 'package:flutter/material.dart';
 
 class RestApiView extends StatefulWidget {
@@ -27,7 +26,7 @@ class RestApiView extends StatefulWidget {
 
 class _RestApiViewState extends State<RestApiView> {
   late TextEditingController _apiPathController;
-  late RestOperation _lastRestOperation;
+  late CancelableOperation _lastRestOperation;
 
   @override
   void initState() {
@@ -39,18 +38,16 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onPutPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.put(
-        restOptions: RestOptions(
-          path: _apiPathController.text,
-          body: ascii.encode('{"name":"Mow the lawn"}'),
-        ),
+      final restOperation = Amplify.API.put(
+        _apiPathController.text,
+        body: HttpPayload.json({'name': 'Mow the lawn'}),
       );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Put SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on Exception catch (e) {
       print('Put FAILED');
       print(e);
@@ -59,18 +56,16 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onPostPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.post(
-        restOptions: RestOptions(
-          path: _apiPathController.text,
-          body: ascii.encode('{"name":"Mow the lawn"}'),
-        ),
+      final restOperation = Amplify.API.post(
+        _apiPathController.text,
+        body: HttpPayload.json({'name': 'Mow the lawn'}),
       );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Post SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on Exception catch (e) {
       print('Post FAILED');
       print(e);
@@ -79,16 +74,15 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onGetPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.get(
-          restOptions: RestOptions(
-        path: _apiPathController.text,
-      ));
+      final restOperation = Amplify.API.get(
+        _apiPathController.text,
+      );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Get SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on ApiException catch (e) {
       print('Get FAILED');
       print(e.toString());
@@ -97,15 +91,14 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onDeletePressed() async {
     try {
-      RestOperation restOperation = Amplify.API.delete(
-        restOptions: RestOptions(path: _apiPathController.text),
+      final restOperation = Amplify.API.delete(
+        _apiPathController.text,
       );
-
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Delete SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on Exception catch (e) {
       print('Delete FAILED');
       print(e);
@@ -123,15 +116,14 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onHeadPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.head(
-        restOptions: RestOptions(path: _apiPathController.text),
+      final restOperation = Amplify.API.head(
+        _apiPathController.text,
       );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      await restOperation.response;
 
       print('Head SUCCESS');
-      print(response);
     } on ApiException catch (e) {
       print('Head FAILED');
       print(e.toString());
@@ -140,15 +132,16 @@ class _RestApiViewState extends State<RestApiView> {
 
   void onPatchPressed() async {
     try {
-      RestOperation restOperation = Amplify.API.patch(
-        restOptions: RestOptions(path: _apiPathController.text),
+      final restOperation = Amplify.API.patch(
+        _apiPathController.text,
+        body: HttpPayload.json({'name': 'Mow the lawn'}),
       );
 
       _lastRestOperation = restOperation;
-      RestResponse response = await restOperation.response;
+      final response = await restOperation.response;
 
       print('Patch SUCCESS');
-      print(response);
+      print(response.decodeBody());
     } on ApiException catch (e) {
       print('Patch FAILED');
       print(e.toString());
diff --git a/packages/api/amplify_api/example/pubspec.yaml b/packages/api/amplify_api/example/pubspec.yaml
index a57976922e..8b2d58e92d 100644
--- a/packages/api/amplify_api/example/pubspec.yaml
+++ b/packages/api/amplify_api/example/pubspec.yaml
@@ -21,6 +21,7 @@ dependencies:
     path: ../../../auth/amplify_auth_cognito
   amplify_flutter:
     path: ../../../amplify/amplify_flutter
+  async: ^2.8.2
   aws_common: ^0.2.0
 
   # The following adds the Cupertino Icons font to your application.
diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart
index 59deb7fca0..95e8f5c17d 100644
--- a/packages/api/amplify_api/lib/src/method_channel_api.dart
+++ b/packages/api/amplify_api/lib/src/method_channel_api.dart
@@ -14,13 +14,14 @@
  */
 
 import 'dart:async';
+import 'dart:convert';
 import 'dart:typed_data';
 
 import 'package:amplify_api/src/graphql/graphql_response_decoder.dart';
 import 'package:amplify_api/src/graphql/graphql_subscription_event.dart';
 import 'package:amplify_api/src/graphql/graphql_subscription_transformer.dart';
 import 'package:amplify_core/amplify_core.dart';
-
+import 'package:async/async.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 
@@ -150,31 +151,19 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
   }
 
   @override
-  GraphQLOperation<T> query<T>({required GraphQLRequest<T> request}) {
-    Future<GraphQLResponse<T>> response =
+  CancelableOperation<GraphQLResponse<T>> query<T>(
+      {required GraphQLRequest<T> request}) {
+    Future<GraphQLResponse<T>> responseFuture =
         _getMethodChannelResponse(methodName: 'query', request: request);
-
-    //TODO: Cancel implementation will be added along with REST API as it is shared
-    GraphQLOperation<T> result = GraphQLOperation<T>(
-      cancel: () => cancelRequest(request.id),
-      response: response,
-    );
-
-    return result;
+    return CancelableOperation.fromFuture(responseFuture);
   }
 
   @override
-  GraphQLOperation<T> mutate<T>({required GraphQLRequest<T> request}) {
-    Future<GraphQLResponse<T>> response =
+  CancelableOperation<GraphQLResponse<T>> mutate<T>(
+      {required GraphQLRequest<T> request}) {
+    Future<GraphQLResponse<T>> responseFuture =
         _getMethodChannelResponse(methodName: 'mutate', request: request);
-
-    //TODO: Cancel implementation will be added along with REST API as it is shared
-    GraphQLOperation<T> result = GraphQLOperation<T>(
-      cancel: () => cancelRequest(request.id),
-      response: response,
-    );
-
-    return result;
+    return CancelableOperation.fromFuture(responseFuture);
   }
 
   @override
@@ -248,21 +237,73 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
   }
 
   // ====== RestAPI ======
-  RestOperation _restFunctionHelper(
-      {required String methodName, required RestOptions restOptions}) {
-    // Send Request cancelToken to Native
-    String cancelToken = UUID.getUUID();
 
-    Future<RestResponse> futureResponse =
-        _callNativeRestMethod(methodName, cancelToken, restOptions);
+  Future<AWSStreamedHttpResponse> _restResponseHelper({
+    required String methodName,
+    required String path,
+    required String cancelToken,
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) async {
+    Uint8List? bodyBytes;
+    if (body != null) {
+      final completer = Completer<Uint8List>();
+      final sink = ByteConversionSink.withCallback(
+        (bytes) => completer.complete(Uint8List.fromList(bytes)),
+      );
+      body.listen(
+        sink.add,
+        onError: completer.completeError,
+        onDone: sink.close,
+        cancelOnError: true,
+      );
+      bodyBytes = await completer.future;
+    }
 
-    return RestOperation(
-      response: futureResponse,
-      cancel: () => cancelRequest(cancelToken),
+    final restOptions = RestOptions(
+      path: path,
+      body: bodyBytes,
+      apiName: apiName,
+      queryParameters: queryParameters,
+      headers: headers,
+    );
+    return _callNativeRestMethod(methodName, cancelToken, restOptions);
+  }
+
+  CancelableOperation<AWSStreamedHttpResponse> _restFunctionHelper({
+    required String methodName,
+    required String path,
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    // Send Request cancelToken to Native
+    String cancelToken = uuid();
+    // Ensure Content-Type header matches payload.
+    var modifiedHeaders = headers != null ? Map.of(headers) : null;
+    final contentType = body?.contentType;
+    if (contentType != null) {
+      modifiedHeaders = modifiedHeaders ?? {};
+      modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType);
+    }
+    final responseFuture = _restResponseHelper(
+      methodName: methodName,
+      path: path,
+      cancelToken: cancelToken,
+      body: body,
+      headers: modifiedHeaders,
+      queryParameters: queryParameters,
+      apiName: apiName,
     );
+
+    return CancelableOperation.fromFuture(responseFuture,
+        onCancel: () => cancelRequest(cancelToken));
   }
 
-  Future<RestResponse> _callNativeRestMethod(
+  Future<AWSStreamedHttpResponse> _callNativeRestMethod(
       String methodName, String cancelToken, RestOptions restOptions) async {
     // Prepare map input
     Map<String, dynamic> inputsMap = <String, dynamic>{};
@@ -284,55 +325,125 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
     }
   }
 
-  bool _shouldThrow(int statusCode) {
-    return statusCode < 200 || statusCode > 299;
-  }
-
-  RestResponse _formatRestResponse(Map<String, dynamic> res) {
+  AWSStreamedHttpResponse _formatRestResponse(Map<String, dynamic> res) {
     final statusCode = res['statusCode'] as int;
-    final headers = res['headers'] as Map?;
-    final response = RestResponse(
-      data: res['data'] as Uint8List?,
-      headers: headers?.cast<String, String>(),
-      statusCode: statusCode,
-    );
-    if (_shouldThrow(statusCode)) {
-      throw RestException(response);
-    }
-    return response;
+    // Make type-safe version of response headers.
+    final serializedHeaders = res['headers'] as Map?;
+    final headers = serializedHeaders?.cast<String, String>();
+    final rawResponseBody = res['data'] as Uint8List?;
+
+    return AWSStreamedHttpResponse(
+        statusCode: statusCode,
+        headers: headers,
+        body: Stream.value(rawResponseBody ?? []));
   }
 
   @override
-  RestOperation get({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'get', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> get(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'get',
+      path: path,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation put({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'put', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> put(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'put',
+      path: path,
+      body: body,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation post({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'post', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> post(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'post',
+      path: path,
+      body: body,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation delete({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'delete', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> delete(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'delete',
+      path: path,
+      body: body,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation head({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'head', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> head(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'head',
+      path: path,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
   @override
-  RestOperation patch({required RestOptions restOptions}) {
-    return _restFunctionHelper(methodName: 'patch', restOptions: restOptions);
+  CancelableOperation<AWSStreamedHttpResponse> patch(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    return _restFunctionHelper(
+      methodName: 'patch',
+      path: path,
+      body: body,
+      headers: headers,
+      queryParameters: queryParameters,
+      apiName: apiName,
+    );
   }
 
-  @override
+  /// Cancels a request with a given request ID.
+  @Deprecated('Use .cancel() on CancelableOperation instead.')
   Future<void> cancelRequest(String cancelToken) async {
     print('Attempting to cancel Operation $cancelToken');
 
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index 9e710688b7..b2d6056f92 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -4,6 +4,7 @@ version: 1.0.0-next.0+1
 homepage: https://docs.amplify.aws/lib/q/platform/flutter/
 repository: https://github.com/aws-amplify/amplify-flutter/tree/next/packages/api/amplify_api
 issue_tracker: https://github.com/aws-amplify/amplify-flutter/issues
+publish_to: none # until finalized
 
 environment:
   sdk: ">=2.17.0 <3.0.0"
@@ -14,6 +15,7 @@ dependencies:
   amplify_api_ios: 1.0.0-next.0
   amplify_core: 1.0.0-next.0
   amplify_flutter: '>=1.0.0-next.0 <1.0.0-next.1'
+  async: ^2.8.2
   aws_common: ^0.2.0
   collection: ^1.15.0
   flutter:
@@ -25,7 +27,6 @@ dev_dependencies:
   amplify_lints: ^2.0.0
   amplify_test:
     path: ../../amplify_test
-  async: ^2.6.0
   build_runner: ^2.0.0
   flutter_test:
     sdk: flutter
diff --git a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart
index 925c940b6b..5106ada1c2 100644
--- a/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart
+++ b/packages/api/amplify_api/test/amplify_rest_api_methods_test.dart
@@ -26,9 +26,19 @@ import 'graphql_helpers_test.dart';
 
 const statusOK = 200;
 const statusBadRequest = 400;
-
-// Matchers
-final throwsRestException = throwsA(isA<RestException>());
+const mowLawnBody = '{"name": "Mow the lawn"}';
+const hello = 'Hello from lambda!';
+final helloResponse = ascii.encode(hello);
+final encodedMowLoanBody = ascii.encode(mowLawnBody);
+const queryParameters = {
+  'queryParameterA': 'queryValueA',
+  'queryParameterB': 'queryValueB'
+};
+const headers = {
+  'headerA': 'headerValueA',
+  'headerB': 'headerValueB',
+  AWSHeaders.contentType: 'text/plain'
+};
 
 void main() {
   TestWidgetsFlutterBinding.ensureInitialized();
@@ -42,184 +52,177 @@ void main() {
     await Amplify.addPlugin(api);
   });
 
-  test('PUT returns proper response.data', () async {
-    var responseData = Uint8List.fromList(
-        '{"success": "put call succeed!","url":/items?queryParameterA=queryValueA&queryParameterB=queryValueB","body": {"name": "Mow the lawn"}}'
-            .codeUnits);
-    var body = Uint8List.fromList('{"name":"Mow the lawn"}'.codeUnits);
-    var queryParameters = {
-      'queryParameterA': 'queryValueA',
-      'queryParameterB': 'queryValueB'
-    };
-    var headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'};
+  Future<void> _assertResponse(AWSStreamedHttpResponse response) async {
+    final actualResponseBody = await response.decodeBody();
+    expect(actualResponseBody, hello);
+    expect(response.statusCode, statusOK);
+  }
 
+  test('PUT returns proper response.data', () async {
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
       if (methodCall.method == 'put') {
         Map<dynamic, dynamic> restOptions =
             methodCall.arguments['restOptions'] as Map;
         expect(restOptions['apiName'], 'restapi');
         expect(restOptions['path'], '/items');
-        expect(restOptions['body'], body);
+        expect(restOptions['body'], encodedMowLoanBody);
         expect(restOptions['queryParameters'], queryParameters);
         expect(restOptions['headers'], headers);
-
-        return {'data': responseData, 'statusCode': statusOK};
+        return {'data': helloResponse, 'statusCode': statusOK};
       }
     });
 
-    RestOperation restOperation = api.put(
-      restOptions: RestOptions(
-        path: '/items',
-        body: body,
-        apiName: 'restapi',
-        queryParameters: queryParameters,
-        headers: headers,
-      ),
+    final restOperation = api.put(
+      '/items',
+      body: HttpPayload.string(mowLawnBody),
+      apiName: 'restapi',
+      queryParameters: queryParameters,
+      headers: headers,
     );
 
-    RestResponse response = await restOperation.response;
-
-    expect(response.data, responseData);
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
   test('POST returns proper response.data', () async {
-    var responseData = Uint8List.fromList(
-        '{"success": "post call succeed!","url":"/items?queryParameterA=queryValueA&queryParameterB=queryValueB","body": {"name": "Mow the lawn"}}'
-            .codeUnits);
-    var body = Uint8List.fromList('{"name":"Mow the lawn"}'.codeUnits);
-    var queryParameters = {
-      'queryParameterA': 'queryValueA',
-      'queryParameterB': 'queryValueB'
-    };
-    var headers = {'headerA': 'headerValueA', 'headerB': 'headerValueB'};
-
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
       if (methodCall.method == 'post') {
         Map<dynamic, dynamic> restOptions =
             methodCall.arguments['restOptions'] as Map;
         expect(restOptions['apiName'], 'restapi');
         expect(restOptions['path'], '/items');
-        expect(restOptions['body'], body);
+        expect(restOptions['body'], encodedMowLoanBody);
         expect(restOptions['queryParameters'], queryParameters);
         expect(restOptions['headers'], headers);
-
-        return {'data': responseData, 'statusCode': statusOK};
+        return {'data': helloResponse, 'statusCode': statusOK};
       }
     });
 
-    RestOperation restOperation = api.post(
-      restOptions: RestOptions(
-        path: '/items',
-        body: body,
-        apiName: 'restapi',
-        headers: headers,
-        queryParameters: queryParameters,
-      ),
+    final restOperation = api.post(
+      '/items',
+      body: HttpPayload.string(mowLawnBody),
+      apiName: 'restapi',
+      queryParameters: queryParameters,
+      headers: headers,
     );
 
-    RestResponse response = await restOperation.response;
-
-    expect(response.data, responseData);
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
   test('GET returns proper response.data', () async {
-    var responseData = Uint8List.fromList(
-        '{"success":"get call succeed!","url":"/items"}'.codeUnits);
-
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
       if (methodCall.method == 'get') {
         Map<dynamic, dynamic> restOptions =
             methodCall.arguments['restOptions'] as Map;
+        expect(restOptions['apiName'], 'restapi');
         expect(restOptions['path'], '/items');
-
-        return {'data': responseData, 'statusCode': statusOK};
+        expect(restOptions['queryParameters'], queryParameters);
+        expect(restOptions['headers'], headers);
+        return {'data': helloResponse, 'statusCode': statusOK};
       }
     });
 
-    RestOperation restOperation = api.get(
-        restOptions: const RestOptions(
-      path: '/items',
-    ));
-
-    RestResponse response = await restOperation.response;
+    final restOperation = api.get(
+      '/items',
+      apiName: 'restapi',
+      queryParameters: queryParameters,
+      headers: headers,
+    );
 
-    expect(response.data, responseData);
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
   test('DELETE returns proper response.data', () async {
-    var responseData = Uint8List.fromList(
-        '{"success":"delete call succeed!","url":"/items"}'.codeUnits);
-
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
       if (methodCall.method == 'delete') {
         Map<dynamic, dynamic> restOptions =
             methodCall.arguments['restOptions'] as Map;
+        expect(restOptions['apiName'], 'restapi');
         expect(restOptions['path'], '/items');
-
-        return {'data': responseData, 'statusCode': statusOK};
+        expect(restOptions['body'], encodedMowLoanBody);
+        expect(restOptions['queryParameters'], queryParameters);
+        expect(restOptions['headers'], headers);
+        return {'data': helloResponse, 'statusCode': statusOK};
       }
     });
 
-    RestOperation restOperation = api.delete(
-        restOptions: const RestOptions(
-      path: '/items',
-    ));
-
-    RestResponse response = await restOperation.response;
+    final restOperation = api.delete(
+      '/items',
+      body: HttpPayload.string(mowLawnBody),
+      apiName: 'restapi',
+      queryParameters: queryParameters,
+      headers: headers,
+    );
 
-    expect(response.data, responseData);
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
-  test('GET Status Code Error throws proper error', () async {
+  test(
+      'POST with form-encoded body gets proper response with response headers included',
+      () async {
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
-      if (methodCall.method == 'get') {
-        throw PlatformException(code: 'ApiException', details: {
-          'message': 'AMPLIFY_API_MUTATE_FAILED',
-          'recoverySuggestion': 'some insightful suggestion',
-          'underlyingException': 'Act of God'
-        });
+      if (methodCall.method == 'post') {
+        Map<dynamic, dynamic> restOptions =
+            methodCall.arguments['restOptions'] as Map;
+        expect(restOptions['apiName'], 'restapi');
+        expect(restOptions['path'], '/items');
+        expect(restOptions['queryParameters'], queryParameters);
+        expect(restOptions['headers'][AWSHeaders.contentType],
+            'application/x-www-form-urlencoded');
+        expect(utf8.decode(restOptions['body'] as List<int>), 'foo=bar');
+        return {
+          'data': helloResponse,
+          'statusCode': statusOK,
+          'headers': {'foo': 'bar'}
+        };
       }
     });
 
-    try {
-      RestOperation restOperation = api.get(
-          restOptions: const RestOptions(
-        path: '/items',
-      ));
-      await restOperation.response;
-    } on ApiException catch (e) {
-      expect(e.message, 'AMPLIFY_API_MUTATE_FAILED');
-      expect(e.recoverySuggestion, 'some insightful suggestion');
-      expect(e.underlyingException, 'Act of God');
-    }
+    final restOperation = api.post(
+      '/items',
+      apiName: 'restapi',
+      body: HttpPayload.formFields({'foo': 'bar'}),
+      queryParameters: queryParameters,
+    );
+
+    final response = await restOperation.value;
+    expect(response.headers['foo'], 'bar');
+    await _assertResponse(response);
   });
 
-  test('GET exception adds the httpStatusCode to exception if available',
+  test(
+      'POST with json-encoded body has property Content-Type and gets proper response',
       () async {
-    const statusCode = 500;
-    const data = 'Internal server error';
-
     apiChannel.setMockMethodCallHandler((MethodCall methodCall) async {
-      if (methodCall.method == 'get') {
+      if (methodCall.method == 'post') {
+        Map<dynamic, dynamic> restOptions =
+            methodCall.arguments['restOptions'] as Map;
+        expect(restOptions['apiName'], 'restapi');
+        expect(restOptions['path'], '/items');
+        expect(restOptions['queryParameters'], queryParameters);
+        expect(
+            restOptions['headers'][AWSHeaders.contentType], 'application/json');
+        expect(utf8.decode(restOptions['body'] as List<int>), '{"foo":"bar"}');
         return {
-          'statusCode': statusCode,
-          'headers': <String, String>{},
-          'data': Uint8List.fromList(data.codeUnits),
+          'data': helloResponse,
+          'statusCode': statusOK,
+          'headers': {'foo': 'bar'}
         };
       }
     });
 
-    try {
-      RestOperation restOperation = api.get(
-        restOptions: const RestOptions(
-          path: '/items',
-        ),
-      );
-      await restOperation.response;
-    } on RestException catch (e) {
-      expect(e.response.statusCode, 500);
-      expect(e.response.body, data);
-    }
+    final restOperation = api.post(
+      '/items',
+      apiName: 'restapi',
+      body: HttpPayload.json({'foo': 'bar'}),
+      queryParameters: queryParameters,
+    );
+
+    final response = await restOperation.value;
+    await _assertResponse(response);
   });
 
   test('CANCEL success does not throw error', () async {
@@ -237,50 +240,9 @@ void main() {
       }
     });
 
-    RestOperation restOperation = api.get(
-        restOptions: const RestOptions(
-      path: '/items',
-    ));
+    final restOperation = api.get('/items');
 
     //RestResponse response = await restOperation.response;
     restOperation.cancel();
   });
-
-  group('non-2xx status code', () {
-    const testBody = 'test';
-    const testResponseHeaders = {'key': 'value'};
-
-    setUpAll(() {
-      apiChannel.setMockMethodCallHandler((call) async {
-        return {
-          'data': utf8.encode(testBody),
-          'statusCode': statusBadRequest,
-          'headers': testResponseHeaders,
-        };
-      });
-    });
-
-    test('throws RestException', () async {
-      final restOp = api.get(restOptions: const RestOptions(path: '/'));
-      await expectLater(restOp.response, throwsRestException);
-    });
-
-    test('has valid RestResponse', () async {
-      final restOp = api.get(restOptions: const RestOptions(path: '/'));
-
-      RestException restException;
-      try {
-        await restOp.response;
-        fail('RestOperation should throw');
-      } on Exception catch (e) {
-        expect(e, isA<RestException>());
-        restException = e as RestException;
-      }
-
-      final response = restException.response;
-      expect(response.statusCode, statusBadRequest);
-      expect(response.headers, testResponseHeaders);
-      expect(response.body, testBody);
-    });
-  });
 }
diff --git a/packages/auth/amplify_auth_cognito/example/lib/main.dart b/packages/auth/amplify_auth_cognito/example/lib/main.dart
index bf1e0a3609..fe282ac4cf 100644
--- a/packages/auth/amplify_auth_cognito/example/lib/main.dart
+++ b/packages/auth/amplify_auth_cognito/example/lib/main.dart
@@ -12,9 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import 'dart:convert';
 import 'dart:io';
-import 'dart:typed_data';
 
 import 'package:amplify_api/amplify_api.dart';
 import 'package:amplify_auth_cognito/amplify_auth_cognito.dart';
@@ -177,14 +175,13 @@ class _HomeScreenState extends State<HomeScreen> {
     try {
       final response = await Amplify.API
           .post(
-            restOptions: RestOptions(
-              path: '/hello',
-              body: utf8.encode(_controller.text) as Uint8List,
-            ),
+            '/hello',
+            body: HttpPayload.string(_controller.text),
           )
-          .response;
+          .value;
+      final decodedBody = await response.decodeBody();
       setState(() {
-        _greeting = response.body;
+        _greeting = decodedBody;
       });
     } on Exception catch (e) {
       setState(() {

From ef1b223dfc3d2dd68edb0b9ca8954627a9cf63cd Mon Sep 17 00:00:00 2001
From: Elijah Quartey <Equartey@users.noreply.github.com>
Date: Thu, 23 Jun 2022 11:39:46 -0500
Subject: [PATCH 20/33] chore(api): API Native Bridge for .addPlugin() (#1756)

---
 packages/api/amplify_api/Makefile             |   4 +
 packages/api/amplify_api/example/pubspec.yaml |   3 +-
 packages/api/amplify_api/lib/amplify_api.dart |  21 ++--
 .../amplify_api/lib/src/api_plugin_impl.dart  |  81 ++++++++++++++
 .../lib/src/native_api_plugin.dart            |  63 +++++++++++
 .../pigeons/native_api_plugin.dart            |  43 ++++++++
 packages/api/amplify_api/pubspec.yaml         |  10 +-
 .../amplify/amplify_api/AmplifyApi.kt         |  52 +++++----
 .../amplify_api/NativeApiPluginBindings.java  |  87 +++++++++++++++
 packages/api/amplify_api_android/pubspec.yaml |   3 +-
 .../ios/Classes/NativeApiPlugin.h             |  35 ++++++
 .../ios/Classes/NativeApiPlugin.m             | 102 ++++++++++++++++++
 .../ios/Classes/SwiftAmplifyApiPlugin.swift   |  43 ++++----
 .../ios/Classes/amplify_api_ios.h             |  21 ++++
 .../ios/amplify_api_ios.podspec               |  14 +++
 .../api/amplify_api_ios/ios/module.modulemap  |   6 ++
 packages/api/amplify_api_ios/pubspec.yaml     |   3 +-
 17 files changed, 526 insertions(+), 65 deletions(-)
 create mode 100644 packages/api/amplify_api/Makefile
 create mode 100644 packages/api/amplify_api/lib/src/api_plugin_impl.dart
 create mode 100644 packages/api/amplify_api/lib/src/native_api_plugin.dart
 create mode 100644 packages/api/amplify_api/pigeons/native_api_plugin.dart
 create mode 100644 packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
 create mode 100644 packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
 create mode 100644 packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
 create mode 100644 packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h
 create mode 100644 packages/api/amplify_api_ios/ios/module.modulemap

diff --git a/packages/api/amplify_api/Makefile b/packages/api/amplify_api/Makefile
new file mode 100644
index 0000000000..f1c3ac38ba
--- /dev/null
+++ b/packages/api/amplify_api/Makefile
@@ -0,0 +1,4 @@
+.PHONY: pigeons
+pigeons:
+	flutter pub run pigeon --input pigeons/native_api_plugin.dart
+	flutter format --fix lib/src/native_api_plugin.dart
diff --git a/packages/api/amplify_api/example/pubspec.yaml b/packages/api/amplify_api/example/pubspec.yaml
index 8b2d58e92d..3303c4f462 100644
--- a/packages/api/amplify_api/example/pubspec.yaml
+++ b/packages/api/amplify_api/example/pubspec.yaml
@@ -32,7 +32,8 @@ dependencies:
     sdk: flutter
 
 dev_dependencies:
-  amplify_lints: ^2.0.0
+  amplify_lints: 
+    path: ../../../amplify_lints
   amplify_test:
     path: ../../../amplify_test
   flutter_driver:
diff --git a/packages/api/amplify_api/lib/amplify_api.dart b/packages/api/amplify_api/lib/amplify_api.dart
index f0ca3c2c4f..a4db7b1e97 100644
--- a/packages/api/amplify_api/lib/amplify_api.dart
+++ b/packages/api/amplify_api/lib/amplify_api.dart
@@ -15,9 +15,7 @@
 
 library amplify_api_plugin;
 
-import 'dart:io';
-
-import 'package:amplify_api/src/method_channel_api.dart';
+import 'package:amplify_api/src/api_plugin_impl.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:meta/meta.dart';
 
@@ -32,18 +30,11 @@ export './model_subscriptions.dart';
 /// {@endtemplate}
 abstract class AmplifyAPI extends APIPluginInterface {
   /// {@macro amplify_api.amplify_api}
-  factory AmplifyAPI({
-    List<APIAuthProvider> authProviders = const [],
-    ModelProviderInterface? modelProvider,
-  }) {
-    if (zIsWeb || Platform.isWindows || Platform.isMacOS || Platform.isLinux) {
-      throw UnsupportedError('This platform is not supported yet');
-    }
-    return AmplifyAPIMethodChannel(
-      authProviders: authProviders,
-      modelProvider: modelProvider,
-    );
-  }
+  factory AmplifyAPI(
+          {List<APIAuthProvider> authProviders = const [],
+          ModelProviderInterface? modelProvider}) =>
+      AmplifyAPIDart(
+          authProviders: authProviders, modelProvider: modelProvider);
 
   /// Protected constructor for subclasses.
   @protected
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
new file mode 100644
index 0000000000..5ac4fc36ff
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -0,0 +1,81 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+library amplify_api;
+
+import 'dart:io';
+
+import 'package:amplify_api/amplify_api.dart';
+import 'package:amplify_api/src/native_api_plugin.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter/services.dart';
+
+/// {@template amplify_api.amplify_api_dart}
+/// The AWS implementation of the Amplify API category.
+/// {@endtemplate}
+class AmplifyAPIDart extends AmplifyAPI {
+  late final AWSApiPluginConfig _apiConfig;
+
+  /// The registered [APIAuthProvider] instances.
+  final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};
+
+  /// {@macro amplify_api.amplify_api_dart}
+  AmplifyAPIDart({
+    List<APIAuthProvider> authProviders = const [],
+    this.modelProvider,
+  }) : super.protected() {
+    authProviders.forEach(registerAuthProvider);
+  }
+
+  @override
+  Future<void> configure({AmplifyConfig? config}) async {
+    final apiConfig = config?.api?.awsPlugin;
+    if (apiConfig == null) {
+      throw const ApiException('No AWS API config found',
+          recoverySuggestion: 'Add API from the Amplify CLI. See '
+              'https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/flutter/#configure-api');
+    }
+    _apiConfig = apiConfig;
+  }
+
+  @override
+  Future<void> addPlugin() async {
+    if (zIsWeb || !(Platform.isAndroid || Platform.isIOS)) {
+      return;
+    }
+
+    final nativeBridge = NativeApiBridge();
+    try {
+      final authProvidersList =
+          _authProviders.keys.map((key) => key.rawValue).toList();
+      await nativeBridge.addPlugin(authProvidersList);
+    } on PlatformException catch (e) {
+      if (e.code == 'AmplifyAlreadyConfiguredException') {
+        throw const AmplifyAlreadyConfiguredException(
+            AmplifyExceptionMessages.alreadyConfiguredDefaultMessage,
+            recoverySuggestion:
+                AmplifyExceptionMessages.alreadyConfiguredDefaultSuggestion);
+      }
+      throw AmplifyException.fromMap((e.details as Map).cast());
+    }
+  }
+
+  @override
+  final ModelProviderInterface? modelProvider;
+
+  @override
+  void registerAuthProvider(APIAuthProvider authProvider) {
+    _authProviders[authProvider.type] = authProvider;
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/native_api_plugin.dart b/packages/api/amplify_api/lib/src/native_api_plugin.dart
new file mode 100644
index 0000000000..e7c5af4d04
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/native_api_plugin.dart
@@ -0,0 +1,63 @@
+//
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//
+// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
+// @dart = 2.12
+import 'dart:async';
+import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;
+
+import 'package:flutter/foundation.dart' show WriteBuffer, ReadBuffer;
+import 'package:flutter/services.dart';
+
+class _NativeApiBridgeCodec extends StandardMessageCodec {
+  const _NativeApiBridgeCodec();
+}
+
+class NativeApiBridge {
+  /// Constructor for [NativeApiBridge].  The [binaryMessenger] named argument is
+  /// available for dependency injection.  If it is left null, the default
+  /// BinaryMessenger will be used which routes to the host platform.
+  NativeApiBridge({BinaryMessenger? binaryMessenger})
+      : _binaryMessenger = binaryMessenger;
+
+  final BinaryMessenger? _binaryMessenger;
+
+  static const MessageCodec<Object?> codec = _NativeApiBridgeCodec();
+
+  Future<void> addPlugin(List<String?> arg_authProvidersList) async {
+    final BasicMessageChannel<Object?> channel = BasicMessageChannel<Object?>(
+        'dev.flutter.pigeon.NativeApiBridge.addPlugin', codec,
+        binaryMessenger: _binaryMessenger);
+    final Map<Object?, Object?>? replyMap = await channel
+        .send(<Object?>[arg_authProvidersList]) as Map<Object?, Object?>?;
+    if (replyMap == null) {
+      throw PlatformException(
+        code: 'channel-error',
+        message: 'Unable to establish connection on channel.',
+      );
+    } else if (replyMap['error'] != null) {
+      final Map<Object?, Object?> error =
+          (replyMap['error'] as Map<Object?, Object?>?)!;
+      throw PlatformException(
+        code: (error['code'] as String?)!,
+        message: error['message'] as String?,
+        details: error['details'],
+      );
+    } else {
+      return;
+    }
+  }
+}
diff --git a/packages/api/amplify_api/pigeons/native_api_plugin.dart b/packages/api/amplify_api/pigeons/native_api_plugin.dart
new file mode 100644
index 0000000000..a36f7397f9
--- /dev/null
+++ b/packages/api/amplify_api/pigeons/native_api_plugin.dart
@@ -0,0 +1,43 @@
+//
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//
+
+// ignore_for_file: avoid_positional_boolean_parameters
+
+@ConfigurePigeon(
+  PigeonOptions(
+    copyrightHeader: '../../../tool/license.txt',
+    dartOptions: DartOptions(),
+    dartOut: 'lib/src/native_Api_plugin.dart',
+    javaOptions: JavaOptions(
+      className: 'NativeApiPluginBindings',
+      package: 'com.amazonaws.amplify.amplify_api',
+    ),
+    javaOut:
+        '../amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java',
+    objcOptions: ObjcOptions(
+      header: 'NativeApiPlugin.h',
+    ),
+    objcHeaderOut: '../amplify_api_ios/ios/Classes/NativeApiPlugin.h',
+    objcSourceOut: '../amplify_api_ios/ios/Classes/NativeApiPlugin.m',
+  ),
+)
+library native_api_plugin;
+
+import 'package:pigeon/pigeon.dart';
+
+@HostApi()
+abstract class NativeApiBridge {
+  void addPlugin(List<String> authProvidersList);
+}
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index b2d6056f92..317faa108c 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -23,14 +23,22 @@ dependencies:
   meta: ^1.7.0
   plugin_platform_interface: ^2.0.0
 
+dependency_overrides:
+  # TODO(dnys1): Remove when pigeon is bumped
+  # https://github.com/flutter/flutter/issues/105090
+  analyzer: ^3.0.0
+
+
 dev_dependencies:
-  amplify_lints: ^2.0.0
+  amplify_lints: 
+    path: ../../amplify_lints
   amplify_test:
     path: ../../amplify_test
   build_runner: ^2.0.0
   flutter_test:
     sdk: flutter
   mockito: ^5.0.0
+  pigeon: ^3.1.5
 
 # The following section is specific to Flutter.
 flutter:
diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
index 02de711722..0205877bf7 100644
--- a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
+++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
@@ -39,7 +39,7 @@ import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.Dispatchers
 
 /** AmplifyApiPlugin */
-class AmplifyApi : FlutterPlugin, MethodCallHandler {
+class AmplifyApi : FlutterPlugin, MethodCallHandler, NativeApiPluginBindings.NativeApiBridge {
 
     private companion object {
         /**
@@ -83,6 +83,11 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler {
             "com.amazonaws.amplify/api_observe_events"
         )
         eventchannel!!.setStreamHandler(graphqlSubscriptionStreamHandler)
+
+        NativeApiPluginBindings.NativeApiBridge.setup(
+            flutterPluginBinding.binaryMessenger,
+            this
+        )
     }
 
     @Suppress("UNCHECKED_CAST")
@@ -94,27 +99,6 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler {
         if (methodName == "cancel") {
             onCancel(result, (call.arguments as String))
             return
-        } else if (methodName == "addPlugin") {
-            try {
-                val authProvidersList: List<String> =
-                    (arguments["authProviders"] as List<*>?)?.cast() ?: listOf()
-                val authProviders = authProvidersList.map { AuthorizationType.valueOf(it) }
-                if (flutterAuthProviders == null) {
-                    flutterAuthProviders = FlutterAuthProviders(authProviders)
-                }
-                flutterAuthProviders!!.setMethodChannel(channel)
-                Amplify.addPlugin(
-                    AWSApiPlugin
-                        .builder()
-                        .apiAuthProviders(flutterAuthProviders!!.factory)
-                        .build()
-                )
-                logger.info("Added API plugin")
-                result.success(null)
-            } catch (e: Exception) {
-                handleAddPluginException("API", e, result)
-            }
-            return
         }
 
         try {
@@ -168,5 +152,29 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler {
         eventchannel = null
         graphqlSubscriptionStreamHandler?.close()
         graphqlSubscriptionStreamHandler = null
+
+        NativeApiPluginBindings.NativeApiBridge.setup(
+            flutterPluginBinding.binaryMessenger,
+            null,
+        )
+    }
+
+    override fun addPlugin(authProvidersList: MutableList<String>) {
+        try {
+            val authProviders = authProvidersList.map { AuthorizationType.valueOf(it) }
+            if (flutterAuthProviders == null) {
+                flutterAuthProviders = FlutterAuthProviders(authProviders)
+            }
+            flutterAuthProviders!!.setMethodChannel(channel)
+            Amplify.addPlugin(
+                AWSApiPlugin
+                    .builder()
+                    .apiAuthProviders(flutterAuthProviders!!.factory)
+                    .build()
+            )
+            logger.info("Added API plugin")
+        } catch (e: Exception) {
+            logger.error(e.message)
+        }
     }
 }
diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
new file mode 100644
index 0000000000..d8d07f4add
--- /dev/null
+++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
@@ -0,0 +1,87 @@
+// 
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// 
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+// 
+//  http://aws.amazon.com/apache2.0
+// 
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//  
+// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+package com.amazonaws.amplify.amplify_api;
+
+import android.util.Log;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import io.flutter.plugin.common.BasicMessageChannel;
+import io.flutter.plugin.common.BinaryMessenger;
+import io.flutter.plugin.common.MessageCodec;
+import io.flutter.plugin.common.StandardMessageCodec;
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.HashMap;
+
+/** Generated class from Pigeon. */
+@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
+public class NativeApiPluginBindings {
+  private static class NativeApiBridgeCodec extends StandardMessageCodec {
+    public static final NativeApiBridgeCodec INSTANCE = new NativeApiBridgeCodec();
+    private NativeApiBridgeCodec() {}
+  }
+
+  /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
+  public interface NativeApiBridge {
+    void addPlugin(@NonNull List<String> authProvidersList);
+
+    /** The codec used by NativeApiBridge. */
+    static MessageCodec<Object> getCodec() {
+      return NativeApiBridgeCodec.INSTANCE;
+    }
+
+    /** Sets up an instance of `NativeApiBridge` to handle messages through the `binaryMessenger`. */
+    static void setup(BinaryMessenger binaryMessenger, NativeApiBridge api) {
+      {
+        BasicMessageChannel<Object> channel =
+            new BasicMessageChannel<>(binaryMessenger, "dev.flutter.pigeon.NativeApiBridge.addPlugin", getCodec());
+        if (api != null) {
+          channel.setMessageHandler((message, reply) -> {
+            Map<String, Object> wrapped = new HashMap<>();
+            try {
+              ArrayList<Object> args = (ArrayList<Object>)message;
+              List<String> authProvidersListArg = (List<String>)args.get(0);
+              if (authProvidersListArg == null) {
+                throw new NullPointerException("authProvidersListArg unexpectedly null.");
+              }
+              api.addPlugin(authProvidersListArg);
+              wrapped.put("result", null);
+            }
+            catch (Error | RuntimeException exception) {
+              wrapped.put("error", wrapError(exception));
+            }
+            reply.reply(wrapped);
+          });
+        } else {
+          channel.setMessageHandler(null);
+        }
+      }
+    }
+  }
+  private static Map<String, Object> wrapError(Throwable exception) {
+    Map<String, Object> errorMap = new HashMap<>();
+    errorMap.put("message", exception.toString());
+    errorMap.put("code", exception.getClass().getSimpleName());
+    errorMap.put("details", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
+    return errorMap;
+  }
+}
diff --git a/packages/api/amplify_api_android/pubspec.yaml b/packages/api/amplify_api_android/pubspec.yaml
index 1047613c82..72272c62a5 100644
--- a/packages/api/amplify_api_android/pubspec.yaml
+++ b/packages/api/amplify_api_android/pubspec.yaml
@@ -14,7 +14,8 @@ dependencies:
     sdk: flutter
 
 dev_dependencies:
-  amplify_lints: ^2.0.0
+  amplify_lints: 
+    path: ../../amplify_lints
   flutter_test:
     sdk: flutter
 
diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
new file mode 100644
index 0000000000..7b3bad24ed
--- /dev/null
+++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
@@ -0,0 +1,35 @@
+// 
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// 
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+// 
+//  http://aws.amazon.com/apache2.0
+// 
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//  
+// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import <Foundation/Foundation.h>
+@protocol FlutterBinaryMessenger;
+@protocol FlutterMessageCodec;
+@class FlutterError;
+@class FlutterStandardTypedData;
+
+NS_ASSUME_NONNULL_BEGIN
+
+
+/// The codec used by NativeApiBridge.
+NSObject<FlutterMessageCodec> *NativeApiBridgeGetCodec(void);
+
+@protocol NativeApiBridge
+- (void)addPluginAuthProvidersList:(NSArray<NSString *> *)authProvidersList error:(FlutterError *_Nullable *_Nonnull)error;
+@end
+
+extern void NativeApiBridgeSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<NativeApiBridge> *_Nullable api);
+
+NS_ASSUME_NONNULL_END
diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
new file mode 100644
index 0000000000..c936591be5
--- /dev/null
+++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
@@ -0,0 +1,102 @@
+// 
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// 
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+// 
+//  http://aws.amazon.com/apache2.0
+// 
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+//  
+// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+#import "NativeApiPlugin.h"
+#import <Flutter/Flutter.h>
+
+#if !__has_feature(objc_arc)
+#error File requires ARC to be enabled.
+#endif
+
+static NSDictionary<NSString *, id> *wrapResult(id result, FlutterError *error) {
+  NSDictionary *errorDict = (NSDictionary *)[NSNull null];
+  if (error) {
+    errorDict = @{
+        @"code": (error.code ?: [NSNull null]),
+        @"message": (error.message ?: [NSNull null]),
+        @"details": (error.details ?: [NSNull null]),
+        };
+  }
+  return @{
+      @"result": (result ?: [NSNull null]),
+      @"error": errorDict,
+      };
+}
+static id GetNullableObject(NSDictionary* dict, id key) {
+  id result = dict[key];
+  return (result == [NSNull null]) ? nil : result;
+}
+static id GetNullableObjectAtIndex(NSArray* array, NSInteger key) {
+  id result = array[key];
+  return (result == [NSNull null]) ? nil : result;
+}
+
+
+
+@interface NativeApiBridgeCodecReader : FlutterStandardReader
+@end
+@implementation NativeApiBridgeCodecReader
+@end
+
+@interface NativeApiBridgeCodecWriter : FlutterStandardWriter
+@end
+@implementation NativeApiBridgeCodecWriter
+@end
+
+@interface NativeApiBridgeCodecReaderWriter : FlutterStandardReaderWriter
+@end
+@implementation NativeApiBridgeCodecReaderWriter
+- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data {
+  return [[NativeApiBridgeCodecWriter alloc] initWithData:data];
+}
+- (FlutterStandardReader *)readerWithData:(NSData *)data {
+  return [[NativeApiBridgeCodecReader alloc] initWithData:data];
+}
+@end
+
+NSObject<FlutterMessageCodec> *NativeApiBridgeGetCodec() {
+  static dispatch_once_t sPred = 0;
+  static FlutterStandardMessageCodec *sSharedObject = nil;
+  dispatch_once(&sPred, ^{
+    NativeApiBridgeCodecReaderWriter *readerWriter = [[NativeApiBridgeCodecReaderWriter alloc] init];
+    sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter];
+  });
+  return sSharedObject;
+}
+
+
+void NativeApiBridgeSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<NativeApiBridge> *api) {
+  {
+    FlutterBasicMessageChannel *channel =
+      [[FlutterBasicMessageChannel alloc]
+        initWithName:@"dev.flutter.pigeon.NativeApiBridge.addPlugin"
+        binaryMessenger:binaryMessenger
+        codec:NativeApiBridgeGetCodec()        ];
+    if (api) {
+      NSCAssert([api respondsToSelector:@selector(addPluginAuthProvidersList:error:)], @"NativeApiBridge api (%@) doesn't respond to @selector(addPluginAuthProvidersList:error:)", api);
+      [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
+        NSArray *args = message;
+        NSArray<NSString *> *arg_authProvidersList = GetNullableObjectAtIndex(args, 0);
+        FlutterError *error;
+        [api addPluginAuthProvidersList:arg_authProvidersList error:&error];
+        callback(wrapResult(nil, error));
+      }];
+    }
+    else {
+      [channel setMessageHandler:nil];
+    }
+  }
+}
diff --git a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
index 7ad1accd1a..63ce5c373c 100644
--- a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
+++ b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
@@ -20,7 +20,7 @@ import AmplifyPlugins
 import amplify_flutter_ios
 import AWSPluginsCore
 
-public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin {
+public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
     private let bridge: ApiBridge
     private let graphQLSubscriptionsStreamHandler: GraphQLSubscriptionsStreamHandler
     static var methodChannel: FlutterMethodChannel!
@@ -43,6 +43,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin {
         let instance = SwiftAmplifyApiPlugin()
         eventchannel.setStreamHandler(instance.graphQLSubscriptionsStreamHandler)
         registrar.addMethodCallDelegate(instance, channel: methodChannel)
+        NativeApiBridgeSetup(registrar.messenger(), instance)
     }
 
     public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
@@ -62,33 +63,26 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin {
 
             let arguments = try FlutterApiRequest.getMap(args: callArgs)
 
-            if method == "addPlugin"{
-                let authProvidersList = arguments["authProviders"] as? [String] ?? []
-                let authProviders = authProvidersList.compactMap {
-                    AWSAuthorizationType(rawValue: $0)
-                }
-                addPlugin(authProviders: authProviders, result: result)
-                return
-            }
-
             try innerHandle(method: method, arguments: arguments, result: result)
         } catch {
             print("Failed to parse query arguments with \(error)")
             FlutterApiErrorHandler.handleApiError(error: APIError(error: error), flutterResult: result)
         }
     }
-
-    private func addPlugin(authProviders: [AWSAuthorizationType], result: FlutterResult) {
+    
+    public func addPluginAuthProvidersList(_ authProvidersList: [String], error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
         do {
+            let authProviders = authProvidersList.compactMap {
+                AWSAuthorizationType(rawValue: $0)
+            }
             try Amplify.add(
                 plugin: AWSAPIPlugin(
                     sessionFactory: FlutterURLSessionBehaviorFactory(),
                     apiAuthProviderFactory: FlutterAuthProviders(authProviders)))
-            result(true)
         } catch let apiError as APIError {
-            ErrorUtil.postErrorToFlutterChannel(
-                result: result,
-                errorCode: "APIException",
+            error.pointee = FlutterError(
+                code: "APIException",
+                message: apiError.localizedDescription,
                 details: [
                     "message": apiError.errorDescription,
                     "recoverySuggestion": apiError.recoverySuggestion,
@@ -100,20 +94,21 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin {
             if case .amplifyAlreadyConfigured = configError {
                 errorCode = "AmplifyAlreadyConfiguredException"
             }
-            ErrorUtil.postErrorToFlutterChannel(
-                result: result,
-                errorCode: errorCode,
+            error.pointee = FlutterError(
+                code: errorCode,
+                message: configError.localizedDescription,
                 details: [
                     "message": configError.errorDescription,
                     "recoverySuggestion": configError.recoverySuggestion,
                     "underlyingError": configError.underlyingError?.localizedDescription ?? ""
                 ]
             )
-        } catch {
-            ErrorUtil.postErrorToFlutterChannel(
-                result: result,
-                errorCode: "UNKNOWN",
-                details: ["message": error.localizedDescription])
+        } catch let e {
+            error.pointee = FlutterError(
+                code: "UNKNOWN",
+                message: e.localizedDescription,
+                details: nil
+            )
         }
     }
 
diff --git a/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h b/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h
new file mode 100644
index 0000000000..0b890efd4f
--- /dev/null
+++ b/packages/api/amplify_api_ios/ios/Classes/amplify_api_ios.h
@@ -0,0 +1,21 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// 
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// 
+//      http://www.apache.org/licenses/LICENSE-2.0
+// 
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef amplify_api_ios_h
+#define amplify_api_ios_h
+
+#import "NativeApiPlugin.h"
+#import "AmplifyApi.h"
+
+#endif /* amplify_api_ios_h */
diff --git a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
index 181063b97c..276c97012b 100644
--- a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
+++ b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
@@ -21,6 +21,20 @@ The API module for Amplify Flutter.
   s.platform = :ios, '11.0'
   s.swift_version = '5.0'
 
+  # Use a custom module map with a manually written umbrella header.
+  #
+  # Since we use `package:pigeon` to generate our platform interface 
+  # in ObjC, and since the rest of the module is written in Swift, we
+  # fall victim to this issue: https://github.com/CocoaPods/CocoaPods/issues/10544
+  # 
+  # This is because we have an ObjC -> Swift -> ObjC import cycle:
+  # ApiPlugin -> SwiftAmplifyApiPlugin -> NativeApiPlugin
+  # 
+  # The easiest solution to this problem is to create the umbrella
+  # header which would otherwise be auto-generated by Cocoapods but
+  # name it what's expected by the Swift compiler (amplify_api_ios.h).
+  s.module_map = 'module.modulemap'
+
   # Flutter.framework does not contain a i386 slice.
   s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
 end
diff --git a/packages/api/amplify_api_ios/ios/module.modulemap b/packages/api/amplify_api_ios/ios/module.modulemap
new file mode 100644
index 0000000000..acac87c311
--- /dev/null
+++ b/packages/api/amplify_api_ios/ios/module.modulemap
@@ -0,0 +1,6 @@
+framework module amplify_api_ios {
+    umbrella header "amplify_api_ios.h"
+
+    export *
+    module * { export * }
+}
diff --git a/packages/api/amplify_api_ios/pubspec.yaml b/packages/api/amplify_api_ios/pubspec.yaml
index a9dbcb40c4..3ce5b1f2b6 100644
--- a/packages/api/amplify_api_ios/pubspec.yaml
+++ b/packages/api/amplify_api_ios/pubspec.yaml
@@ -15,7 +15,8 @@ dependencies:
     sdk: flutter
 
 dev_dependencies:
-  amplify_lints: ^2.0.0
+  amplify_lints: 
+    path: ../../amplify_lints
   flutter_test:
     sdk: flutter
 

From 4621a666ea2dab8833104601275b41c1c680d096 Mon Sep 17 00:00:00 2001
From: Elijah Quartey <Equartey@users.noreply.github.com>
Date: Mon, 27 Jun 2022 11:43:25 -0500
Subject: [PATCH 21/33] chore(api): API Pigeon update (#1813)

---
 .../lib/src/native_api_plugin.dart            |  2 +-
 .../pigeons/native_api_plugin.dart            |  1 +
 packages/api/amplify_api/pubspec.yaml         |  8 +-----
 .../amplify/amplify_api/AmplifyApi.kt         |  7 +++++-
 .../amplify_api/NativeApiPluginBindings.java  | 25 +++++++++++++++----
 .../ios/Classes/NativeApiPlugin.h             |  4 +--
 .../ios/Classes/NativeApiPlugin.m             | 10 ++++----
 .../ios/Classes/SwiftAmplifyApiPlugin.swift   |  9 ++++---
 .../ios/amplify_api_ios.podspec               |  6 +++--
 9 files changed, 45 insertions(+), 27 deletions(-)

diff --git a/packages/api/amplify_api/lib/src/native_api_plugin.dart b/packages/api/amplify_api/lib/src/native_api_plugin.dart
index e7c5af4d04..3ff74bd774 100644
--- a/packages/api/amplify_api/lib/src/native_api_plugin.dart
+++ b/packages/api/amplify_api/lib/src/native_api_plugin.dart
@@ -12,7 +12,7 @@
 // express or implied. See the License for the specific language governing
 // permissions and limitations under the License.
 //
-// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// Autogenerated from Pigeon (v3.2.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name
 // @dart = 2.12
diff --git a/packages/api/amplify_api/pigeons/native_api_plugin.dart b/packages/api/amplify_api/pigeons/native_api_plugin.dart
index a36f7397f9..0e54029724 100644
--- a/packages/api/amplify_api/pigeons/native_api_plugin.dart
+++ b/packages/api/amplify_api/pigeons/native_api_plugin.dart
@@ -39,5 +39,6 @@ import 'package:pigeon/pigeon.dart';
 
 @HostApi()
 abstract class NativeApiBridge {
+  @async
   void addPlugin(List<String> authProvidersList);
 }
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index 317faa108c..e7392862e1 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -23,12 +23,6 @@ dependencies:
   meta: ^1.7.0
   plugin_platform_interface: ^2.0.0
 
-dependency_overrides:
-  # TODO(dnys1): Remove when pigeon is bumped
-  # https://github.com/flutter/flutter/issues/105090
-  analyzer: ^3.0.0
-
-
 dev_dependencies:
   amplify_lints: 
     path: ../../amplify_lints
@@ -38,7 +32,7 @@ dev_dependencies:
   flutter_test:
     sdk: flutter
   mockito: ^5.0.0
-  pigeon: ^3.1.5
+  pigeon: ^3.1.6
 
 # The following section is specific to Flutter.
 flutter:
diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
index 0205877bf7..e49a66932a 100644
--- a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
+++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/AmplifyApi.kt
@@ -159,7 +159,10 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler, NativeApiPluginBindings.Nat
         )
     }
 
-    override fun addPlugin(authProvidersList: MutableList<String>) {
+    override fun addPlugin(
+        authProvidersList: MutableList<String>,
+        result: NativeApiPluginBindings.Result<Void>
+    ) {
         try {
             val authProviders = authProvidersList.map { AuthorizationType.valueOf(it) }
             if (flutterAuthProviders == null) {
@@ -173,8 +176,10 @@ class AmplifyApi : FlutterPlugin, MethodCallHandler, NativeApiPluginBindings.Nat
                     .build()
             )
             logger.info("Added API plugin")
+            result.success(null)
         } catch (e: Exception) {
             logger.error(e.message)
+            result.error(e)
         }
     }
 }
diff --git a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
index d8d07f4add..70c59352c8 100644
--- a/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
+++ b/packages/api/amplify_api_android/android/src/main/kotlin/com/amazonaws/amplify/amplify_api/NativeApiPluginBindings.java
@@ -12,7 +12,7 @@
 // express or implied. See the License for the specific language governing
 // permissions and limitations under the License.
 //  
-// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// Autogenerated from Pigeon (v3.2.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 
 package com.amazonaws.amplify.amplify_api;
@@ -35,6 +35,11 @@
 /** Generated class from Pigeon. */
 @SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})
 public class NativeApiPluginBindings {
+
+  public interface Result<T> {
+    void success(T result);
+    void error(Throwable error);
+  }
   private static class NativeApiBridgeCodec extends StandardMessageCodec {
     public static final NativeApiBridgeCodec INSTANCE = new NativeApiBridgeCodec();
     private NativeApiBridgeCodec() {}
@@ -42,7 +47,7 @@ private NativeApiBridgeCodec() {}
 
   /** Generated interface from Pigeon that represents a handler of messages from Flutter.*/
   public interface NativeApiBridge {
-    void addPlugin(@NonNull List<String> authProvidersList);
+    void addPlugin(@NonNull List<String> authProvidersList, Result<Void> result);
 
     /** The codec used by NativeApiBridge. */
     static MessageCodec<Object> getCodec() {
@@ -63,13 +68,23 @@ static void setup(BinaryMessenger binaryMessenger, NativeApiBridge api) {
               if (authProvidersListArg == null) {
                 throw new NullPointerException("authProvidersListArg unexpectedly null.");
               }
-              api.addPlugin(authProvidersListArg);
-              wrapped.put("result", null);
+              Result<Void> resultCallback = new Result<Void>() {
+                public void success(Void result) {
+                  wrapped.put("result", null);
+                  reply.reply(wrapped);
+                }
+                public void error(Throwable error) {
+                  wrapped.put("error", wrapError(error));
+                  reply.reply(wrapped);
+                }
+              };
+
+              api.addPlugin(authProvidersListArg, resultCallback);
             }
             catch (Error | RuntimeException exception) {
               wrapped.put("error", wrapError(exception));
+              reply.reply(wrapped);
             }
-            reply.reply(wrapped);
           });
         } else {
           channel.setMessageHandler(null);
diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
index 7b3bad24ed..cf89fcb539 100644
--- a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
+++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.h
@@ -12,7 +12,7 @@
 // express or implied. See the License for the specific language governing
 // permissions and limitations under the License.
 //  
-// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// Autogenerated from Pigeon (v3.2.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 #import <Foundation/Foundation.h>
 @protocol FlutterBinaryMessenger;
@@ -27,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
 NSObject<FlutterMessageCodec> *NativeApiBridgeGetCodec(void);
 
 @protocol NativeApiBridge
-- (void)addPluginAuthProvidersList:(NSArray<NSString *> *)authProvidersList error:(FlutterError *_Nullable *_Nonnull)error;
+- (void)addPluginAuthProvidersList:(NSArray<NSString *> *)authProvidersList completion:(void(^)(FlutterError *_Nullable))completion;
 @end
 
 extern void NativeApiBridgeSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<NativeApiBridge> *_Nullable api);
diff --git a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
index c936591be5..bae599aa4b 100644
--- a/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
+++ b/packages/api/amplify_api_ios/ios/Classes/NativeApiPlugin.m
@@ -12,7 +12,7 @@
 // express or implied. See the License for the specific language governing
 // permissions and limitations under the License.
 //  
-// Autogenerated from Pigeon (v3.1.5), do not edit directly.
+// Autogenerated from Pigeon (v3.2.0), do not edit directly.
 // See also: https://pub.dev/packages/pigeon
 #import "NativeApiPlugin.h"
 #import <Flutter/Flutter.h>
@@ -86,13 +86,13 @@ void NativeApiBridgeSetup(id<FlutterBinaryMessenger> binaryMessenger, NSObject<N
         binaryMessenger:binaryMessenger
         codec:NativeApiBridgeGetCodec()        ];
     if (api) {
-      NSCAssert([api respondsToSelector:@selector(addPluginAuthProvidersList:error:)], @"NativeApiBridge api (%@) doesn't respond to @selector(addPluginAuthProvidersList:error:)", api);
+      NSCAssert([api respondsToSelector:@selector(addPluginAuthProvidersList:completion:)], @"NativeApiBridge api (%@) doesn't respond to @selector(addPluginAuthProvidersList:completion:)", api);
       [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) {
         NSArray *args = message;
         NSArray<NSString *> *arg_authProvidersList = GetNullableObjectAtIndex(args, 0);
-        FlutterError *error;
-        [api addPluginAuthProvidersList:arg_authProvidersList error:&error];
-        callback(wrapResult(nil, error));
+        [api addPluginAuthProvidersList:arg_authProvidersList completion:^(FlutterError *_Nullable error) {
+          callback(wrapResult(nil, error));
+        }];
       }];
     }
     else {
diff --git a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
index 63ce5c373c..01c14b8e0c 100644
--- a/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
+++ b/packages/api/amplify_api_ios/ios/Classes/SwiftAmplifyApiPlugin.swift
@@ -70,7 +70,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
         }
     }
     
-    public func addPluginAuthProvidersList(_ authProvidersList: [String], error: AutoreleasingUnsafeMutablePointer<FlutterError?>) {
+    public func addPluginAuthProvidersList(_ authProvidersList: [String]) async -> FlutterError? {
         do {
             let authProviders = authProvidersList.compactMap {
                 AWSAuthorizationType(rawValue: $0)
@@ -79,8 +79,9 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
                 plugin: AWSAPIPlugin(
                     sessionFactory: FlutterURLSessionBehaviorFactory(),
                     apiAuthProviderFactory: FlutterAuthProviders(authProviders)))
+            return nil
         } catch let apiError as APIError {
-            error.pointee = FlutterError(
+            return FlutterError(
                 code: "APIException",
                 message: apiError.localizedDescription,
                 details: [
@@ -94,7 +95,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
             if case .amplifyAlreadyConfigured = configError {
                 errorCode = "AmplifyAlreadyConfiguredException"
             }
-            error.pointee = FlutterError(
+            return FlutterError(
                 code: errorCode,
                 message: configError.localizedDescription,
                 details: [
@@ -104,7 +105,7 @@ public class SwiftAmplifyApiPlugin: NSObject, FlutterPlugin, NativeApiBridge {
                 ]
             )
         } catch let e {
-            error.pointee = FlutterError(
+            return FlutterError(
                 code: "UNKNOWN",
                 message: e.localizedDescription,
                 details: nil
diff --git a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
index 276c97012b..f5a6147bff 100644
--- a/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
+++ b/packages/api/amplify_api_ios/ios/amplify_api_ios.podspec
@@ -18,8 +18,10 @@ The API module for Amplify Flutter.
   s.dependency 'Amplify', '1.23.0'
   s.dependency 'AmplifyPlugins/AWSAPIPlugin', '1.23.0'
   s.dependency 'amplify_flutter_ios'
-  s.platform = :ios, '11.0'
-  s.swift_version = '5.0'
+
+  # These are needed to support async/await with pigeon
+  s.platform = :ios, '13.0'
+  s.swift_version = '5.5'
 
   # Use a custom module map with a manually written umbrella header.
   #

From 438c23662c11ad05c9df1a0aec0e5b954cf19831 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Mon, 27 Jun 2022 14:28:39 -0800
Subject: [PATCH 22/33] feat(api): REST methods in dart with auth mode none
 (#1783)

---
 .../amplify_flutter/lib/src/hybrid_impl.dart  |   1 +
 .../lib/src/amplify_api_config.dart           |  74 ++++++++
 .../amplify_authorization_rest_client.dart    |  58 +++++++
 .../amplify_api/lib/src/api_plugin_impl.dart  | 163 +++++++++++++++++-
 .../lib/src/method_channel_api.dart           |  10 +-
 packages/api/amplify_api/lib/src/util.dart    |  32 ++++
 packages/api/amplify_api/pubspec.yaml         |   1 +
 .../test/amplify_dart_rest_methods_test.dart  | 103 +++++++++++
 .../test_data/fake_amplify_configuration.dart |  79 +++++++++
 packages/api/amplify_api/test/util_test.dart  |  42 +++++
 10 files changed, 554 insertions(+), 9 deletions(-)
 create mode 100644 packages/api/amplify_api/lib/src/amplify_api_config.dart
 create mode 100644 packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
 create mode 100644 packages/api/amplify_api/lib/src/util.dart
 create mode 100644 packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
 create mode 100644 packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
 create mode 100644 packages/api/amplify_api/test/util_test.dart

diff --git a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
index a8c91af3a0..5eb3f1257e 100644
--- a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
+++ b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
@@ -34,6 +34,7 @@ class AmplifyHybridImpl extends AmplifyClassImpl {
     );
     await Future.wait(
       [
+        ...API.plugins,
         ...Auth.plugins,
       ].map((p) => p.configure(config: amplifyConfig)),
       eagerError: true,
diff --git a/packages/api/amplify_api/lib/src/amplify_api_config.dart b/packages/api/amplify_api/lib/src/amplify_api_config.dart
new file mode 100644
index 0000000000..960f11bf9b
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/amplify_api_config.dart
@@ -0,0 +1,74 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:collection/collection.dart';
+import 'package:meta/meta.dart';
+
+const _slash = '/';
+
+@internal
+class EndpointConfig with AWSEquatable<EndpointConfig> {
+  const EndpointConfig(this.name, this.config);
+
+  final String name;
+  final AWSApiConfig config;
+
+  @override
+  List<Object?> get props => [name, config];
+
+  /// Gets the host with environment path prefix from Amplify config and combines
+  /// with [path] and [queryParameters] to return a full [Uri].
+  Uri getUri(String path, Map<String, dynamic>? queryParameters) {
+    final parsed = Uri.parse(config.endpoint);
+    // Remove leading slashes which are suggested in public documentation.
+    // https://docs.amplify.aws/lib/restapi/getting-started/q/platform/flutter/#make-a-post-request
+    if (path.startsWith(_slash)) {
+      path = path.substring(1);
+    }
+    // Avoid URI-encoding slashes in path from caller.
+    final pathSegmentsFromPath = path.split(_slash);
+    return parsed.replace(pathSegments: [
+      ...parsed.pathSegments,
+      ...pathSegmentsFromPath,
+    ], queryParameters: queryParameters);
+  }
+}
+
+@internal
+extension AWSApiPluginConfigHelpers on AWSApiPluginConfig {
+  EndpointConfig getEndpoint({
+    required EndpointType type,
+    String? apiName,
+  }) {
+    final typeConfigs =
+        entries.where((config) => config.value.endpointType == type);
+    if (apiName != null) {
+      final config = typeConfigs.firstWhere(
+        (config) => config.key == apiName,
+        orElse: () => throw ApiException(
+          'No API endpoint found matching apiName $apiName.',
+        ),
+      );
+      return EndpointConfig(config.key, config.value);
+    }
+    final onlyConfig = typeConfigs.singleOrNull;
+    if (onlyConfig == null) {
+      throw const ApiException(
+        'Multiple API endpoints defined. Pass apiName parameter to specify '
+        'which one to use.',
+      );
+    }
+    return EndpointConfig(onlyConfig.key, onlyConfig.value);
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
new file mode 100644
index 0000000000..e58885385c
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
@@ -0,0 +1,58 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+/// Implementation of http [http.Client] that authorizes HTTP requests with
+/// Amplify.
+@internal
+class AmplifyAuthorizationRestClient extends http.BaseClient
+    implements Closeable {
+  /// Determines how requests with this client are authorized.
+  final AWSApiConfig endpointConfig;
+  final http.Client _baseClient;
+  final bool _useDefaultBaseClient;
+
+  /// Provide an [AWSApiConfig] which will determine how requests from this
+  /// client are authorized.
+  AmplifyAuthorizationRestClient({
+    required this.endpointConfig,
+    http.Client? baseClient,
+  })  : _useDefaultBaseClient = baseClient == null,
+        _baseClient = baseClient ?? http.Client();
+
+  /// Implementation of [send] that authorizes any request without "Authorization"
+  /// header already set.
+  @override
+  Future<http.StreamedResponse> send(http.BaseRequest request) async =>
+      _baseClient.send(_authorizeRequest(request));
+
+  @override
+  void close() {
+    if (_useDefaultBaseClient) _baseClient.close();
+  }
+
+  http.BaseRequest _authorizeRequest(http.BaseRequest request) {
+    if (!request.headers.containsKey(AWSHeaders.authorization) &&
+        endpointConfig.authorizationType != APIAuthorizationType.none) {
+      // ignore: todo
+      // TODO: Use auth providers from core to transform the request.
+    }
+    return request;
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index 5ac4fc36ff..0926c0a462 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -19,13 +19,25 @@ import 'dart:io';
 import 'package:amplify_api/amplify_api.dart';
 import 'package:amplify_api/src/native_api_plugin.dart';
 import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
 import 'package:flutter/services.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+import 'amplify_api_config.dart';
+import 'amplify_authorization_rest_client.dart';
+import 'util.dart';
 
 /// {@template amplify_api.amplify_api_dart}
 /// The AWS implementation of the Amplify API category.
 /// {@endtemplate}
 class AmplifyAPIDart extends AmplifyAPI {
   late final AWSApiPluginConfig _apiConfig;
+  final http.Client? _baseHttpClient;
+
+  /// A map of the keys from the Amplify API config to HTTP clients to use for
+  /// requests to that endpoint.
+  final Map<String, AmplifyAuthorizationRestClient> _clientPool = {};
 
   /// The registered [APIAuthProvider] instances.
   final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};
@@ -33,8 +45,10 @@ class AmplifyAPIDart extends AmplifyAPI {
   /// {@macro amplify_api.amplify_api_dart}
   AmplifyAPIDart({
     List<APIAuthProvider> authProviders = const [],
+    http.Client? baseHttpClient,
     this.modelProvider,
-  }) : super.protected() {
+  })  : _baseHttpClient = baseHttpClient,
+        super.protected() {
     authProviders.forEach(registerAuthProvider);
   }
 
@@ -71,6 +85,43 @@ class AmplifyAPIDart extends AmplifyAPI {
     }
   }
 
+  /// Returns the HTTP client to be used for REST operations.
+  ///
+  /// Use [apiName] if there are multiple REST endpoints.
+  @visibleForTesting
+  http.Client getRestClient({String? apiName}) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.rest,
+      apiName: apiName,
+    );
+    return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
+      endpointConfig: endpoint.config,
+      baseClient: _baseHttpClient,
+    );
+  }
+
+  Uri _getRestUri(
+      String path, String? apiName, Map<String, dynamic>? queryParameters) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.rest,
+      apiName: apiName,
+    );
+    return endpoint.getUri(path, queryParameters);
+  }
+
+  /// NOTE: http does not support request abort https://github.com/dart-lang/http/issues/424.
+  /// For now, just make a [CancelableOperation] to cancel the future.
+  /// To actually abort calls at network layer, need to call in
+  /// dart:io/dart:html or other library with custom http default Client() implementation.
+  CancelableOperation<T> _makeCancelable<T>(Future<T> responseFuture) {
+    return CancelableOperation.fromFuture(responseFuture);
+  }
+
+  CancelableOperation<AWSStreamedHttpResponse> _prepareRestResponse(
+      Future<AWSStreamedHttpResponse> responseFuture) {
+    return _makeCancelable(responseFuture);
+  }
+
   @override
   final ModelProviderInterface? modelProvider;
 
@@ -78,4 +129,114 @@ class AmplifyAPIDart extends AmplifyAPI {
   void registerAuthProvider(APIAuthProvider authProvider) {
     _authProviders[authProvider.type] = authProvider;
   }
+
+  // ====== REST =======
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> delete(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(AWSStreamedHttpRequest.delete(
+      uri,
+      body: body ?? HttpPayload.empty(),
+      headers: addContentTypeToHeaders(headers, body),
+    ).send(client));
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> get(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSHttpRequest.get(
+        uri,
+        headers: headers,
+      ).send(client),
+    );
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> head(
+    String path, {
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSHttpRequest.head(
+        uri,
+        headers: headers,
+      ).send(client),
+    );
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> patch(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSStreamedHttpRequest.patch(
+        uri,
+        headers: addContentTypeToHeaders(headers, body),
+        body: body ?? HttpPayload.empty(),
+      ).send(client),
+    );
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> post(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSStreamedHttpRequest.post(
+        uri,
+        headers: addContentTypeToHeaders(headers, body),
+        body: body ?? HttpPayload.empty(),
+      ).send(client),
+    );
+  }
+
+  @override
+  CancelableOperation<AWSStreamedHttpResponse> put(
+    String path, {
+    HttpPayload? body,
+    Map<String, String>? headers,
+    Map<String, String>? queryParameters,
+    String? apiName,
+  }) {
+    final uri = _getRestUri(path, apiName, queryParameters);
+    final client = getRestClient(apiName: apiName);
+    return _prepareRestResponse(
+      AWSStreamedHttpRequest.put(
+        uri,
+        headers: addContentTypeToHeaders(headers, body),
+        body: body ?? HttpPayload.empty(),
+      ).send(client),
+    );
+  }
 }
diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart
index 95e8f5c17d..c0285a6305 100644
--- a/packages/api/amplify_api/lib/src/method_channel_api.dart
+++ b/packages/api/amplify_api/lib/src/method_channel_api.dart
@@ -26,6 +26,7 @@ import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 
 import '../amplify_api.dart';
+import 'util.dart';
 
 part 'auth_token.dart';
 
@@ -282,19 +283,12 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
   }) {
     // Send Request cancelToken to Native
     String cancelToken = uuid();
-    // Ensure Content-Type header matches payload.
-    var modifiedHeaders = headers != null ? Map.of(headers) : null;
-    final contentType = body?.contentType;
-    if (contentType != null) {
-      modifiedHeaders = modifiedHeaders ?? {};
-      modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType);
-    }
     final responseFuture = _restResponseHelper(
       methodName: methodName,
       path: path,
       cancelToken: cancelToken,
       body: body,
-      headers: modifiedHeaders,
+      headers: addContentTypeToHeaders(headers, body),
       queryParameters: queryParameters,
       apiName: apiName,
     );
diff --git a/packages/api/amplify_api/lib/src/util.dart b/packages/api/amplify_api/lib/src/util.dart
new file mode 100644
index 0000000000..d91d58ce48
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/util.dart
@@ -0,0 +1,32 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:meta/meta.dart';
+
+/// Sets the 'Content-Type' of headers to match the [HttpPayload] body.
+@internal
+Map<String, String>? addContentTypeToHeaders(
+  Map<String, String>? headers,
+  HttpPayload? body,
+) {
+  final contentType = body?.contentType;
+  if (contentType == null) {
+    return headers;
+  }
+  // Create new map to avoid modifying input headers which may be unmodifiable.
+  final modifiedHeaders = Map<String, String>.of(headers ?? {});
+  modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType);
+  return modifiedHeaders;
+}
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index e7392862e1..c82797fc29 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -20,6 +20,7 @@ dependencies:
   collection: ^1.15.0
   flutter:
     sdk: flutter
+  http: ^0.13.4
   meta: ^1.7.0
   plugin_platform_interface: ^2.0.0
 
diff --git a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
new file mode 100644
index 0000000000..d8c5162377
--- /dev/null
+++ b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
@@ -0,0 +1,103 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import 'dart:convert';
+
+import 'package:amplify_api/amplify_api.dart';
+import 'package:amplify_api/src/api_plugin_impl.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:async/async.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+
+import 'test_data/fake_amplify_configuration.dart';
+
+const _expectedRestResponseBody = '"Hello from Lambda!"';
+const _pathThatShouldFail = 'notHere';
+
+class MockAmplifyAPI extends AmplifyAPIDart {
+  @override
+  http.Client getRestClient({String? apiName}) => MockClient((request) async {
+        if (request.body.isNotEmpty) {
+          expect(request.headers['Content-Type'], 'application/json');
+        }
+        if (request.url.path.contains(_pathThatShouldFail)) {
+          return http.Response('Not found', 404);
+        }
+        return http.Response(_expectedRestResponseBody, 200);
+      });
+}
+
+void main() {
+  late AmplifyAPI api;
+
+  setUpAll(() async {
+    await Amplify.addPlugin(MockAmplifyAPI());
+    await Amplify.configure(amplifyconfig);
+  });
+  group('REST API', () {
+    Future<void> _verifyRestOperation(
+      CancelableOperation<AWSStreamedHttpResponse> operation,
+    ) async {
+      final response =
+          await operation.value.timeout(const Duration(seconds: 3));
+      final body = await response.decodeBody();
+      expect(body, _expectedRestResponseBody);
+      expect(response.statusCode, 200);
+    }
+
+    test('get() should get 200', () async {
+      final operation = Amplify.API.get('items');
+      await _verifyRestOperation(operation);
+    });
+
+    test('head() should get 200', () async {
+      final operation = Amplify.API.head('items');
+      final response = await operation.value;
+      expect(response.statusCode, 200);
+    });
+
+    test('patch() should get 200', () async {
+      final operation = Amplify.API
+          .patch('items', body: HttpPayload.json({'name': 'Mow the lawn'}));
+      await _verifyRestOperation(operation);
+    });
+
+    test('post() should get 200', () async {
+      final operation = Amplify.API
+          .post('items', body: HttpPayload.json({'name': 'Mow the lawn'}));
+      await _verifyRestOperation(operation);
+    });
+
+    test('put() should get 200', () async {
+      final operation = Amplify.API
+          .put('items', body: HttpPayload.json({'name': 'Mow the lawn'}));
+      await _verifyRestOperation(operation);
+    });
+
+    test('delete() should get 200', () async {
+      final operation = Amplify.API
+          .delete('items', body: HttpPayload.json({'name': 'Mow the lawn'}));
+      await _verifyRestOperation(operation);
+    });
+
+    test('canceled request should never resolve', () async {
+      final operation = Amplify.API.get('items');
+      operation.cancel();
+      operation.then((p0) => fail('Request should have been cancelled.'));
+      await operation.valueOrCancellation();
+      expect(operation.isCanceled, isTrue);
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
new file mode 100644
index 0000000000..7b8fd53be0
--- /dev/null
+++ b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
@@ -0,0 +1,79 @@
+const amplifyconfig = '''{
+  "UserAgent": "aws-amplify-cli/2.0",
+  "Version": "1.0",
+  "api": {
+      "plugins": {
+          "awsAPIPlugin": {
+              "apiIntegrationTestGraphQL": {
+                  "endpointType": "GraphQL",
+                  "endpoint": "https://abc123.appsync-api.us-east-1.amazonaws.com/graphql",
+                  "region": "us-east-1",
+                  "authorizationType": "API_KEY",
+                  "apiKey": "abc123"
+              },
+              "api123": {
+                  "endpointType": "REST",
+                  "endpoint": "https://abc123.execute-api.us-east-1.amazonaws.com/test",
+                  "region": "us-east-1",
+                  "authorizationType": "AWS_IAM"
+              }
+          }
+      }
+  },
+  "auth": {
+      "plugins": {
+          "awsCognitoAuthPlugin": {
+              "UserAgent": "aws-amplify-cli/0.1.0",
+              "Version": "0.1.0",
+              "IdentityManager": {
+                  "Default": {}
+              },
+              "AppSync": {
+                  "Default": {
+                      "ApiUrl": "https://abc123.appsync-api.us-east-1.amazonaws.com/graphql",
+                      "Region": "us-east-1",
+                      "AuthMode": "API_KEY",
+                      "ApiKey": "abc123",
+                      "ClientDatabasePrefix": "apiIntegrationTestGraphQL_API_KEY"
+                  }
+              },
+              "CredentialsProvider": {
+                  "CognitoIdentity": {
+                      "Default": {
+                          "PoolId": "us-east-1:abc123",
+                          "Region": "us-east-1"
+                      }
+                  }
+              },
+              "CognitoUserPool": {
+                  "Default": {
+                      "PoolId": "us-east-1_abc123",
+                      "AppClientId": "abc123",
+                      "Region": "us-east-1"
+                  }
+              },
+              "Auth": {
+                  "Default": {
+                      "authenticationFlowType": "USER_SRP_AUTH",
+                      "socialProviders": [],
+                      "usernameAttributes": [],
+                      "signupAttributes": [
+                          "EMAIL"
+                      ],
+                      "passwordProtectionSettings": {
+                          "passwordPolicyMinLength": 8,
+                          "passwordPolicyCharacters": []
+                      },
+                      "mfaConfiguration": "OFF",
+                      "mfaTypes": [
+                          "SMS"
+                      ],
+                      "verificationMechanisms": [
+                          "EMAIL"
+                      ]
+                  }
+              }
+          }
+      }
+  }
+}''';
diff --git a/packages/api/amplify_api/test/util_test.dart b/packages/api/amplify_api/test/util_test.dart
new file mode 100644
index 0000000000..062b8f7276
--- /dev/null
+++ b/packages/api/amplify_api/test/util_test.dart
@@ -0,0 +1,42 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+import 'package:amplify_api/src/util.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  group('addContentTypeToHeaders', () {
+    test('adds Content-Type header from payload', () {
+      final resultHeaders = addContentTypeToHeaders(
+          null, HttpPayload.json({'name': 'now the lawn'}));
+      expect(resultHeaders?['Content-Type'], 'application/json');
+    });
+
+    test('no-op when payload null', () {
+      final inputHeaders = {'foo': 'bar'};
+      final resultHeaders = addContentTypeToHeaders(inputHeaders, null);
+      expect(resultHeaders, inputHeaders);
+    });
+
+    test('does not change input headers', () {
+      final inputHeaders = {'foo': 'bar'};
+      final resultHeaders = addContentTypeToHeaders(
+          inputHeaders, HttpPayload.json({'name': 'now the lawn'}));
+      expect(resultHeaders?['Content-Type'], 'application/json');
+      expect(inputHeaders['Content-Type'], isNull);
+    });
+  });
+}

From c99ca0c626f2f50a5e85116f0584d1a80417f118 Mon Sep 17 00:00:00 2001
From: Elijah Quartey <Equartey@users.noreply.github.com>
Date: Wed, 13 Jul 2022 15:27:11 -0500
Subject: [PATCH 23/33] feat!(api): GraphQL API key auth mode (#1858)

* feat(api): GraphQL API key auth mode

* BREAKING CHANGE: GraphQL response errors now nullable
---
 .../types/api/graphql/graphql_response.dart   |   9 +-
 packages/api/amplify_api/LICENSE              |  29 ++-
 .../amplify/amplify_api/MainActivityTest.kt   |  16 ++
 .../integration_test/graphql_tests.dart       |   8 +-
 .../provision_integration_test_resources.sh   |  14 ++
 .../lib/src/amplify_api_config.dart           |   3 +-
 .../amplify_authorization_rest_client.dart    |  14 +-
 .../amplify_api/lib/src/api_plugin_impl.dart  |  48 +++-
 .../src/graphql/graphql_response_decoder.dart |   2 +-
 .../src/graphql/model_mutations_factory.dart  |  14 ++
 .../lib/src/graphql/send_graphql_request.dart |  57 +++++
 .../lib/src/method_channel_api.dart           |  17 +-
 packages/api/amplify_api/lib/src/util.dart    |  18 ++
 .../test/amplify_api_config_test.dart         |  89 +++++++
 .../amplify_api/test/dart_graphql_test.dart   | 229 ++++++++++++++++++
 .../amplify_api/test/graphql_error_test.dart  |   2 +-
 .../query_predicate_graphql_filter_test.dart  |  14 ++
 .../test_data/fake_amplify_configuration.dart |  14 ++
 18 files changed, 568 insertions(+), 29 deletions(-)
 create mode 100644 packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
 create mode 100644 packages/api/amplify_api/test/amplify_api_config_test.dart
 create mode 100644 packages/api/amplify_api/test/dart_graphql_test.dart

diff --git a/packages/amplify_core/lib/src/types/api/graphql/graphql_response.dart b/packages/amplify_core/lib/src/types/api/graphql/graphql_response.dart
index 8a7580fd7d..dc8d2345d6 100644
--- a/packages/amplify_core/lib/src/types/api/graphql/graphql_response.dart
+++ b/packages/amplify_core/lib/src/types/api/graphql/graphql_response.dart
@@ -22,8 +22,8 @@ class GraphQLResponse<T> {
   /// This will be `null` if there are any GraphQL errors during execution.
   final T? data;
 
-  /// A list of errors from execution. If no errors, it will be an empty list.
-  final List<GraphQLResponseError> errors;
+  /// A list of errors from execution. If no errors, it will be `null`.
+  final List<GraphQLResponseError>? errors;
 
   const GraphQLResponse({
     this.data,
@@ -36,7 +36,10 @@ class GraphQLResponse<T> {
   }) {
     return GraphQLResponse(
       data: data,
-      errors: errors ?? const [],
+      errors: errors,
     );
   }
+
+  // Returns true when errors are present and not empty.
+  bool get hasErrors => errors != null && errors!.isNotEmpty;
 }
diff --git a/packages/api/amplify_api/LICENSE b/packages/api/amplify_api/LICENSE
index 19dc35b243..d645695673 100644
--- a/packages/api/amplify_api/LICENSE
+++ b/packages/api/amplify_api/LICENSE
@@ -172,4 +172,31 @@
       of any other Contributor, and only if You agree to indemnify,
       defend, and hold each Contributor harmless for any liability
       incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
\ No newline at end of file
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/packages/api/amplify_api/example/android/app/src/androidTest/java/com/amazonaws/amplify/amplify_api/MainActivityTest.kt b/packages/api/amplify_api/example/android/app/src/androidTest/java/com/amazonaws/amplify/amplify_api/MainActivityTest.kt
index 6f677739be..8b9960a876 100644
--- a/packages/api/amplify_api/example/android/app/src/androidTest/java/com/amazonaws/amplify/amplify_api/MainActivityTest.kt
+++ b/packages/api/amplify_api/example/android/app/src/androidTest/java/com/amazonaws/amplify/amplify_api/MainActivityTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
 package com.amazonaws.amplify.amplify_api_example
 
 import androidx.test.rule.ActivityTestRule
diff --git a/packages/api/amplify_api/example/integration_test/graphql_tests.dart b/packages/api/amplify_api/example/integration_test/graphql_tests.dart
index d632e2ef14..f1a9a42362 100644
--- a/packages/api/amplify_api/example/integration_test/graphql_tests.dart
+++ b/packages/api/amplify_api/example/integration_test/graphql_tests.dart
@@ -44,7 +44,7 @@ void main() {
       final req = GraphQLRequest<String>(
           document: graphQLDocument, variables: <String, String>{'id': id});
       final response = await Amplify.API.mutate(request: req).response;
-      if (response.errors.isNotEmpty) {
+      if (response.hasErrors) {
         fail(
             'GraphQL error while deleting a blog: ${response.errors.toString()}');
       }
@@ -561,7 +561,7 @@ void main() {
         // With stream established, exec callback with stream events.
         final subscription = await _getEstablishedSubscriptionOperation<T>(
             subscriptionRequest, (event) {
-          if (event.errors.isNotEmpty) {
+          if (event.hasErrors) {
             fail('subscription errors: ${event.errors}');
           }
           dataCompleter.complete(event);
@@ -657,6 +657,8 @@ void main() {
 
         expect(postFromEvent?.title, equals(title));
       });
-    });
+    },
+        skip:
+            'TODO(ragingsquirrel3): re-enable tests once subscriptions are implemented.');
   });
 }
diff --git a/packages/api/amplify_api/example/tool/provision_integration_test_resources.sh b/packages/api/amplify_api/example/tool/provision_integration_test_resources.sh
index 072ebabbda..d74e2dc37d 100755
--- a/packages/api/amplify_api/example/tool/provision_integration_test_resources.sh
+++ b/packages/api/amplify_api/example/tool/provision_integration_test_resources.sh
@@ -1,4 +1,18 @@
 #!/bin/bash
+# Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
 set -e
 IFS='|'
 
diff --git a/packages/api/amplify_api/lib/src/amplify_api_config.dart b/packages/api/amplify_api/lib/src/amplify_api_config.dart
index 960f11bf9b..4d4c21e9fa 100644
--- a/packages/api/amplify_api/lib/src/amplify_api_config.dart
+++ b/packages/api/amplify_api/lib/src/amplify_api_config.dart
@@ -29,7 +29,8 @@ class EndpointConfig with AWSEquatable<EndpointConfig> {
 
   /// Gets the host with environment path prefix from Amplify config and combines
   /// with [path] and [queryParameters] to return a full [Uri].
-  Uri getUri(String path, Map<String, dynamic>? queryParameters) {
+  Uri getUri({String? path, Map<String, dynamic>? queryParameters}) {
+    path = path ?? '';
     final parsed = Uri.parse(config.endpoint);
     // Remove leading slashes which are suggested in public documentation.
     // https://docs.amplify.aws/lib/restapi/getting-started/q/platform/flutter/#make-a-post-request
diff --git a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
index e58885385c..8a2d0678b5 100644
--- a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
+++ b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
@@ -18,6 +18,8 @@ import 'package:amplify_core/amplify_core.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
 
+const _xApiKey = 'X-Api-Key';
+
 /// Implementation of http [http.Client] that authorizes HTTP requests with
 /// Amplify.
 @internal
@@ -50,8 +52,16 @@ class AmplifyAuthorizationRestClient extends http.BaseClient
   http.BaseRequest _authorizeRequest(http.BaseRequest request) {
     if (!request.headers.containsKey(AWSHeaders.authorization) &&
         endpointConfig.authorizationType != APIAuthorizationType.none) {
-      // ignore: todo
-      // TODO: Use auth providers from core to transform the request.
+      // TODO(ragingsquirrel3): Use auth providers from core to transform the request.
+      final apiKey = endpointConfig.apiKey;
+      if (endpointConfig.authorizationType == APIAuthorizationType.apiKey) {
+        if (apiKey == null) {
+          throw const ApiException(
+              'Auth mode is API Key, but no API Key was found in config.');
+        }
+
+        request.headers.putIfAbsent(_xApiKey, () => apiKey);
+      }
     }
     return request;
   }
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index 0926c0a462..a54ad5ee2b 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -26,6 +26,7 @@ import 'package:meta/meta.dart';
 
 import 'amplify_api_config.dart';
 import 'amplify_authorization_rest_client.dart';
+import 'graphql/send_graphql_request.dart';
 import 'util.dart';
 
 /// {@template amplify_api.amplify_api_dart}
@@ -85,6 +86,19 @@ class AmplifyAPIDart extends AmplifyAPI {
     }
   }
 
+  /// Returns the HTTP client to be used for GraphQL operations.
+  ///
+  /// Use [apiName] if there are multiple GraphQL endpoints.
+  @visibleForTesting
+  http.Client getGraphQLClient({String? apiName}) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.graphQL,
+      apiName: apiName,
+    );
+    return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
+        endpointConfig: endpoint.config, baseClient: _baseHttpClient);
+  }
+
   /// Returns the HTTP client to be used for REST operations.
   ///
   /// Use [apiName] if there are multiple REST endpoints.
@@ -100,13 +114,21 @@ class AmplifyAPIDart extends AmplifyAPI {
     );
   }
 
+  Uri _getGraphQLUri(String? apiName) {
+    final endpoint = _apiConfig.getEndpoint(
+      type: EndpointType.graphQL,
+      apiName: apiName,
+    );
+    return endpoint.getUri(path: null, queryParameters: null);
+  }
+
   Uri _getRestUri(
       String path, String? apiName, Map<String, dynamic>? queryParameters) {
     final endpoint = _apiConfig.getEndpoint(
       type: EndpointType.rest,
       apiName: apiName,
     );
-    return endpoint.getUri(path, queryParameters);
+    return endpoint.getUri(path: path, queryParameters: queryParameters);
   }
 
   /// NOTE: http does not support request abort https://github.com/dart-lang/http/issues/424.
@@ -130,6 +152,30 @@ class AmplifyAPIDart extends AmplifyAPI {
     _authProviders[authProvider.type] = authProvider;
   }
 
+  // ====== GraphQL ======
+
+  @override
+  CancelableOperation<GraphQLResponse<T>> query<T>(
+      {required GraphQLRequest<T> request}) {
+    final graphQLClient = getGraphQLClient(apiName: request.apiName);
+    final uri = _getGraphQLUri(request.apiName);
+
+    final responseFuture = sendGraphQLRequest<T>(
+        request: request, client: graphQLClient, uri: uri);
+    return _makeCancelable<GraphQLResponse<T>>(responseFuture);
+  }
+
+  @override
+  CancelableOperation<GraphQLResponse<T>> mutate<T>(
+      {required GraphQLRequest<T> request}) {
+    final graphQLClient = getGraphQLClient(apiName: request.apiName);
+    final uri = _getGraphQLUri(request.apiName);
+
+    final responseFuture = sendGraphQLRequest<T>(
+        request: request, client: graphQLClient, uri: uri);
+    return _makeCancelable<GraphQLResponse<T>>(responseFuture);
+  }
+
   // ====== REST =======
 
   @override
diff --git a/packages/api/amplify_api/lib/src/graphql/graphql_response_decoder.dart b/packages/api/amplify_api/lib/src/graphql/graphql_response_decoder.dart
index 3a66a4cafb..ec77157480 100644
--- a/packages/api/amplify_api/lib/src/graphql/graphql_response_decoder.dart
+++ b/packages/api/amplify_api/lib/src/graphql/graphql_response_decoder.dart
@@ -34,7 +34,7 @@ class GraphQLResponseDecoder {
   GraphQLResponse<T> decode<T>(
       {required GraphQLRequest request,
       String? data,
-      required List<GraphQLResponseError> errors}) {
+      List<GraphQLResponseError>? errors}) {
     if (data == null) {
       return GraphQLResponse(data: null, errors: errors);
     }
diff --git a/packages/api/amplify_api/lib/src/graphql/model_mutations_factory.dart b/packages/api/amplify_api/lib/src/graphql/model_mutations_factory.dart
index c0c2a4927a..1793cbee49 100644
--- a/packages/api/amplify_api/lib/src/graphql/model_mutations_factory.dart
+++ b/packages/api/amplify_api/lib/src/graphql/model_mutations_factory.dart
@@ -1,3 +1,17 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import 'package:amplify_api/src/graphql/graphql_request_factory.dart';
 import 'package:amplify_core/amplify_core.dart';
 
diff --git a/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart b/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
new file mode 100644
index 0000000000..6eab7deadd
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import 'dart:convert';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+import '../util.dart';
+import 'graphql_response_decoder.dart';
+
+/// Converts the [GraphQLRequest] to an HTTP POST request and sends with ///[client].
+@internal
+Future<GraphQLResponse<T>> sendGraphQLRequest<T>({
+  required GraphQLRequest<T> request,
+  required http.Client client,
+  required Uri uri,
+}) async {
+  try {
+    final body = {'variables': request.variables, 'query': request.document};
+    final graphQLResponse = await client.post(uri, body: json.encode(body));
+
+    final responseBody = json.decode(graphQLResponse.body);
+
+    if (responseBody is! Map<String, dynamic>) {
+      throw ApiException(
+          'unable to parse GraphQLResponse from server response which was not a JSON object.',
+          underlyingException: graphQLResponse.body);
+    }
+
+    final responseData = responseBody['data'];
+    // Preserve `null`. json.encode(null) returns "null" not `null`
+    final responseDataJson =
+        responseData != null ? json.encode(responseData) : null;
+
+    final errors = deserializeGraphQLResponseErrors(responseBody);
+
+    return GraphQLResponseDecoder.instance
+        .decode<T>(request: request, data: responseDataJson, errors: errors);
+  } on Exception catch (e) {
+    throw ApiException('unable to send GraphQLRequest to client.',
+        underlyingException: e.toString());
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/method_channel_api.dart b/packages/api/amplify_api/lib/src/method_channel_api.dart
index c0285a6305..45f5eb862f 100644
--- a/packages/api/amplify_api/lib/src/method_channel_api.dart
+++ b/packages/api/amplify_api/lib/src/method_channel_api.dart
@@ -207,7 +207,7 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
           AmplifyExceptionMessages.nullReturnedFromMethodChannel,
         );
       }
-      final errors = _deserializeGraphQLResponseErrors(result);
+      final errors = deserializeGraphQLResponseErrors(result);
 
       GraphQLResponse<T> response = GraphQLResponseDecoder.instance.decode<T>(
           request: request, data: result['data'] as String?, errors: errors);
@@ -466,19 +466,4 @@ class AmplifyAPIMethodChannel extends AmplifyAPI {
       );
     }
   }
-
-  List<GraphQLResponseError> _deserializeGraphQLResponseErrors(
-    Map<String, dynamic> response,
-  ) {
-    final errors = response['errors'] as List?;
-    if (errors == null || errors.isEmpty) {
-      return const [];
-    }
-    return errors
-        .cast<Map>()
-        .map((message) => GraphQLResponseError.fromJson(
-              message.cast<String, dynamic>(),
-            ))
-        .toList();
-  }
 }
diff --git a/packages/api/amplify_api/lib/src/util.dart b/packages/api/amplify_api/lib/src/util.dart
index d91d58ce48..2d28b59afc 100644
--- a/packages/api/amplify_api/lib/src/util.dart
+++ b/packages/api/amplify_api/lib/src/util.dart
@@ -30,3 +30,21 @@ Map<String, String>? addContentTypeToHeaders(
   modifiedHeaders.putIfAbsent(AWSHeaders.contentType, () => contentType);
   return modifiedHeaders;
 }
+
+/// Grabs errors from GraphQL Response. Is used in method channel and Dart first code.
+/// TODO(Equartey): Move to Dart first code when method channel GraphQL implementation is removed.
+@internal
+List<GraphQLResponseError>? deserializeGraphQLResponseErrors(
+  Map<String, dynamic> response,
+) {
+  final errors = response['errors'] as List?;
+  if (errors == null || errors.isEmpty) {
+    return null;
+  }
+  return errors
+      .cast<Map>()
+      .map((message) => GraphQLResponseError.fromJson(
+            message.cast<String, dynamic>(),
+          ))
+      .toList();
+}
diff --git a/packages/api/amplify_api/test/amplify_api_config_test.dart b/packages/api/amplify_api/test/amplify_api_config_test.dart
new file mode 100644
index 0000000000..5168adfa04
--- /dev/null
+++ b/packages/api/amplify_api/test/amplify_api_config_test.dart
@@ -0,0 +1,89 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:convert';
+
+import 'package:amplify_api/src/amplify_api_config.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'test_data/fake_amplify_configuration.dart';
+
+void main() {
+  late EndpointConfig endpointConfig;
+
+  group('GraphQL Config', () {
+    const endpointType = EndpointType.graphQL;
+    const endpoint =
+        'https://abc123.appsync-api.us-east-1.amazonaws.com/graphql';
+    const region = 'us-east-1';
+    const authorizationType = APIAuthorizationType.apiKey;
+    const apiKey = 'abc-123';
+
+    setUpAll(() async {
+      const config = AWSApiConfig(
+          endpointType: endpointType,
+          endpoint: endpoint,
+          region: region,
+          authorizationType: authorizationType,
+          apiKey: apiKey);
+
+      endpointConfig = const EndpointConfig('GraphQL', config);
+    });
+
+    test('should return valid URI with null params', () async {
+      final uri = endpointConfig.getUri();
+      final expected = Uri.parse('$endpoint/');
+
+      expect(uri, equals(expected));
+    });
+  });
+
+  group('REST Config', () {
+    const endpointType = EndpointType.rest;
+    const endpoint = 'https://abc123.appsync-api.us-east-1.amazonaws.com/test';
+    const region = 'us-east-1';
+    const authorizationType = APIAuthorizationType.iam;
+
+    setUpAll(() async {
+      const config = AWSApiConfig(
+          endpointType: endpointType,
+          endpoint: endpoint,
+          region: region,
+          authorizationType: authorizationType);
+
+      endpointConfig = const EndpointConfig('REST', config);
+    });
+
+    test('should return valid URI with params', () async {
+      final path = 'path/to/nowhere';
+      final params = {'foo': 'bar', 'bar': 'baz'};
+      final uri = endpointConfig.getUri(path: path, queryParameters: params);
+
+      final expected = Uri.parse('$endpoint/$path?foo=bar&bar=baz');
+
+      expect(uri, equals(expected));
+    });
+
+    test('should handle a leading slash', () async {
+      final path = '/path/to/nowhere';
+      final params = {'foo': 'bar', 'bar': 'baz'};
+      final uri = endpointConfig.getUri(path: path, queryParameters: params);
+
+      final expected = Uri.parse('$endpoint$path?foo=bar&bar=baz');
+
+      expect(uri, equals(expected));
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/dart_graphql_test.dart b/packages/api/amplify_api/test/dart_graphql_test.dart
new file mode 100644
index 0000000000..bedd0092f2
--- /dev/null
+++ b/packages/api/amplify_api/test/dart_graphql_test.dart
@@ -0,0 +1,229 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:convert';
+
+import 'package:amplify_api/amplify_api.dart';
+import 'package:amplify_api/src/api_plugin_impl.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:amplify_test/test_models/ModelProvider.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+
+import 'test_data/fake_amplify_configuration.dart';
+
+final _deepEquals = const DeepCollectionEquality().equals;
+
+// Success Mocks
+const _expectedQuerySuccessResponseBody = {
+  'data': {
+    'listBlogs': {
+      'items': [
+        {
+          'id': 'TEST_ID',
+          'name': 'Test App Blog',
+          'createdAt': '2022-06-28T17:36:52.460Z'
+        }
+      ]
+    }
+  }
+};
+
+final _modelQueryId = uuid();
+final _expectedModelQueryResult = {
+  'data': {
+    'getBlog': {
+      'createdAt': '2021-07-21T22:23:33.707Z',
+      'id': _modelQueryId,
+      'name': 'Test App Blog'
+    }
+  }
+};
+const _expectedMutateSuccessResponseBody = {
+  'data': {
+    'createBlog': {
+      'id': 'TEST_ID',
+      'name': 'Test App Blog',
+      'createdAt': '2022-07-06T18:42:26.126Z'
+    }
+  }
+};
+
+// Error Mocks
+const _errorMessage = 'Unable to parse GraphQL query.';
+const _errorLocations = [
+  {'line': 2, 'column': 3},
+  {'line': 4, 'column': 5}
+];
+const _errorPath = ['a', 1, 'b'];
+const _errorExtensions = {
+  'a': 'blah',
+  'b': {'c': 'd'}
+};
+const _expectedErrorResponseBody = {
+  'data': null,
+  'errors': [
+    {
+      'message': _errorMessage,
+      'locations': _errorLocations,
+      'path': _errorPath,
+      'extensions': _errorExtensions,
+    },
+  ]
+};
+
+class MockAmplifyAPI extends AmplifyAPIDart {
+  MockAmplifyAPI({
+    ModelProviderInterface? modelProvider,
+  }) : super(modelProvider: modelProvider);
+
+  @override
+  http.Client getGraphQLClient({String? apiName}) =>
+      MockClient((request) async {
+        if (request.body.contains('getBlog')) {
+          return http.Response(json.encode(_expectedModelQueryResult), 200);
+        }
+        if (request.body.contains('TestMutate')) {
+          return http.Response(
+              json.encode(_expectedMutateSuccessResponseBody), 400);
+        }
+        if (request.body.contains('TestError')) {
+          return http.Response(json.encode(_expectedErrorResponseBody), 400);
+        }
+
+        return http.Response(
+            json.encode(_expectedQuerySuccessResponseBody), 200);
+      });
+}
+
+void main() {
+  setUpAll(() async {
+    await Amplify.addPlugin(MockAmplifyAPI(
+      modelProvider: ModelProvider.instance,
+    ));
+    await Amplify.configure(amplifyconfig);
+  });
+  group('Vanilla GraphQL', () {
+    test('Query returns proper response.data', () async {
+      String graphQLDocument = ''' query TestQuery {
+          listBlogs {
+            items {
+              id
+              name
+              createdAt
+            }
+          }
+        } ''';
+      final req = GraphQLRequest(document: graphQLDocument, variables: {});
+
+      final operation = Amplify.API.query(request: req);
+      final res = await operation.value;
+
+      final expected = json.encode(_expectedQuerySuccessResponseBody['data']);
+
+      expect(res.data, equals(expected));
+      expect(res.errors, equals(null));
+    });
+
+    test('Mutate returns proper response.data', () async {
+      String graphQLDocument = ''' mutation TestMutate(\$name: String!) {
+          createBlog(input: {name: \$name}) {
+            id
+            name
+            createdAt
+          }
+        } ''';
+      final graphQLVariables = {'name': 'Test Blog 1'};
+      final req = GraphQLRequest(
+          document: graphQLDocument, variables: graphQLVariables);
+
+      final operation = Amplify.API.mutate(request: req);
+      final res = await operation.value;
+
+      final expected = json.encode(_expectedMutateSuccessResponseBody['data']);
+
+      expect(res.data, equals(expected));
+      expect(res.errors, equals(null));
+    });
+  });
+  group('Model Helpers', () {
+    const blogSelectionSet =
+        'id name createdAt file { bucket region key meta { name } } files { bucket region key meta { name } } updatedAt';
+
+    test('Query returns proper response.data for Models', () async {
+      const expectedDoc =
+          'query getBlog(\$id: ID!) { getBlog(id: \$id) { $blogSelectionSet } }';
+      const decodePath = 'getBlog';
+
+      GraphQLRequest<Blog> req =
+          ModelQueries.get<Blog>(Blog.classType, _modelQueryId);
+
+      final operation = Amplify.API.query(request: req);
+      final res = await operation.value;
+
+      // request asserts
+      expect(req.document, expectedDoc);
+      expect(_deepEquals(req.variables, {'id': _modelQueryId}), isTrue);
+      expect(req.modelType, Blog.classType);
+      expect(req.decodePath, decodePath);
+      // response asserts
+      expect(res.data, isA<Blog>());
+      expect(res.data?.id, _modelQueryId);
+      expect(res.errors, equals(null));
+    });
+  });
+
+  group('Error Handling', () {
+    test('response errors are decoded', () async {
+      String graphQLDocument = ''' TestError ''';
+      final req = GraphQLRequest(document: graphQLDocument, variables: {});
+
+      final operation = Amplify.API.query(request: req);
+      final res = await operation.value;
+
+      const errorExpected = GraphQLResponseError(
+        message: _errorMessage,
+        locations: [
+          GraphQLResponseErrorLocation(2, 3),
+          GraphQLResponseErrorLocation(4, 5),
+        ],
+        path: <dynamic>[..._errorPath],
+        extensions: <String, dynamic>{..._errorExtensions},
+      );
+
+      expect(res.data, equals(null));
+      expect(res.errors?.single, equals(errorExpected));
+    });
+
+    test('canceled query request should never resolve', () async {
+      final req = GraphQLRequest(document: '', variables: {});
+      final operation = Amplify.API.query(request: req);
+      operation.cancel();
+      operation.then((p0) => fail('Request should have been cancelled.'));
+      await operation.valueOrCancellation();
+      expect(operation.isCanceled, isTrue);
+    });
+
+    test('canceled mutation request should never resolve', () async {
+      final req = GraphQLRequest(document: '', variables: {});
+      final operation = Amplify.API.mutate(request: req);
+      operation.cancel();
+      operation.then((p0) => fail('Request should have been cancelled.'));
+      await operation.valueOrCancellation();
+      expect(operation.isCanceled, isTrue);
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/graphql_error_test.dart b/packages/api/amplify_api/test/graphql_error_test.dart
index ee6588691a..32752299ee 100644
--- a/packages/api/amplify_api/test/graphql_error_test.dart
+++ b/packages/api/amplify_api/test/graphql_error_test.dart
@@ -68,6 +68,6 @@ void main() {
         .response;
 
     expect(resp.data, equals(null));
-    expect(resp.errors.single, equals(expected));
+    expect(resp.errors?.single, equals(expected));
   });
 }
diff --git a/packages/api/amplify_api/test/query_predicate_graphql_filter_test.dart b/packages/api/amplify_api/test/query_predicate_graphql_filter_test.dart
index acf8cf18a8..850fd5e1a4 100644
--- a/packages/api/amplify_api/test/query_predicate_graphql_filter_test.dart
+++ b/packages/api/amplify_api/test/query_predicate_graphql_filter_test.dart
@@ -1,3 +1,17 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 import 'package:amplify_api/src/graphql/graphql_request_factory.dart';
 import 'package:amplify_flutter/amplify_flutter.dart';
 import 'package:amplify_flutter/src/amplify_impl.dart';
diff --git a/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
index 7b8fd53be0..0b3c0dae01 100644
--- a/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
+++ b/packages/api/amplify_api/test/test_data/fake_amplify_configuration.dart
@@ -1,3 +1,17 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
 const amplifyconfig = '''{
   "UserAgent": "aws-amplify-cli/2.0",
   "Version": "1.0",

From 98891d77397517ca75c9846b4a1603afb6fda10c Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Tue, 19 Jul 2022 08:42:49 -0800
Subject: [PATCH 24/33] feat!(core,auth): auth providers definition and
 CognitoIamAuthProvider registers in Auth (#1851)

---
 .../amplify_flutter/lib/src/hybrid_impl.dart  |   3 +-
 packages/amplify_core/lib/amplify_core.dart   |   3 +
 .../lib/src/amplify_class_impl.dart           |   8 +-
 .../src/plugin/amplify_plugin_interface.dart  |   5 +-
 .../api/auth/api_authorization_type.dart      |  18 ++-
 .../types/common/amplify_auth_provider.dart   |  79 +++++++++++
 packages/amplify_core/pubspec.yaml            |   2 +-
 .../test/amplify_auth_provider_test.dart      | 132 ++++++++++++++++++
 .../amplify_api/lib/src/api_plugin_impl.dart  |   5 +-
 .../lib/src/auth_plugin_impl.dart             |  14 +-
 .../src/util/cognito_iam_auth_provider.dart   |  83 +++++++++++
 .../test/plugin/auth_providers_test.dart      | 112 +++++++++++++++
 .../test/plugin/delete_user_test.dart         |  17 ++-
 .../test/plugin/sign_out_test.dart            |  52 +++++--
 14 files changed, 507 insertions(+), 26 deletions(-)
 create mode 100644 packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
 create mode 100644 packages/amplify_core/test/amplify_auth_provider_test.dart
 create mode 100644 packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart
 create mode 100644 packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart

diff --git a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
index 5eb3f1257e..8c166f03f6 100644
--- a/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
+++ b/packages/amplify/amplify_flutter/lib/src/hybrid_impl.dart
@@ -36,7 +36,8 @@ class AmplifyHybridImpl extends AmplifyClassImpl {
       [
         ...API.plugins,
         ...Auth.plugins,
-      ].map((p) => p.configure(config: amplifyConfig)),
+      ].map((p) => p.configure(
+          config: amplifyConfig, authProviderRepo: authProviderRepo)),
       eagerError: true,
     );
     await _methodChannelAmplify.configurePlatform(config);
diff --git a/packages/amplify_core/lib/amplify_core.dart b/packages/amplify_core/lib/amplify_core.dart
index f8cb76f2bb..8086bd78fe 100644
--- a/packages/amplify_core/lib/amplify_core.dart
+++ b/packages/amplify_core/lib/amplify_core.dart
@@ -74,6 +74,9 @@ export 'src/types/api/api_types.dart';
 /// Auth
 export 'src/types/auth/auth_types.dart';
 
+/// Auth providers
+export 'src/types/common/amplify_auth_provider.dart';
+
 /// Datastore
 export 'src/types/datastore/datastore_types.dart' hide DateTimeParse;
 
diff --git a/packages/amplify_core/lib/src/amplify_class_impl.dart b/packages/amplify_core/lib/src/amplify_class_impl.dart
index d802d4a69d..00c9cba346 100644
--- a/packages/amplify_core/lib/src/amplify_class_impl.dart
+++ b/packages/amplify_core/lib/src/amplify_class_impl.dart
@@ -24,6 +24,11 @@ import 'package:meta/meta.dart';
 /// {@endtemplate}
 @internal
 class AmplifyClassImpl extends AmplifyClass {
+  /// Share AmplifyAuthProviders with plugins.
+  @protected
+  final AmplifyAuthProviderRepository authProviderRepo =
+      AmplifyAuthProviderRepository();
+
   /// {@macro amplify_flutter.amplify_class_impl}
   AmplifyClassImpl() : super.protected();
 
@@ -57,7 +62,8 @@ class AmplifyClassImpl extends AmplifyClass {
         ...Auth.plugins,
         ...DataStore.plugins,
         ...Storage.plugins,
-      ].map((p) => p.configure(config: amplifyConfig)),
+      ].map((p) => p.configure(
+          config: amplifyConfig, authProviderRepo: authProviderRepo)),
       eagerError: true,
     );
   }
diff --git a/packages/amplify_core/lib/src/plugin/amplify_plugin_interface.dart b/packages/amplify_core/lib/src/plugin/amplify_plugin_interface.dart
index 821c6fe38e..4ca5f7c2a1 100644
--- a/packages/amplify_core/lib/src/plugin/amplify_plugin_interface.dart
+++ b/packages/amplify_core/lib/src/plugin/amplify_plugin_interface.dart
@@ -30,7 +30,10 @@ abstract class AmplifyPluginInterface {
   Future<void> addPlugin() async {}
 
   /// Configures the plugin using the registered [config].
-  Future<void> configure({AmplifyConfig? config}) async {}
+  Future<void> configure({
+    AmplifyConfig? config,
+    required AmplifyAuthProviderRepository authProviderRepo,
+  }) async {}
 
   /// Resets the plugin by removing all traces of it from the device.
   @visibleForTesting
diff --git a/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart b/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
index e81ef856f4..f15da13b9f 100644
--- a/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
+++ b/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
@@ -13,6 +13,7 @@
  * permissions and limitations under the License.
  */
 
+import 'package:amplify_core/src/types/common/amplify_auth_provider.dart';
 import 'package:collection/collection.dart';
 import 'package:json_annotation/json_annotation.dart';
 
@@ -24,17 +25,17 @@ part 'api_authorization_type.g.dart';
 /// See also:
 /// - [AppSync Security](https://docs.aws.amazon.com/appsync/latest/devguide/security.html)
 @JsonEnum(alwaysCreate: true)
-enum APIAuthorizationType {
+enum APIAuthorizationType<T extends AmplifyAuthProvider> {
   /// For public APIs.
   @JsonValue('NONE')
-  none,
+  none(AmplifyAuthProviderToken<AmplifyAuthProvider>()),
 
   /// A hardcoded key which can provide throttling for an unauthenticated API.
   ///
   /// See also:
   /// - [API Key Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#api-key-authorization)
   @JsonValue('API_KEY')
-  apiKey,
+  apiKey(AmplifyAuthProviderToken<AmplifyAuthProvider>()),
 
   /// Use an IAM access/secret key credential pair to authorize access to an API.
   ///
@@ -42,7 +43,7 @@ enum APIAuthorizationType {
   /// - [IAM Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security.html#aws-iam-authorization)
   /// - [IAM Introduction](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)
   @JsonValue('AWS_IAM')
-  iam,
+  iam(AmplifyAuthProviderToken<AWSIamAmplifyAuthProvider>()),
 
   /// OpenID Connect is a simple identity layer on top of OAuth2.0.
   ///
@@ -50,21 +51,24 @@ enum APIAuthorizationType {
   /// - [OpenID Connect Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#openid-connect-authorization)
   /// - [OpenID Connect Specification](https://openid.net/specs/openid-connect-core-1_0.html)
   @JsonValue('OPENID_CONNECT')
-  oidc,
+  oidc(AmplifyAuthProviderToken<TokenAmplifyAuthProvider>()),
 
   /// Control access to date by putting users into different permissions pools.
   ///
   /// See also:
   /// - [Amazon Cognito User Pools](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#amazon-cognito-user-pools-authorization)
   @JsonValue('AMAZON_COGNITO_USER_POOLS')
-  userPools,
+  userPools(AmplifyAuthProviderToken<TokenAmplifyAuthProvider>()),
 
   /// Control access by calling a lambda function.
   ///
   /// See also:
   /// - [Introducing Lambda authorization for AWS AppSync GraphQL APIs](https://aws.amazon.com/blogs/mobile/appsync-lambda-auth/)
   @JsonValue('AWS_LAMBDA')
-  function
+  function(AmplifyAuthProviderToken<TokenAmplifyAuthProvider>());
+
+  const APIAuthorizationType(this.authProviderToken);
+  final AmplifyAuthProviderToken<T> authProviderToken;
 }
 
 /// Helper methods for [APIAuthorizationType].
diff --git a/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart b/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
new file mode 100644
index 0000000000..30c00ff053
--- /dev/null
+++ b/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:aws_signature_v4/aws_signature_v4.dart';
+
+/// An identifier to use as a key in an [AmplifyAuthProviderRepository] so that
+/// a retrieved auth provider can be typed more accurately.
+class AmplifyAuthProviderToken<T extends AmplifyAuthProvider> extends Token<T> {
+  const AmplifyAuthProviderToken();
+}
+
+abstract class AuthProviderOptions {
+  const AuthProviderOptions();
+}
+
+/// Options required by IAM to sign any given request at runtime.
+class IamAuthProviderOptions extends AuthProviderOptions {
+  final String region;
+  final AWSService service;
+
+  const IamAuthProviderOptions({required this.region, required this.service});
+}
+
+abstract class AmplifyAuthProvider {
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant AuthProviderOptions? options,
+  });
+}
+
+abstract class AWSIamAmplifyAuthProvider extends AmplifyAuthProvider
+    implements AWSCredentialsProvider {
+  @override
+  Future<AWSSignedRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant IamAuthProviderOptions options,
+  });
+}
+
+abstract class TokenAmplifyAuthProvider extends AmplifyAuthProvider {
+  Future<String> getLatestAuthToken();
+
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant AuthProviderOptions? options,
+  }) async {
+    final token = await getLatestAuthToken();
+    request.headers.putIfAbsent(AWSHeaders.authorization, () => token);
+    return request;
+  }
+}
+
+class AmplifyAuthProviderRepository {
+  final Map<AmplifyAuthProviderToken, AmplifyAuthProvider> _authProviders = {};
+
+  T? getAuthProvider<T extends AmplifyAuthProvider>(
+      AmplifyAuthProviderToken<T> token) {
+    return _authProviders[token] as T?;
+  }
+
+  void registerAuthProvider<T extends AmplifyAuthProvider>(
+      AmplifyAuthProviderToken<T> token, AmplifyAuthProvider authProvider) {
+    _authProviders[token] = authProvider;
+  }
+}
diff --git a/packages/amplify_core/pubspec.yaml b/packages/amplify_core/pubspec.yaml
index 60d959babd..f044a78d7e 100644
--- a/packages/amplify_core/pubspec.yaml
+++ b/packages/amplify_core/pubspec.yaml
@@ -13,7 +13,7 @@ dependencies:
   aws_common: ^0.2.0
   aws_signature_v4: ^0.2.0
   collection: ^1.15.0
-  http: ^0.13.0
+  http: ^0.13.4
   intl: ^0.17.0
   json_annotation: ^4.6.0
   logging: ^1.0.0
diff --git a/packages/amplify_core/test/amplify_auth_provider_test.dart b/packages/amplify_core/test/amplify_auth_provider_test.dart
new file mode 100644
index 0000000000..08a0e06e4d
--- /dev/null
+++ b/packages/amplify_core/test/amplify_auth_provider_test.dart
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:aws_signature_v4/aws_signature_v4.dart';
+import 'package:test/test.dart';
+
+const _testAuthKey = 'TestAuthKey';
+const _testToken = 'abc123-fake-token';
+
+AWSHttpRequest _generateTestRequest() {
+  return AWSHttpRequest(
+    method: AWSHttpMethod.get,
+    uri: Uri.parse('https://www.amazon.com'),
+  );
+}
+
+class TestAuthProvider extends AmplifyAuthProvider {
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant AuthProviderOptions? options,
+  }) async {
+    request.headers.putIfAbsent(_testAuthKey, () => 'foo');
+    return request;
+  }
+}
+
+class SecondTestAuthProvider extends AmplifyAuthProvider {
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant AuthProviderOptions? options,
+  }) async {
+    request.headers.putIfAbsent(_testAuthKey, () => 'bar');
+    return request;
+  }
+}
+
+class TestAWSCredentialsAuthProvider extends AWSIamAmplifyAuthProvider {
+  @override
+  Future<AWSCredentials> retrieve() async {
+    return const AWSCredentials(
+        'fake-access-key-123', 'fake-secret-access-key-456');
+  }
+
+  @override
+  Future<AWSSignedRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant IamAuthProviderOptions? options,
+  }) async {
+    request.headers.putIfAbsent(_testAuthKey, () => 'foo');
+    return request as AWSSignedRequest;
+  }
+}
+
+class TestTokenProvider extends TokenAmplifyAuthProvider {
+  @override
+  Future<String> getLatestAuthToken() async {
+    return _testToken;
+  }
+}
+
+void main() {
+  final authProvider = TestAuthProvider();
+
+  group('AmplifyAuthProvider', () {
+    test('can authorize an HTTP request', () async {
+      final authorizedRequest =
+          await authProvider.authorizeRequest(_generateTestRequest());
+      expect(authorizedRequest.headers[_testAuthKey], 'foo');
+    });
+  });
+
+  group('TokenAmplifyAuthProvider', () {
+    test('will assign the token to the "Authorization" header', () async {
+      final tokenAuthProvider = TestTokenProvider();
+      final authorizedRequest =
+          await tokenAuthProvider.authorizeRequest(_generateTestRequest());
+      expect(authorizedRequest.headers[AWSHeaders.authorization], _testToken);
+    });
+  });
+
+  group('AmplifyAuthProviderRepository', () {
+    test('can register a valid auth provider and use to retrieve', () async {
+      final authRepo = AmplifyAuthProviderRepository();
+
+      const providerKey = AmplifyAuthProviderToken();
+      authRepo.registerAuthProvider(providerKey, authProvider);
+      final actualAuthProvider = authRepo.getAuthProvider(providerKey);
+      final authorizedRequest =
+          await actualAuthProvider!.authorizeRequest(_generateTestRequest());
+      expect(authorizedRequest.headers[_testAuthKey], 'foo');
+    });
+
+    test('will correctly type the retrieved auth provider', () async {
+      final authRepo = AmplifyAuthProviderRepository();
+
+      final credentialAuthProvider = TestAWSCredentialsAuthProvider();
+      const providerKey = AmplifyAuthProviderToken<AWSIamAmplifyAuthProvider>();
+      authRepo.registerAuthProvider(providerKey, credentialAuthProvider);
+      AWSIamAmplifyAuthProvider? actualAuthProvider =
+          authRepo.getAuthProvider(providerKey);
+      expect(actualAuthProvider, isA<AWSIamAmplifyAuthProvider>());
+    });
+
+    test('will overwrite previous provider in same key', () async {
+      final authRepo = AmplifyAuthProviderRepository();
+
+      const providerKey = AmplifyAuthProviderToken();
+      authRepo.registerAuthProvider(providerKey, authProvider);
+      authRepo.registerAuthProvider(providerKey, SecondTestAuthProvider());
+      final actualAuthProvider = authRepo.getAuthProvider(providerKey);
+
+      final authorizedRequest =
+          await actualAuthProvider!.authorizeRequest(_generateTestRequest());
+      expect(authorizedRequest.headers[_testAuthKey], 'bar');
+    });
+  });
+}
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index a54ad5ee2b..a5dfd58ce6 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -54,7 +54,10 @@ class AmplifyAPIDart extends AmplifyAPI {
   }
 
   @override
-  Future<void> configure({AmplifyConfig? config}) async {
+  Future<void> configure({
+    AmplifyConfig? config,
+    required AmplifyAuthProviderRepository authProviderRepo,
+  }) async {
     final apiConfig = config?.api?.awsPlugin;
     if (apiConfig == null) {
       throw const ApiException('No AWS API config found',
diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
index 08a5f53f0e..1db9c30481 100644
--- a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
+++ b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
@@ -51,6 +51,7 @@ import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart
         VerifyUserAttributeRequest;
 import 'package:amplify_auth_cognito_dart/src/sdk/sdk_bridge.dart';
 import 'package:amplify_auth_cognito_dart/src/state/state.dart';
+import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:amplify_secure_storage_dart/amplify_secure_storage_dart.dart';
 import 'package:built_collection/built_collection.dart';
@@ -174,10 +175,21 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
   }
 
   @override
-  Future<void> configure({AmplifyConfig? config}) async {
+  Future<void> configure({
+    AmplifyConfig? config,
+    required AmplifyAuthProviderRepository authProviderRepo,
+  }) async {
     if (config == null) {
       throw const AuthException('No Cognito plugin config detected');
     }
+
+    // Register auth providers to provide auth functionality to other plugins
+    // without requiring other plugins to call `Amplify.Auth...` directly.
+    authProviderRepo.registerAuthProvider(
+      APIAuthorizationType.iam.authProviderToken,
+      CognitoIamAuthProvider(),
+    );
+
     if (_stateMachine.getOrCreate(AuthStateMachine.type).currentState.type !=
         AuthStateType.notConfigured) {
       throw const AmplifyAlreadyConfiguredException(
diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart
new file mode 100644
index 0000000000..b50be60932
--- /dev/null
+++ b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_iam_auth_provider.dart
@@ -0,0 +1,83 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:aws_signature_v4/aws_signature_v4.dart';
+import 'package:meta/meta.dart';
+
+/// [AmplifyAuthProvider] implementation that signs a request using AWS credentials
+/// from `Amplify.Auth.fetchAuthSession()` or allows getting credentials directly.
+@internal
+class CognitoIamAuthProvider extends AWSIamAmplifyAuthProvider {
+  /// AWS credentials from Auth category.
+  @override
+  Future<AWSCredentials> retrieve() async {
+    final authSession = await Amplify.Auth.fetchAuthSession(
+      options: const CognitoSessionOptions(getAWSCredentials: true),
+    ) as CognitoAuthSession;
+    final credentials = authSession.credentials;
+    if (credentials == null) {
+      throw const InvalidCredentialsException(
+        'Unable to authorize request with IAM. No AWS credentials.',
+      );
+    }
+    return credentials;
+  }
+
+  /// Signs request with AWSSigV4Signer and AWS credentials from `.getCredentials()`.
+  @override
+  Future<AWSSignedRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    IamAuthProviderOptions? options,
+  }) async {
+    if (options == null) {
+      throw const AuthException(
+        'Unable to authorize request with IAM. No region or service provided.',
+      );
+    }
+
+    return _signRequest(
+      request,
+      region: options.region,
+      service: options.service,
+      credentials: await retrieve(),
+    );
+  }
+
+  /// Takes input [request] as canonical request and generates a signed version.
+  Future<AWSSignedRequest> _signRequest(
+    AWSBaseHttpRequest request, {
+    required String region,
+    required AWSService service,
+    required AWSCredentials credentials,
+  }) {
+    // Create signer helper params.
+    final signer = AWSSigV4Signer(
+      credentialsProvider: AWSCredentialsProvider(credentials),
+    );
+    final scope = AWSCredentialScope(
+      region: region,
+      service: service,
+    );
+
+    // Finally, create and sign canonical request.
+    return signer.sign(
+      request,
+      credentialScope: scope,
+    );
+  }
+}
diff --git a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
new file mode 100644
index 0000000000..acb126fa66
--- /dev/null
+++ b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
@@ -0,0 +1,112 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import 'dart:async';
+
+import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart'
+    hide InternalErrorException;
+import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:test/test.dart';
+
+import '../common/mock_config.dart';
+import '../common/mock_secure_storage.dart';
+
+AWSHttpRequest _generateTestRequest() {
+  return AWSHttpRequest(
+    method: AWSHttpMethod.get,
+    uri: Uri.parse('https://www.amazon.com'),
+  );
+}
+
+/// Returns dummy AWS credentials.
+class TestAmplifyAuth extends AmplifyAuthCognitoDart {
+  @override
+  Future<AuthSession> fetchAuthSession({
+    required AuthSessionRequest request,
+  }) async {
+    return const CognitoAuthSession(
+      isSignedIn: true,
+      credentials: AWSCredentials('fakeKeyId', 'fakeSecret'),
+    );
+  }
+}
+
+void main() {
+  group(
+      'AmplifyAuthCognitoDart plugin registers auth providers during configuration',
+      () {
+    late AmplifyAuthCognitoDart plugin;
+
+    setUp(() async {
+      plugin = AmplifyAuthCognitoDart(credentialStorage: MockSecureStorage());
+    });
+
+    test('registers CognitoIamAuthProvider', () async {
+      final testAuthRepo = AmplifyAuthProviderRepository();
+      await plugin.configure(
+        config: mockConfig,
+        authProviderRepo: testAuthRepo,
+      );
+      final authProvider = testAuthRepo.getAuthProvider(
+        APIAuthorizationType.iam.authProviderToken,
+      );
+      expect(authProvider, isA<CognitoIamAuthProvider>());
+    });
+  });
+
+  group('CognitoIamAuthProvider', () {
+    setUpAll(() async {
+      await Amplify.addPlugin(TestAmplifyAuth());
+    });
+
+    test('gets AWS credentials from Amplify.Auth.fetchAuthSession', () async {
+      final authProvider = CognitoIamAuthProvider();
+      final credentials = await authProvider.retrieve();
+      expect(credentials.accessKeyId, isA<String>());
+      expect(credentials.secretAccessKey, isA<String>());
+    });
+
+    test('signs a request when calling authorizeRequest', () async {
+      final authProvider = CognitoIamAuthProvider();
+      final authorizedRequest = await authProvider.authorizeRequest(
+        _generateTestRequest(),
+        options: const IamAuthProviderOptions(
+          region: 'us-east-1',
+          service: AWSService.appSync,
+        ),
+      );
+      // Note: not intended to be complete test of sigv4 algorithm.
+      expect(authorizedRequest.headers[AWSHeaders.authorization], isNotEmpty);
+      const userAgentHeader =
+          zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+      expect(
+        authorizedRequest.headers[AWSHeaders.host],
+        isNotEmpty,
+        skip: zIsWeb,
+      );
+      expect(
+        authorizedRequest.headers[userAgentHeader],
+        contains('aws-sigv4'),
+      );
+    });
+
+    test('throws when no options provided', () async {
+      final authProvider = CognitoIamAuthProvider();
+      await expectLater(
+        authProvider.authorizeRequest(_generateTestRequest()),
+        throwsA(isA<AuthException>()),
+      );
+    });
+  });
+}
diff --git a/packages/auth/amplify_auth_cognito_test/test/plugin/delete_user_test.dart b/packages/auth/amplify_auth_cognito_test/test/plugin/delete_user_test.dart
index b589b6b110..15e08de206 100644
--- a/packages/auth/amplify_auth_cognito_test/test/plugin/delete_user_test.dart
+++ b/packages/auth/amplify_auth_cognito_test/test/plugin/delete_user_test.dart
@@ -58,6 +58,8 @@ void main() {
   late StreamController<AuthHubEvent> hubEventsController;
   late Stream<AuthHubEvent> hubEvents;
 
+  final testAuthRepo = AmplifyAuthProviderRepository();
+
   final userDeletedEvent = isA<AuthHubEvent>().having(
     (event) => event.type,
     'type',
@@ -83,7 +85,10 @@ void main() {
 
     group('deleteUser', () {
       test('throws when signed out', () async {
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
         await expectLater(plugin.deleteUser(), throwsSignedOutException);
 
         expect(hubEvents, neverEmits(userDeletedEvent));
@@ -96,7 +101,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(() async {});
         stateMachine.addInstance<CognitoIdentityProviderClient>(mockIdp);
@@ -113,7 +121,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(() async {
           throw InternalErrorException();
diff --git a/packages/auth/amplify_auth_cognito_test/test/plugin/sign_out_test.dart b/packages/auth/amplify_auth_cognito_test/test/plugin/sign_out_test.dart
index 6c9f3fe3a2..fe14fc98be 100644
--- a/packages/auth/amplify_auth_cognito_test/test/plugin/sign_out_test.dart
+++ b/packages/auth/amplify_auth_cognito_test/test/plugin/sign_out_test.dart
@@ -69,6 +69,8 @@ void main() {
   late StreamController<AuthHubEvent> hubEventsController;
   late Stream<AuthHubEvent> hubEvents;
 
+  final testAuthRepo = AmplifyAuthProviderRepository();
+
   final emitsSignOutEvent = emitsThrough(
     isA<AuthHubEvent>().having(
       (event) => event.type,
@@ -112,14 +114,20 @@ void main() {
 
     group('signOut', () {
       test('completes when already signed out', () async {
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
         expect(plugin.signOut(), completes);
         expect(hubEvents, emitsSignOutEvent);
       });
 
       test('does not clear AWS creds when already signed out', () async {
         seedStorage(secureStorage, identityPoolKeys: identityPoolKeys);
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
         await expectLater(plugin.signOut(), completes);
         expect(hubEvents, emitsSignOutEvent);
 
@@ -144,7 +152,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(
           globalSignOut: () async => GlobalSignOutResponse(),
@@ -165,7 +176,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(
           globalSignOut:
@@ -194,7 +208,10 @@ void main() {
           userPoolKeys: userPoolKeys,
           identityPoolKeys: identityPoolKeys,
         );
-        await plugin.configure(config: mockConfig);
+        await plugin.configure(
+          config: mockConfig,
+          authProviderRepo: testAuthRepo,
+        );
 
         final mockIdp = MockCognitoIdpClient(
           globalSignOut: () async => GlobalSignOutResponse(),
@@ -217,7 +234,10 @@ void main() {
 
       test('can sign out in user pool-only mode', () async {
         seedStorage(secureStorage, userPoolKeys: userPoolKeys);
-        await plugin.configure(config: userPoolOnlyConfig);
+        await plugin.configure(
+          config: userPoolOnlyConfig,
+          authProviderRepo: testAuthRepo,
+        );
         expect(plugin.signOut(), completes);
       });
 
@@ -229,7 +249,10 @@ void main() {
             identityPoolKeys: identityPoolKeys,
             hostedUiKeys: hostedUiKeys,
           );
-          await plugin.configure(config: mockConfig);
+          await plugin.configure(
+            config: mockConfig,
+            authProviderRepo: testAuthRepo,
+          );
 
           final mockIdp = MockCognitoIdpClient(
             globalSignOut: () async => GlobalSignOutResponse(),
@@ -250,7 +273,10 @@ void main() {
             identityPoolKeys: identityPoolKeys,
             hostedUiKeys: hostedUiKeys,
           );
-          await plugin.configure(config: mockConfig);
+          await plugin.configure(
+            config: mockConfig,
+            authProviderRepo: testAuthRepo,
+          );
 
           final mockIdp = MockCognitoIdpClient(
             globalSignOut:
@@ -279,7 +305,10 @@ void main() {
             identityPoolKeys: identityPoolKeys,
             hostedUiKeys: hostedUiKeys,
           );
-          await plugin.configure(config: mockConfig);
+          await plugin.configure(
+            config: mockConfig,
+            authProviderRepo: testAuthRepo,
+          );
 
           final mockIdp = MockCognitoIdpClient(
             globalSignOut: () async => GlobalSignOutResponse(),
@@ -321,7 +350,10 @@ void main() {
             ),
             HostedUiPlatform.token,
           );
-          await plugin.configure(config: mockConfig);
+          await plugin.configure(
+            config: mockConfig,
+            authProviderRepo: testAuthRepo,
+          );
 
           await expectLater(plugin.getUserPoolTokens(), completes);
           await expectLater(

From d0a254e2e325d115f85bf90efeb2a1eb7f6cf492 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Thu, 21 Jul 2022 12:50:37 -0800
Subject: [PATCH 25/33] feat(core,api): IAM auth mode for HTTP requests (REST
 and GQL) (#1893)

---
 .../api/auth/api_authorization_type.dart      |   2 +-
 .../types/common/amplify_auth_provider.dart   |  14 ++
 .../amplify_authorization_rest_client.dart    |  30 ++--
 .../amplify_api/lib/src/api_plugin_impl.dart  |  64 ++++---
 .../decorators/authorize_http_request.dart    | 110 ++++++++++++
 .../app_sync_api_key_auth_provider.dart       |  38 +++++
 packages/api/amplify_api/pubspec.yaml         |   1 +
 .../test/amplify_dart_rest_methods_test.dart  |   5 +-
 .../test/authorize_http_request_test.dart     | 159 ++++++++++++++++++
 .../amplify_api/test/dart_graphql_test.dart   |   2 +-
 .../test/plugin_configuration_test.dart       | 112 ++++++++++++
 packages/api/amplify_api/test/util.dart       |  53 ++++++
 12 files changed, 538 insertions(+), 52 deletions(-)
 create mode 100644 packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
 create mode 100644 packages/api/amplify_api/lib/src/graphql/app_sync_api_key_auth_provider.dart
 create mode 100644 packages/api/amplify_api/test/authorize_http_request_test.dart
 create mode 100644 packages/api/amplify_api/test/plugin_configuration_test.dart
 create mode 100644 packages/api/amplify_api/test/util.dart

diff --git a/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart b/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
index f15da13b9f..95b73a4cac 100644
--- a/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
+++ b/packages/amplify_core/lib/src/types/api/auth/api_authorization_type.dart
@@ -35,7 +35,7 @@ enum APIAuthorizationType<T extends AmplifyAuthProvider> {
   /// See also:
   /// - [API Key Authorization](https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#api-key-authorization)
   @JsonValue('API_KEY')
-  apiKey(AmplifyAuthProviderToken<AmplifyAuthProvider>()),
+  apiKey(AmplifyAuthProviderToken<ApiKeyAmplifyAuthProvider>()),
 
   /// Use an IAM access/secret key credential pair to authorize access to an API.
   ///
diff --git a/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart b/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
index 30c00ff053..16707d6afd 100644
--- a/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
+++ b/packages/amplify_core/lib/src/types/common/amplify_auth_provider.dart
@@ -34,6 +34,12 @@ class IamAuthProviderOptions extends AuthProviderOptions {
   const IamAuthProviderOptions({required this.region, required this.service});
 }
 
+class ApiKeyAuthProviderOptions extends AuthProviderOptions {
+  final String apiKey;
+
+  const ApiKeyAuthProviderOptions(this.apiKey);
+}
+
 abstract class AmplifyAuthProvider {
   Future<AWSBaseHttpRequest> authorizeRequest(
     AWSBaseHttpRequest request, {
@@ -50,6 +56,14 @@ abstract class AWSIamAmplifyAuthProvider extends AmplifyAuthProvider
   });
 }
 
+abstract class ApiKeyAmplifyAuthProvider extends AmplifyAuthProvider {
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    covariant ApiKeyAuthProviderOptions? options,
+  });
+}
+
 abstract class TokenAmplifyAuthProvider extends AmplifyAuthProvider {
   Future<String> getLatestAuthToken();
 
diff --git a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
index 8a2d0678b5..a0b7aece44 100644
--- a/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
+++ b/packages/api/amplify_api/lib/src/amplify_authorization_rest_client.dart
@@ -18,15 +18,19 @@ import 'package:amplify_core/amplify_core.dart';
 import 'package:http/http.dart' as http;
 import 'package:meta/meta.dart';
 
-const _xApiKey = 'X-Api-Key';
+import 'decorators/authorize_http_request.dart';
 
 /// Implementation of http [http.Client] that authorizes HTTP requests with
 /// Amplify.
 @internal
 class AmplifyAuthorizationRestClient extends http.BaseClient
     implements Closeable {
+  /// [AmplifyAuthProviderRepository] for any auth modes this client may use.
+  final AmplifyAuthProviderRepository authProviderRepo;
+
   /// Determines how requests with this client are authorized.
   final AWSApiConfig endpointConfig;
+
   final http.Client _baseClient;
   final bool _useDefaultBaseClient;
 
@@ -34,6 +38,7 @@ class AmplifyAuthorizationRestClient extends http.BaseClient
   /// client are authorized.
   AmplifyAuthorizationRestClient({
     required this.endpointConfig,
+    required this.authProviderRepo,
     http.Client? baseClient,
   })  : _useDefaultBaseClient = baseClient == null,
         _baseClient = baseClient ?? http.Client();
@@ -42,27 +47,14 @@ class AmplifyAuthorizationRestClient extends http.BaseClient
   /// header already set.
   @override
   Future<http.StreamedResponse> send(http.BaseRequest request) async =>
-      _baseClient.send(_authorizeRequest(request));
+      _baseClient.send(await authorizeHttpRequest(
+        request,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      ));
 
   @override
   void close() {
     if (_useDefaultBaseClient) _baseClient.close();
   }
-
-  http.BaseRequest _authorizeRequest(http.BaseRequest request) {
-    if (!request.headers.containsKey(AWSHeaders.authorization) &&
-        endpointConfig.authorizationType != APIAuthorizationType.none) {
-      // TODO(ragingsquirrel3): Use auth providers from core to transform the request.
-      final apiKey = endpointConfig.apiKey;
-      if (endpointConfig.authorizationType == APIAuthorizationType.apiKey) {
-        if (apiKey == null) {
-          throw const ApiException(
-              'Auth mode is API Key, but no API Key was found in config.');
-        }
-
-        request.headers.putIfAbsent(_xApiKey, () => apiKey);
-      }
-    }
-    return request;
-  }
 }
diff --git a/packages/api/amplify_api/lib/src/api_plugin_impl.dart b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
index a5dfd58ce6..e353c70a31 100644
--- a/packages/api/amplify_api/lib/src/api_plugin_impl.dart
+++ b/packages/api/amplify_api/lib/src/api_plugin_impl.dart
@@ -26,6 +26,7 @@ import 'package:meta/meta.dart';
 
 import 'amplify_api_config.dart';
 import 'amplify_authorization_rest_client.dart';
+import 'graphql/app_sync_api_key_auth_provider.dart';
 import 'graphql/send_graphql_request.dart';
 import 'util.dart';
 
@@ -35,10 +36,11 @@ import 'util.dart';
 class AmplifyAPIDart extends AmplifyAPI {
   late final AWSApiPluginConfig _apiConfig;
   final http.Client? _baseHttpClient;
+  late final AmplifyAuthProviderRepository _authProviderRepo;
 
   /// A map of the keys from the Amplify API config to HTTP clients to use for
   /// requests to that endpoint.
-  final Map<String, AmplifyAuthorizationRestClient> _clientPool = {};
+  final Map<String, http.Client> _clientPool = {};
 
   /// The registered [APIAuthProvider] instances.
   final Map<APIAuthorizationType, APIAuthProvider> _authProviders = {};
@@ -65,6 +67,21 @@ class AmplifyAPIDart extends AmplifyAPI {
               'https://docs.amplify.aws/lib/graphqlapi/getting-started/q/platform/flutter/#configure-api');
     }
     _apiConfig = apiConfig;
+    _authProviderRepo = authProviderRepo;
+    _registerApiPluginAuthProviders();
+  }
+
+  /// If an endpoint has an API key, ensure valid auth provider registered.
+  void _registerApiPluginAuthProviders() {
+    _apiConfig.endpoints.forEach((key, value) {
+      // Check the presence of apiKey (not auth type) because other modes might
+      // have a key if not the primary auth mode.
+      if (value.apiKey != null) {
+        _authProviderRepo.registerAuthProvider(
+            value.authorizationType.authProviderToken,
+            AppSyncApiKeyAuthProvider());
+      }
+    });
   }
 
   @override
@@ -89,32 +106,21 @@ class AmplifyAPIDart extends AmplifyAPI {
     }
   }
 
-  /// Returns the HTTP client to be used for GraphQL operations.
+  /// Returns the HTTP client to be used for REST/GraphQL operations.
   ///
-  /// Use [apiName] if there are multiple GraphQL endpoints.
+  /// Use [apiName] if there are multiple endpoints of the same type.
   @visibleForTesting
-  http.Client getGraphQLClient({String? apiName}) {
+  http.Client getHttpClient(EndpointType type, {String? apiName}) {
     final endpoint = _apiConfig.getEndpoint(
-      type: EndpointType.graphQL,
+      type: type,
       apiName: apiName,
     );
-    return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
-        endpointConfig: endpoint.config, baseClient: _baseHttpClient);
-  }
-
-  /// Returns the HTTP client to be used for REST operations.
-  ///
-  /// Use [apiName] if there are multiple REST endpoints.
-  @visibleForTesting
-  http.Client getRestClient({String? apiName}) {
-    final endpoint = _apiConfig.getEndpoint(
-      type: EndpointType.rest,
-      apiName: apiName,
-    );
-    return _clientPool[endpoint.name] ??= AmplifyAuthorizationRestClient(
+    return _clientPool[endpoint.name] ??= AmplifyHttpClient(
+        baseClient: AmplifyAuthorizationRestClient(
       endpointConfig: endpoint.config,
       baseClient: _baseHttpClient,
-    );
+      authProviderRepo: _authProviderRepo,
+    ));
   }
 
   Uri _getGraphQLUri(String? apiName) {
@@ -160,7 +166,8 @@ class AmplifyAPIDart extends AmplifyAPI {
   @override
   CancelableOperation<GraphQLResponse<T>> query<T>(
       {required GraphQLRequest<T> request}) {
-    final graphQLClient = getGraphQLClient(apiName: request.apiName);
+    final graphQLClient =
+        getHttpClient(EndpointType.graphQL, apiName: request.apiName);
     final uri = _getGraphQLUri(request.apiName);
 
     final responseFuture = sendGraphQLRequest<T>(
@@ -171,7 +178,8 @@ class AmplifyAPIDart extends AmplifyAPI {
   @override
   CancelableOperation<GraphQLResponse<T>> mutate<T>(
       {required GraphQLRequest<T> request}) {
-    final graphQLClient = getGraphQLClient(apiName: request.apiName);
+    final graphQLClient =
+        getHttpClient(EndpointType.graphQL, apiName: request.apiName);
     final uri = _getGraphQLUri(request.apiName);
 
     final responseFuture = sendGraphQLRequest<T>(
@@ -190,7 +198,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(AWSStreamedHttpRequest.delete(
       uri,
       body: body ?? HttpPayload.empty(),
@@ -206,7 +214,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSHttpRequest.get(
         uri,
@@ -223,7 +231,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSHttpRequest.head(
         uri,
@@ -241,7 +249,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSStreamedHttpRequest.patch(
         uri,
@@ -260,7 +268,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSStreamedHttpRequest.post(
         uri,
@@ -279,7 +287,7 @@ class AmplifyAPIDart extends AmplifyAPI {
     String? apiName,
   }) {
     final uri = _getRestUri(path, apiName, queryParameters);
-    final client = getRestClient(apiName: apiName);
+    final client = getHttpClient(EndpointType.rest, apiName: apiName);
     return _prepareRestResponse(
       AWSStreamedHttpRequest.put(
         uri,
diff --git a/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
new file mode 100644
index 0000000000..3cab4d7443
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
@@ -0,0 +1,110 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+/// Transforms an HTTP request according to auth providers that match the endpoint
+/// configuration.
+@internal
+Future<http.BaseRequest> authorizeHttpRequest(http.BaseRequest request,
+    {required AWSApiConfig endpointConfig,
+    required AmplifyAuthProviderRepository authProviderRepo}) async {
+  if (request.headers.containsKey(AWSHeaders.authorization)) {
+    return request;
+  }
+  final authType = endpointConfig.authorizationType;
+
+  switch (authType) {
+    case APIAuthorizationType.apiKey:
+      final authProvider = _validateAuthProvider(
+          authProviderRepo
+              .getAuthProvider(APIAuthorizationType.apiKey.authProviderToken),
+          authType);
+      final apiKey = endpointConfig.apiKey;
+      if (apiKey == null) {
+        throw const ApiException(
+            'Auth mode is API Key, but no API Key was found in config.');
+      }
+
+      final authorizedRequest = await authProvider.authorizeRequest(
+          _httpToAWSRequest(request),
+          options: ApiKeyAuthProviderOptions(apiKey));
+      return authorizedRequest.httpRequest;
+    case APIAuthorizationType.iam:
+      final authProvider = _validateAuthProvider(
+          authProviderRepo
+              .getAuthProvider(APIAuthorizationType.iam.authProviderToken),
+          authType);
+      final service = endpointConfig.endpointType == EndpointType.graphQL
+          ? AWSService.appSync
+          : AWSService.apiGatewayManagementApi; // resolves to "execute-api"
+
+      final authorizedRequest = await authProvider.authorizeRequest(
+        _httpToAWSRequest(request),
+        options: IamAuthProviderOptions(
+          region: endpointConfig.region,
+          service: service,
+        ),
+      );
+      return authorizedRequest.httpRequest;
+    case APIAuthorizationType.function:
+    case APIAuthorizationType.oidc:
+    case APIAuthorizationType.userPools:
+      throw UnimplementedError('${authType.name} not implemented.');
+    case APIAuthorizationType.none:
+      return request;
+  }
+}
+
+T _validateAuthProvider<T extends AmplifyAuthProvider>(
+    T? authProvider, APIAuthorizationType authType) {
+  if (authProvider == null) {
+    throw ApiException('No auth provider found for auth mode ${authType.name}.',
+        recoverySuggestion: 'Ensure auth plugin correctly configured.');
+  }
+  return authProvider;
+}
+
+AWSBaseHttpRequest _httpToAWSRequest(http.BaseRequest request) {
+  final method = AWSHttpMethod.fromString(request.method);
+  if (request is http.Request) {
+    return AWSHttpRequest(
+      method: method,
+      uri: request.url,
+      headers: {
+        AWSHeaders.contentType: 'application/x-amz-json-1.1',
+        ...request.headers,
+      },
+      body: request.bodyBytes,
+    );
+  } else if (request is http.StreamedRequest) {
+    return AWSStreamedHttpRequest(
+      method: method,
+      uri: request.url,
+      headers: {
+        AWSHeaders.contentType: 'application/x-amz-json-1.1',
+        ...request.headers,
+      },
+      body: request.finalize(),
+    );
+  } else {
+    throw UnimplementedError(
+      'Multipart HTTP requests are not supported.',
+    );
+  }
+}
diff --git a/packages/api/amplify_api/lib/src/graphql/app_sync_api_key_auth_provider.dart b/packages/api/amplify_api/lib/src/graphql/app_sync_api_key_auth_provider.dart
new file mode 100644
index 0000000000..bdafe6dbed
--- /dev/null
+++ b/packages/api/amplify_api/lib/src/graphql/app_sync_api_key_auth_provider.dart
@@ -0,0 +1,38 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:meta/meta.dart';
+
+/// "X-Api-Key", key used for API key header in API key auth mode.
+const xApiKey = 'X-Api-Key';
+
+/// [AmplifyAuthProvider] implementation that puts an API key in the header.
+@internal
+class AppSyncApiKeyAuthProvider extends ApiKeyAmplifyAuthProvider {
+  @override
+  Future<AWSBaseHttpRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    ApiKeyAuthProviderOptions? options,
+  }) async {
+    if (options == null) {
+      throw const ApiException(
+          'Called API key auth provider without passing a valid API key.');
+    }
+    request.headers.putIfAbsent(xApiKey, () => options.apiKey);
+    return request;
+  }
+}
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index c82797fc29..a4b2121efe 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -29,6 +29,7 @@ dev_dependencies:
     path: ../../amplify_lints
   amplify_test:
     path: ../../amplify_test
+  aws_signature_v4: ^0.1.0 
   build_runner: ^2.0.0
   flutter_test:
     sdk: flutter
diff --git a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
index d8c5162377..8469354830 100644
--- a/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
+++ b/packages/api/amplify_api/test/amplify_dart_rest_methods_test.dart
@@ -11,8 +11,6 @@
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
 // limitations under the License.
-import 'dart:convert';
-
 import 'package:amplify_api/amplify_api.dart';
 import 'package:amplify_api/src/api_plugin_impl.dart';
 import 'package:amplify_core/amplify_core.dart';
@@ -28,7 +26,8 @@ const _pathThatShouldFail = 'notHere';
 
 class MockAmplifyAPI extends AmplifyAPIDart {
   @override
-  http.Client getRestClient({String? apiName}) => MockClient((request) async {
+  http.Client getHttpClient(EndpointType type, {String? apiName}) =>
+      MockClient((request) async {
         if (request.body.isNotEmpty) {
           expect(request.headers['Content-Type'], 'application/json');
         }
diff --git a/packages/api/amplify_api/test/authorize_http_request_test.dart b/packages/api/amplify_api/test/authorize_http_request_test.dart
new file mode 100644
index 0000000000..2179a07ad8
--- /dev/null
+++ b/packages/api/amplify_api/test/authorize_http_request_test.dart
@@ -0,0 +1,159 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License").
+// You may not use this file except in compliance with the License.
+// A copy of the License is located at
+//
+//  http://aws.amazon.com/apache2.0
+//
+// or in the "license" file accompanying this file. This file is distributed
+// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+// express or implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+import 'package:amplify_api/src/decorators/authorize_http_request.dart';
+import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'util.dart';
+
+const _region = 'us-east-1';
+const _gqlEndpoint =
+    'https://abc123.appsync-api.$_region.amazonaws.com/graphql';
+const _restEndpoint = 'https://xyz456.execute-api.$_region.amazonaws.com/test';
+
+http.Request _generateTestRequest(String url) {
+  return http.Request('GET', Uri.parse(url));
+}
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  final authProviderRepo = AmplifyAuthProviderRepository();
+
+  setUpAll(() {
+    authProviderRepo.registerAuthProvider(
+        APIAuthorizationType.apiKey.authProviderToken,
+        AppSyncApiKeyAuthProvider());
+    authProviderRepo.registerAuthProvider(
+        APIAuthorizationType.iam.authProviderToken, TestIamAuthProvider());
+  });
+
+  group('authorizeHttpRequest', () {
+    test('no-op for auth mode NONE', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.none,
+          endpoint: _restEndpoint,
+          endpointType: EndpointType.rest,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      expect(authorizedRequest.headers.containsKey(AWSHeaders.authorization),
+          isFalse);
+      expect(authorizedRequest, inputRequest);
+    });
+
+    test('no-op for request with Authorization header already set', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.userPools,
+          endpoint: _restEndpoint,
+          endpointType: EndpointType.rest,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      const testAuthValue = 'foo';
+      inputRequest.headers
+          .putIfAbsent(AWSHeaders.authorization, () => testAuthValue);
+
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      expect(
+          authorizedRequest.headers[AWSHeaders.authorization], testAuthValue);
+      expect(authorizedRequest, inputRequest);
+    });
+
+    test('authorizes request with IAM auth provider', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.iam,
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      validateSignedRequest(authorizedRequest);
+    });
+
+    test('authorizes request with API key', () async {
+      const testApiKey = 'abc-123-fake-key';
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.apiKey,
+          apiKey: testApiKey,
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      expect(
+        authorizedRequest.headers[xApiKey],
+        testApiKey,
+      );
+    });
+
+    test('throws when API key not in config', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.apiKey,
+          // no apiKey value provided
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      expectLater(
+          authorizeHttpRequest(
+            inputRequest,
+            endpointConfig: endpointConfig,
+            authProviderRepo: authProviderRepo,
+          ),
+          throwsA(isA<ApiException>()));
+    });
+
+    test('authorizes with Cognito User Pools auth mode', () {}, skip: true);
+
+    test('authorizes with OIDC auth mode', () {}, skip: true);
+
+    test('authorizes with lambda auth mode', () {}, skip: true);
+
+    test('throws when no auth provider found', () async {
+      final emptyAuthRepo = AmplifyAuthProviderRepository();
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.apiKey,
+          apiKey: 'abc-123-fake-key',
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      expectLater(
+          authorizeHttpRequest(
+            inputRequest,
+            endpointConfig: endpointConfig,
+            authProviderRepo: emptyAuthRepo,
+          ),
+          throwsA(isA<ApiException>()));
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/dart_graphql_test.dart b/packages/api/amplify_api/test/dart_graphql_test.dart
index bedd0092f2..4d9d8ec47f 100644
--- a/packages/api/amplify_api/test/dart_graphql_test.dart
+++ b/packages/api/amplify_api/test/dart_graphql_test.dart
@@ -91,7 +91,7 @@ class MockAmplifyAPI extends AmplifyAPIDart {
   }) : super(modelProvider: modelProvider);
 
   @override
-  http.Client getGraphQLClient({String? apiName}) =>
+  http.Client getHttpClient(EndpointType type, {String? apiName}) =>
       MockClient((request) async {
         if (request.body.contains('getBlog')) {
           return http.Response(json.encode(_expectedModelQueryResult), 200);
diff --git a/packages/api/amplify_api/test/plugin_configuration_test.dart b/packages/api/amplify_api/test/plugin_configuration_test.dart
new file mode 100644
index 0000000000..fcf3692114
--- /dev/null
+++ b/packages/api/amplify_api/test/plugin_configuration_test.dart
@@ -0,0 +1,112 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+import 'dart:convert';
+
+import 'package:amplify_api/src/api_plugin_impl.dart';
+import 'package:amplify_api/src/graphql/app_sync_api_key_auth_provider.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+import 'package:http/testing.dart';
+
+import 'test_data/fake_amplify_configuration.dart';
+import 'util.dart';
+
+const _expectedQuerySuccessResponseBody = {
+  'data': {
+    'listBlogs': {
+      'items': [
+        {
+          'id': 'TEST_ID',
+          'name': 'Test App Blog',
+          'createdAt': '2022-06-28T17:36:52.460Z'
+        }
+      ]
+    }
+  }
+};
+
+/// Asserts user agent and API key present.
+final _mockGqlClient = MockClient((request) async {
+  const userAgentHeader =
+      zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+  expect(request.headers[userAgentHeader], contains('amplify-flutter'));
+  expect(request.headers[xApiKey], isA<String>());
+  return http.Response(json.encode(_expectedQuerySuccessResponseBody), 200);
+});
+
+/// Asserts user agent and signed.
+final _mockRestClient = MockClient((request) async {
+  const userAgentHeader =
+      zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+  expect(request.headers[userAgentHeader], contains('amplify-flutter'));
+  validateSignedRequest(request);
+  return http.Response('"Hello from Lambda!"', 200);
+});
+
+void main() {
+  TestWidgetsFlutterBinding.ensureInitialized();
+
+  final authProviderRepo = AmplifyAuthProviderRepository();
+  authProviderRepo.registerAuthProvider(
+      APIAuthorizationType.iam.authProviderToken, TestIamAuthProvider());
+  final config =
+      AmplifyConfig.fromJson(jsonDecode(amplifyconfig) as Map<String, Object?>);
+
+  group('AmplifyAPI plugin configuration', () {
+    test(
+        'should register an API key auth provider when the configuration has an API key',
+        () async {
+      final plugin = AmplifyAPIDart();
+      await plugin.configure(
+          authProviderRepo: authProviderRepo, config: config);
+      final apiKeyAuthProvider = authProviderRepo
+          .getAuthProvider(APIAuthorizationType.apiKey.authProviderToken);
+      expect(apiKeyAuthProvider, isA<AppSyncApiKeyAuthProvider>());
+    });
+
+    test(
+        'should configure an HTTP client for GraphQL that authorizes with auth providers and adds user-agent',
+        () async {
+      final plugin = AmplifyAPIDart(baseHttpClient: _mockGqlClient);
+      await plugin.configure(
+          authProviderRepo: authProviderRepo, config: config);
+
+      String graphQLDocument = '''query TestQuery {
+          listBlogs {
+            items {
+              id
+              name
+              createdAt
+            }
+          }
+        }''';
+      final request =
+          GraphQLRequest<String>(document: graphQLDocument, variables: {});
+      await plugin.query(request: request).value;
+      // no assertion here because assertion implemented in mock HTTP client
+    });
+
+    test(
+        'should configure an HTTP client for REST that authorizes with auth providers and adds user-agent',
+        () async {
+      final plugin = AmplifyAPIDart(baseHttpClient: _mockRestClient);
+      await plugin.configure(
+          authProviderRepo: authProviderRepo, config: config);
+
+      await plugin.get('/items').value;
+      // no assertion here because assertion implemented in mock HTTP client
+    });
+  });
+}
diff --git a/packages/api/amplify_api/test/util.dart b/packages/api/amplify_api/test/util.dart
new file mode 100644
index 0000000000..f3c2ef551e
--- /dev/null
+++ b/packages/api/amplify_api/test/util.dart
@@ -0,0 +1,53 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'package:amplify_core/amplify_core.dart';
+import 'package:aws_signature_v4/aws_signature_v4.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:http/http.dart' as http;
+
+class TestIamAuthProvider extends AWSIamAmplifyAuthProvider {
+  @override
+  Future<AWSCredentials> retrieve() async {
+    return const AWSCredentials(
+        'fake-access-key-123', 'fake-secret-access-key-456');
+  }
+
+  @override
+  Future<AWSSignedRequest> authorizeRequest(
+    AWSBaseHttpRequest request, {
+    IamAuthProviderOptions? options,
+  }) async {
+    final signer = AWSSigV4Signer(
+      credentialsProvider: AWSCredentialsProvider(await retrieve()),
+    );
+    final scope = AWSCredentialScope(
+      region: options!.region,
+      service: AWSService.appSync,
+    );
+    return signer.sign(
+      request,
+      credentialScope: scope,
+    );
+  }
+}
+
+void validateSignedRequest(http.BaseRequest request) {
+  const userAgentHeader =
+      zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+  expect(
+    request.headers[userAgentHeader],
+    contains('aws-sigv4'),
+  );
+}

From c63ddd6025670dddced42305a486fec2a314afd9 Mon Sep 17 00:00:00 2001
From: Elijah Quartey <Equartey@users.noreply.github.com>
Date: Fri, 29 Jul 2022 09:31:47 -0500
Subject: [PATCH 26/33] feat(api): GraphQL Custom Request Headers (#1938)

---
 .../lib/src/types/api/graphql/graphql_request.dart           | 5 +++++
 .../amplify_api/lib/src/graphql/send_graphql_request.dart    | 3 ++-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/packages/amplify_core/lib/src/types/api/graphql/graphql_request.dart b/packages/amplify_core/lib/src/types/api/graphql/graphql_request.dart
index ff77c1713c..26778fdee7 100644
--- a/packages/amplify_core/lib/src/types/api/graphql/graphql_request.dart
+++ b/packages/amplify_core/lib/src/types/api/graphql/graphql_request.dart
@@ -22,6 +22,9 @@ class GraphQLRequest<T> {
   /// Only required if your backend has multiple GraphQL endpoints in the amplifyconfiguration.dart file. This parameter is then needed to specify which one to use for this request.
   final String? apiName;
 
+  /// A map of Strings to dynamically use for custom headers in the http request.
+  final Map<String, String>? headers;
+
   /// The body of the request, starting with the operation type and operation name.
   ///
   /// See https://graphql.org/learn/queries/#operation-name for examples and more information.
@@ -57,12 +60,14 @@ class GraphQLRequest<T> {
       {this.apiName,
       required this.document,
       this.variables = const <String, dynamic>{},
+      this.headers,
       this.decodePath,
       this.modelType});
 
   Map<String, dynamic> serializeAsMap() => <String, dynamic>{
         'document': document,
         'variables': variables,
+        'headers': headers,
         'cancelToken': id,
         if (apiName != null) 'apiName': apiName,
       };
diff --git a/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart b/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
index 6eab7deadd..3ba0a36c7d 100644
--- a/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
+++ b/packages/api/amplify_api/lib/src/graphql/send_graphql_request.dart
@@ -31,7 +31,8 @@ Future<GraphQLResponse<T>> sendGraphQLRequest<T>({
 }) async {
   try {
     final body = {'variables': request.variables, 'query': request.document};
-    final graphQLResponse = await client.post(uri, body: json.encode(body));
+    final graphQLResponse = await client.post(uri,
+        body: json.encode(body), headers: request.headers);
 
     final responseBody = json.decode(graphQLResponse.body);
 

From 64a50f620cfcd391aeec659c45105767453db9fc Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Mon, 8 Aug 2022 08:58:01 -0800
Subject: [PATCH 27/33] feat(auth,api): cognito user pools auth provider & auth
 mode for API HTTP requests (#1913)

---
 .../decorators/authorize_http_request.dart    |   9 +-
 .../test/authorize_http_request_test.dart     |  34 ++-
 packages/api/amplify_api/test/util.dart       |   9 +
 .../lib/src/auth_plugin_impl.dart             |  14 +-
 .../cognito_user_pools_auth_provider.dart     |  37 +++
 .../test/plugin/auth_providers_test.dart      | 210 ++++++++++++++----
 6 files changed, 255 insertions(+), 58 deletions(-)
 create mode 100644 packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart

diff --git a/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
index 3cab4d7443..24a343895e 100644
--- a/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
+++ b/packages/api/amplify_api/lib/src/decorators/authorize_http_request.dart
@@ -64,8 +64,15 @@ Future<http.BaseRequest> authorizeHttpRequest(http.BaseRequest request,
       return authorizedRequest.httpRequest;
     case APIAuthorizationType.function:
     case APIAuthorizationType.oidc:
-    case APIAuthorizationType.userPools:
       throw UnimplementedError('${authType.name} not implemented.');
+    case APIAuthorizationType.userPools:
+      final authProvider = _validateAuthProvider(
+        authProviderRepo.getAuthProvider(authType.authProviderToken),
+        authType,
+      );
+      final authorizedRequest =
+          await authProvider.authorizeRequest(_httpToAWSRequest(request));
+      return authorizedRequest.httpRequest;
     case APIAuthorizationType.none:
       return request;
   }
diff --git a/packages/api/amplify_api/test/authorize_http_request_test.dart b/packages/api/amplify_api/test/authorize_http_request_test.dart
index 2179a07ad8..3f1ad3754d 100644
--- a/packages/api/amplify_api/test/authorize_http_request_test.dart
+++ b/packages/api/amplify_api/test/authorize_http_request_test.dart
@@ -33,11 +33,19 @@ void main() {
   final authProviderRepo = AmplifyAuthProviderRepository();
 
   setUpAll(() {
-    authProviderRepo.registerAuthProvider(
+    authProviderRepo
+      ..registerAuthProvider(
         APIAuthorizationType.apiKey.authProviderToken,
-        AppSyncApiKeyAuthProvider());
-    authProviderRepo.registerAuthProvider(
-        APIAuthorizationType.iam.authProviderToken, TestIamAuthProvider());
+        AppSyncApiKeyAuthProvider(),
+      )
+      ..registerAuthProvider(
+        APIAuthorizationType.iam.authProviderToken,
+        TestIamAuthProvider(),
+      )
+      ..registerAuthProvider(
+        APIAuthorizationType.userPools.authProviderToken,
+        TestTokenAuthProvider(),
+      );
   });
 
   group('authorizeHttpRequest', () {
@@ -132,7 +140,23 @@ void main() {
           throwsA(isA<ApiException>()));
     });
 
-    test('authorizes with Cognito User Pools auth mode', () {}, skip: true);
+    test('authorizes with Cognito User Pools auth mode', () async {
+      const endpointConfig = AWSApiConfig(
+          authorizationType: APIAuthorizationType.userPools,
+          endpoint: _gqlEndpoint,
+          endpointType: EndpointType.graphQL,
+          region: _region);
+      final inputRequest = _generateTestRequest(endpointConfig.endpoint);
+      final authorizedRequest = await authorizeHttpRequest(
+        inputRequest,
+        endpointConfig: endpointConfig,
+        authProviderRepo: authProviderRepo,
+      );
+      expect(
+        authorizedRequest.headers[AWSHeaders.authorization],
+        testAccessToken,
+      );
+    });
 
     test('authorizes with OIDC auth mode', () {}, skip: true);
 
diff --git a/packages/api/amplify_api/test/util.dart b/packages/api/amplify_api/test/util.dart
index f3c2ef551e..cd06f8c13c 100644
--- a/packages/api/amplify_api/test/util.dart
+++ b/packages/api/amplify_api/test/util.dart
@@ -17,6 +17,8 @@ import 'package:aws_signature_v4/aws_signature_v4.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:http/http.dart' as http;
 
+const testAccessToken = 'test-access-token-123';
+
 class TestIamAuthProvider extends AWSIamAmplifyAuthProvider {
   @override
   Future<AWSCredentials> retrieve() async {
@@ -43,6 +45,13 @@ class TestIamAuthProvider extends AWSIamAmplifyAuthProvider {
   }
 }
 
+class TestTokenAuthProvider extends TokenAmplifyAuthProvider {
+  @override
+  Future<String> getLatestAuthToken() async {
+    return testAccessToken;
+  }
+}
+
 void validateSignedRequest(http.BaseRequest request) {
   const userAgentHeader =
       zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
index 1db9c30481..d1b1810f6e 100644
--- a/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
+++ b/packages/auth/amplify_auth_cognito_dart/lib/src/auth_plugin_impl.dart
@@ -52,6 +52,7 @@ import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart
 import 'package:amplify_auth_cognito_dart/src/sdk/sdk_bridge.dart';
 import 'package:amplify_auth_cognito_dart/src/state/state.dart';
 import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart';
+import 'package:amplify_auth_cognito_dart/src/util/cognito_user_pools_auth_provider.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:amplify_secure_storage_dart/amplify_secure_storage_dart.dart';
 import 'package:built_collection/built_collection.dart';
@@ -185,10 +186,15 @@ class AmplifyAuthCognitoDart extends AuthPluginInterface
 
     // Register auth providers to provide auth functionality to other plugins
     // without requiring other plugins to call `Amplify.Auth...` directly.
-    authProviderRepo.registerAuthProvider(
-      APIAuthorizationType.iam.authProviderToken,
-      CognitoIamAuthProvider(),
-    );
+    authProviderRepo
+      ..registerAuthProvider(
+        APIAuthorizationType.iam.authProviderToken,
+        CognitoIamAuthProvider(),
+      )
+      ..registerAuthProvider(
+        APIAuthorizationType.userPools.authProviderToken,
+        CognitoUserPoolsAuthProvider(),
+      );
 
     if (_stateMachine.getOrCreate(AuthStateMachine.type).currentState.type !=
         AuthStateType.notConfigured) {
diff --git a/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart
new file mode 100644
index 0000000000..edde7c3bca
--- /dev/null
+++ b/packages/auth/amplify_auth_cognito_dart/lib/src/util/cognito_user_pools_auth_provider.dart
@@ -0,0 +1,37 @@
+// Copyright 2022 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import 'dart:async';
+
+import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart';
+import 'package:amplify_core/amplify_core.dart';
+import 'package:meta/meta.dart';
+
+/// [AmplifyAuthProvider] implementation that adds access token to request headers.
+@internal
+class CognitoUserPoolsAuthProvider extends TokenAmplifyAuthProvider {
+  /// Get access token from `Amplify.Auth.fetchAuthSession()`.
+  @override
+  Future<String> getLatestAuthToken() async {
+    final authSession =
+        await Amplify.Auth.fetchAuthSession() as CognitoAuthSession;
+    final token = authSession.userPoolTokens?.accessToken.raw;
+    if (token == null) {
+      throw const AuthException(
+        'Unable to fetch access token while authorizing with Cognito User Pools.',
+      );
+    }
+    return token;
+  }
+}
diff --git a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
index acb126fa66..de1d20b496 100644
--- a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
+++ b/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
@@ -15,7 +15,9 @@ import 'dart:async';
 
 import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart'
     hide InternalErrorException;
+import 'package:amplify_auth_cognito_dart/src/credentials/cognito_keys.dart';
 import 'package:amplify_auth_cognito_dart/src/util/cognito_iam_auth_provider.dart';
+import 'package:amplify_auth_cognito_dart/src/util/cognito_user_pools_auth_provider.dart';
 import 'package:amplify_core/amplify_core.dart';
 import 'package:test/test.dart';
 
@@ -29,84 +31,196 @@ AWSHttpRequest _generateTestRequest() {
   );
 }
 
-/// Returns dummy AWS credentials.
-class TestAmplifyAuth extends AmplifyAuthCognitoDart {
+/// Mock implementation of user pool only error when trying to get credentials.
+class TestAmplifyAuthUserPoolOnly extends AmplifyAuthCognitoDart {
   @override
   Future<AuthSession> fetchAuthSession({
     required AuthSessionRequest request,
   }) async {
-    return const CognitoAuthSession(
+    final options = request.options as CognitoSessionOptions?;
+    final getAWSCredentials = options?.getAWSCredentials;
+    if (getAWSCredentials != null && getAWSCredentials) {
+      throw const InvalidAccountTypeException.noIdentityPool(
+        recoverySuggestion:
+            'Register an identity pool using the CLI or set getAWSCredentials '
+            'to false',
+      );
+    }
+    return CognitoAuthSession(
       isSignedIn: true,
-      credentials: AWSCredentials('fakeKeyId', 'fakeSecret'),
+      userPoolTokens: CognitoUserPoolTokens(
+        accessToken: accessToken,
+        idToken: idToken,
+        refreshToken: refreshToken,
+      ),
     );
   }
 }
 
 void main() {
+  late AmplifyAuthCognitoDart plugin;
+  late AmplifyAuthProviderRepository testAuthRepo;
+
+  setUpAll(() async {
+    testAuthRepo = AmplifyAuthProviderRepository();
+    final secureStorage = MockSecureStorage();
+    final stateMachine = CognitoAuthStateMachine()..addInstance(secureStorage);
+    plugin = AmplifyAuthCognitoDart(credentialStorage: secureStorage)
+      ..stateMachine = stateMachine;
+
+    seedStorage(
+      secureStorage,
+      userPoolKeys: CognitoUserPoolKeys(userPoolConfig),
+      identityPoolKeys: CognitoIdentityPoolKeys(identityPoolConfig),
+    );
+
+    await plugin.configure(
+      config: mockConfig,
+      authProviderRepo: testAuthRepo,
+    );
+  });
+
   group(
       'AmplifyAuthCognitoDart plugin registers auth providers during configuration',
       () {
-    late AmplifyAuthCognitoDart plugin;
-
-    setUp(() async {
-      plugin = AmplifyAuthCognitoDart(credentialStorage: MockSecureStorage());
-    });
-
     test('registers CognitoIamAuthProvider', () async {
-      final testAuthRepo = AmplifyAuthProviderRepository();
-      await plugin.configure(
-        config: mockConfig,
-        authProviderRepo: testAuthRepo,
-      );
       final authProvider = testAuthRepo.getAuthProvider(
         APIAuthorizationType.iam.authProviderToken,
       );
       expect(authProvider, isA<CognitoIamAuthProvider>());
     });
-  });
-
-  group('CognitoIamAuthProvider', () {
-    setUpAll(() async {
-      await Amplify.addPlugin(TestAmplifyAuth());
-    });
 
-    test('gets AWS credentials from Amplify.Auth.fetchAuthSession', () async {
-      final authProvider = CognitoIamAuthProvider();
-      final credentials = await authProvider.retrieve();
-      expect(credentials.accessKeyId, isA<String>());
-      expect(credentials.secretAccessKey, isA<String>());
+    test('registers CognitoUserPoolsAuthProvider', () async {
+      final authProvider = testAuthRepo.getAuthProvider(
+        APIAuthorizationType.userPools.authProviderToken,
+      );
+      expect(authProvider, isA<CognitoUserPoolsAuthProvider>());
     });
+  });
 
-    test('signs a request when calling authorizeRequest', () async {
+  group('no auth plugin added', () {
+    test('CognitoIamAuthProvider throws when trying to authorize a request',
+        () async {
       final authProvider = CognitoIamAuthProvider();
-      final authorizedRequest = await authProvider.authorizeRequest(
-        _generateTestRequest(),
-        options: const IamAuthProviderOptions(
-          region: 'us-east-1',
-          service: AWSService.appSync,
+      await expectLater(
+        authProvider.authorizeRequest(
+          _generateTestRequest(),
+          options: const IamAuthProviderOptions(
+            region: 'us-east-1',
+            service: AWSService.appSync,
+          ),
         ),
-      );
-      // Note: not intended to be complete test of sigv4 algorithm.
-      expect(authorizedRequest.headers[AWSHeaders.authorization], isNotEmpty);
-      const userAgentHeader =
-          zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
-      expect(
-        authorizedRequest.headers[AWSHeaders.host],
-        isNotEmpty,
-        skip: zIsWeb,
-      );
-      expect(
-        authorizedRequest.headers[userAgentHeader],
-        contains('aws-sigv4'),
+        throwsA(isA<AmplifyException>()),
       );
     });
 
-    test('throws when no options provided', () async {
-      final authProvider = CognitoIamAuthProvider();
+    test('CognitoUserPoolsAuthProvider throws when trying to authorize request',
+        () async {
+      final authProvider = CognitoUserPoolsAuthProvider();
       await expectLater(
         authProvider.authorizeRequest(_generateTestRequest()),
-        throwsA(isA<AuthException>()),
+        throwsA(isA<AmplifyException>()),
       );
     });
   });
+
+  group('auth providers defined in auth plugin', () {
+    setUpAll(() async {
+      await Amplify.reset();
+      await Amplify.addPlugin(plugin);
+    });
+
+    group('CognitoIamAuthProvider', () {
+      test('gets AWS credentials from Amplify.Auth.fetchAuthSession', () async {
+        final authProvider = CognitoIamAuthProvider();
+        final credentials = await authProvider.retrieve();
+        expect(credentials.accessKeyId, isA<String>());
+        expect(credentials.secretAccessKey, isA<String>());
+      });
+
+      test('signs a request when calling authorizeRequest', () async {
+        final authProvider = CognitoIamAuthProvider();
+        final authorizedRequest = await authProvider.authorizeRequest(
+          _generateTestRequest(),
+          options: const IamAuthProviderOptions(
+            region: 'us-east-1',
+            service: AWSService.appSync,
+          ),
+        );
+        // Note: not intended to be complete test of sigv4 algorithm.
+        expect(authorizedRequest.headers[AWSHeaders.authorization], isNotEmpty);
+        const userAgentHeader =
+            zIsWeb ? AWSHeaders.amzUserAgent : AWSHeaders.userAgent;
+        expect(
+          authorizedRequest.headers[AWSHeaders.host],
+          isNotEmpty,
+          skip: zIsWeb,
+        );
+        expect(
+          authorizedRequest.headers[userAgentHeader],
+          contains('aws-sigv4'),
+        );
+      });
+
+      test('throws when no options provided', () async {
+        final authProvider = CognitoIamAuthProvider();
+        await expectLater(
+          authProvider.authorizeRequest(_generateTestRequest()),
+          throwsA(isA<AuthException>()),
+        );
+      });
+    });
+
+    group('CognitoUserPoolsAuthProvider', () {
+      test('gets raw access token from Amplify.Auth.fetchAuthSession',
+          () async {
+        final authProvider = CognitoUserPoolsAuthProvider();
+        final token = await authProvider.getLatestAuthToken();
+        expect(token, accessToken.raw);
+      });
+
+      test('adds access token to header when calling authorizeRequest',
+          () async {
+        final authProvider = CognitoUserPoolsAuthProvider();
+        final authorizedRequest = await authProvider.authorizeRequest(
+          _generateTestRequest(),
+        );
+        expect(
+          authorizedRequest.headers[AWSHeaders.authorization],
+          accessToken.raw,
+        );
+      });
+    });
+  });
+
+  group('auth providers with user pool-only configuration', () {
+    setUpAll(() async {
+      await Amplify.reset();
+      await Amplify.addPlugin(TestAmplifyAuthUserPoolOnly());
+    });
+
+    group('CognitoIamAuthProvider', () {
+      test('throws when trying to retrieve credentials', () async {
+        final authProvider = CognitoIamAuthProvider();
+        await expectLater(
+          authProvider.retrieve(),
+          throwsA(isA<InvalidAccountTypeException>()),
+        );
+      });
+    });
+
+    group('CognitoUserPoolsAuthProvider', () {
+      test('adds access token to header when calling authorizeRequest',
+          () async {
+        final authProvider = CognitoUserPoolsAuthProvider();
+        final authorizedRequest = await authProvider.authorizeRequest(
+          _generateTestRequest(),
+        );
+        expect(
+          authorizedRequest.headers[AWSHeaders.authorization],
+          accessToken.raw,
+        );
+      });
+    });
+  });
 }

From 8b125c668c9f913ac9c48da0327b5df28ac48c10 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Thu, 18 Aug 2022 07:05:03 -0700
Subject: [PATCH 28/33] correct error handling

---
 .../src/graphql/ws/web_socket_connection.dart | 66 ++++++++++++-------
 ...web_socket_message_stream_transformer.dart |  1 +
 .../test/ws/web_socket_connection_test.dart   | 20 +++---
 .../lib/src/auth_plugin_impl.dart             |  7 +-
 4 files changed, 59 insertions(+), 35 deletions(-)

diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
index 683a10afb7..bb21665953 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
@@ -72,12 +72,15 @@ class WebSocketConnection implements Closeable {
   @visibleForTesting
   StreamSubscription<WebSocketMessage> getStreamSubscription(
       Stream<dynamic> stream) {
-    return stream
-        .transform(const WebSocketMessageStreamTransformer())
-        .listen(_onData, onError: (Object e) {
-      _connectionError(ApiException('Connection failed.',
-          underlyingException: e.toString()));
-    });
+    return stream.transform(const WebSocketMessageStreamTransformer()).listen(
+      _onData,
+      cancelOnError: true,
+      onError: (Object e) {
+        _connectionError(
+          ApiException('Connection failed.', underlyingException: e.toString()),
+        );
+      },
+    );
   }
 
   /// Connects WebSocket channel to _subscription stream but does not send connection
@@ -92,7 +95,7 @@ class WebSocketConnection implements Closeable {
   }
 
   void _connectionError(ApiException exception) {
-    _connectionReady.completeError(_connectionError);
+    _connectionReady.completeError(exception);
     _channel?.sink.close();
     _resetConnectionInit();
   }
@@ -134,35 +137,54 @@ class WebSocketConnection implements Closeable {
     return ready;
   }
 
+  Future<void> _sendSubscriptionRegistrationMessage<T>(
+      GraphQLRequest<T> request) async {
+    await init(); // no-op if already connected
+    final subscriptionRegistrationMessage =
+        await generateSubscriptionRegistrationMessage(
+      _config,
+      id: request.id,
+      authRepo: _authProviderRepo,
+      request: request,
+    );
+    send(subscriptionRegistrationMessage);
+  }
+
   /// Subscribes to the given GraphQL request. Returns the subscription object,
   /// or throws an [Exception] if there's an error.
   Stream<GraphQLResponse<T>> subscribe<T>(
     GraphQLRequest<T> request,
     void Function()? onEstablished,
   ) {
-    final subscriptionId = uuid();
-
-    // init is no-op if already connected
-    init().then((_) {
-      // Generate and send an authorized subscription registration message.
-      generateSubscriptionRegistrationMessage(
-        _config,
-        id: subscriptionId,
-        authRepo: _authProviderRepo,
-        request: request,
-      ).then(send);
-    });
+    // Create controller for this subscription so we can add errors.
+    late StreamController<GraphQLResponse<T>> controller;
+    controller = StreamController<GraphQLResponse<T>>.broadcast(
+      onCancel: () {
+        _cancel(request.id);
+        controller.close();
+      },
+    );
 
     // Filter incoming messages that have the subscription ID and return as new
     // stream with messages converted to GraphQLResponse<T>.
-    return _messageStream
-        .where((msg) => msg.id == subscriptionId)
+    _messageStream
+        .where((msg) => msg.id == request.id)
         .transform(WebSocketSubscriptionStreamTransformer(
           request,
           onEstablished,
           logger: _logger,
         ))
-        .asBroadcastStream(onCancel: (_) => _cancel(subscriptionId));
+        .listen(
+          controller.add,
+          onError: controller.addError,
+          onDone: controller.close,
+          cancelOnError: true,
+        );
+
+    _sendSubscriptionRegistrationMessage(request)
+        .catchError(controller.addError);
+
+    return controller.stream;
   }
 
   /// Cancels a subscription.
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
index f9837c7272..0ecbffcee9 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
@@ -59,6 +59,7 @@ class WebSocketSubscriptionStreamTransformer<T>
           break;
         case MessageType.data:
           final payload = event.payload as SubscriptionDataPayload;
+          // TODO(ragingsquirrel3): refactor decoder
           final errors = deserializeGraphQLResponseErrors(payload.toJson());
           yield GraphQLResponseDecoder.instance.decode<T>(
             request: request,
diff --git a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
index 81e0d87d6e..632b840781 100644
--- a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
+++ b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
@@ -62,7 +62,7 @@ void main() {
       Completer<void> establishedCompleter = Completer();
       connection.subscribe(subscriptionRequest, () {
         establishedCompleter.complete();
-      }).listen((event) {});
+      });
 
       expectLater(connection.ready, completes);
       expectLater(establishedCompleter.future, completes);
@@ -76,7 +76,7 @@ void main() {
       Completer<void> establishedCompleter = Completer();
       connection.subscribe(subscriptionRequest, () {
         establishedCompleter.complete();
-      }).listen((event) {});
+      });
       await establishedCompleter.future;
 
       final lastMessage = connection.lastSentMessage;
@@ -88,17 +88,15 @@ void main() {
     });
 
     test('subscribe() should return a subscription stream', () async {
-      Completer<void> establishedCompleter = Completer();
       Completer<String> dataCompleter = Completer();
       final subscription = connection.subscribe(
         subscriptionRequest,
-        () => establishedCompleter.complete(),
+        null,
       );
 
       final streamSub = subscription.listen(
         (event) => dataCompleter.complete(event.data),
       );
-      await expectLater(establishedCompleter.future, completes);
 
       final subscriptionData = await dataCompleter.future;
       expect(subscriptionData, json.encode(mockSubscriptionData));
@@ -106,12 +104,12 @@ void main() {
     });
 
     test('cancel() should send a stop message', () async {
-      Completer<void> establishedCompleter = Completer();
-      final subscription = connection.subscribe(subscriptionRequest, () {
-        establishedCompleter.complete();
-      });
-      final streamSub = subscription.listen((event) {});
-      await establishedCompleter.future;
+      Completer<String> dataCompleter = Completer();
+      final subscription = connection.subscribe(subscriptionRequest, null);
+      final streamSub = subscription.listen(
+        (event) => dataCompleter.complete(event.data),
+      );
+      await dataCompleter.future;
       streamSub.cancel();
       expect(connection.lastSentMessage?.messageType, MessageType.stop);
     });
diff --git a/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart
index b4e7585490..c16bc3ebba 100644
--- a/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart
+++ b/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart
@@ -87,8 +87,11 @@ class AmplifyAuthCognito extends AmplifyAuthCognitoDart with AWSDebuggable {
   }
 
   @override
-  Future<void> configure({AmplifyConfig? config}) async {
-    await super.configure(config: config);
+  Future<void> configure({
+    AmplifyConfig? config,
+    required AmplifyAuthProviderRepository authProviderRepo,
+  }) async {
+    await super.configure(config: config, authProviderRepo: authProviderRepo);
 
     // Update the native cache for the current user on hub events.
     final nativeBridge = stateMachine.get<NativeAuthBridge>();

From 73000249e70b31d7036d9211e07c27c46c443f5a Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Mon, 22 Aug 2022 09:05:41 -0800
Subject: [PATCH 29/33] fix(auth): correct auth providers imports from rebase
 (#2042)

---
 .../amplify_auth_cognito/lib/src/auth_plugin_impl.dart     | 7 +++++--
 .../test/plugin/auth_providers_test.dart                   | 0
 2 files changed, 5 insertions(+), 2 deletions(-)
 rename packages/auth/{amplify_auth_cognito_dart => amplify_auth_cognito_test}/test/plugin/auth_providers_test.dart (100%)

diff --git a/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart b/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart
index b4e7585490..c16bc3ebba 100644
--- a/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart
+++ b/packages/auth/amplify_auth_cognito/lib/src/auth_plugin_impl.dart
@@ -87,8 +87,11 @@ class AmplifyAuthCognito extends AmplifyAuthCognitoDart with AWSDebuggable {
   }
 
   @override
-  Future<void> configure({AmplifyConfig? config}) async {
-    await super.configure(config: config);
+  Future<void> configure({
+    AmplifyConfig? config,
+    required AmplifyAuthProviderRepository authProviderRepo,
+  }) async {
+    await super.configure(config: config, authProviderRepo: authProviderRepo);
 
     // Update the native cache for the current user on hub events.
     final nativeBridge = stateMachine.get<NativeAuthBridge>();
diff --git a/packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart b/packages/auth/amplify_auth_cognito_test/test/plugin/auth_providers_test.dart
similarity index 100%
rename from packages/auth/amplify_auth_cognito_dart/test/plugin/auth_providers_test.dart
rename to packages/auth/amplify_auth_cognito_test/test/plugin/auth_providers_test.dart

From e1ac0ce9c0c02b06d4cd2ac165dcada2a667a3c6 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Mon, 22 Aug 2022 12:32:46 -0700
Subject: [PATCH 30/33] improve test syntax

---
 .../src/decorators/web_socket_auth_utils.dart | 39 ++++++++++--------
 ...web_socket_message_stream_transformer.dart | 18 ++++++--
 .../lib/src/graphql/ws/web_socket_types.dart  | 12 ++----
 .../amplify_api/test/dart_graphql_test.dart   | 41 +++++++++++++++----
 .../test/ws/web_socket_connection_test.dart   | 20 ++++-----
 5 files changed, 79 insertions(+), 51 deletions(-)

diff --git a/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
index f685c3821f..1a6b063588 100644
--- a/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
+++ b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
@@ -12,6 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+@internal
+library amplify_api.decorators.web_socket_auth_utils;
+
 import 'dart:convert';
 
 import 'package:amplify_core/amplify_core.dart';
@@ -22,9 +25,11 @@ import '../graphql/ws/web_socket_types.dart';
 import 'authorize_http_request.dart';
 
 // Constants for header values as noted in https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html.
-const _acceptHeaderValue = 'application/json, text/javascript';
-const _contentEncodingHeaderValue = 'amz-1.0';
-const _contentTypeHeaderValue = 'application/json; charset=UTF-8';
+const _requiredHeaders = {
+  AWSHeaders.accept: 'application/json, text/javascript',
+  AWSHeaders.contentEncoding: 'amz-1.0',
+  AWSHeaders.contentType: 'application/json; charset=UTF-8',
+};
 
 // AppSync expects "{}" encoded in the URI as the payload during handshake.
 const _emptyBody = '{}';
@@ -32,11 +37,11 @@ const _emptyBody = '{}';
 /// Generate a URI for the connection and all subscriptions.
 ///
 /// See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection=
-@internal
 Future<Uri> generateConnectionUri(
     AWSApiConfig config, AmplifyAuthProviderRepository authRepo) async {
   final authorizationHeaders = await _generateAuthorizationHeaders(
     config,
+    isConnectionInit: true,
     authRepo: authRepo,
     body: _emptyBody,
   );
@@ -55,7 +60,6 @@ Future<Uri> generateConnectionUri(
 /// Generate websocket message with authorized payload to register subscription.
 ///
 /// See https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#subscription-registration-message
-@internal
 Future<WebSocketSubscriptionRegistrationMessage>
     generateSubscriptionRegistrationMessage(
   AWSApiConfig config, {
@@ -65,8 +69,12 @@ Future<WebSocketSubscriptionRegistrationMessage>
 }) async {
   final body =
       jsonEncode({'variables': request.variables, 'query': request.document});
-  final authorizationHeaders = await _generateAuthorizationHeaders(config,
-      authRepo: authRepo, body: body);
+  final authorizationHeaders = await _generateAuthorizationHeaders(
+    config,
+    isConnectionInit: false,
+    authRepo: authRepo,
+    body: body,
+  );
 
   return WebSocketSubscriptionRegistrationMessage(
     id: id,
@@ -82,29 +90,26 @@ Future<WebSocketSubscriptionRegistrationMessage>
 /// are formatted correctly to be either encoded into URI query params or subscription
 /// registration payload headers.
 ///
-/// If body is "{}" then headers are formatted like connection URI. Any other string
-/// for body will be formatted as subscription registration. This is done by creating
+/// If `isConnectionInit` true then headers are formatted like connection URI.
+/// Otherwise body will be formatted as subscription registration. This is done by creating
 /// a canonical HTTP request that is authorized but never sent. The headers from
 /// the HTTP request are reformatted and returned. This logic applies for all auth
 /// modes as determined by [authRepo] parameter.
 Future<Map<String, String>> _generateAuthorizationHeaders(
   AWSApiConfig config, {
+  required bool isConnectionInit,
   required AmplifyAuthProviderRepository authRepo,
   required String body,
 }) async {
   final endpointHost = Uri.parse(config.endpoint).host;
   // Create canonical HTTP request to authorize but never send.
   //
-  // The canonical request URL is a little different depending on if connection_init
-  // or start (subscription registration).
-  final maybeConnect = body != _emptyBody ? '' : '/connect';
+  // The canonical request URL is a little different depending on if authorizing
+  // connection URI or start message (subscription registration).
+  final maybeConnect = isConnectionInit ? '' : '/connect';
   final canonicalHttpRequest =
       http.Request('POST', Uri.parse('${config.endpoint}$maybeConnect'));
-  canonicalHttpRequest.headers.addAll({
-    AWSHeaders.accept: _acceptHeaderValue,
-    AWSHeaders.contentEncoding: _contentEncodingHeaderValue,
-    AWSHeaders.contentType: _contentTypeHeaderValue,
-  });
+  canonicalHttpRequest.headers.addAll(_requiredHeaders);
   canonicalHttpRequest.body = body;
   final authorizedHttpRequest = await authorizeHttpRequest(
     canonicalHttpRequest,
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
index 0ecbffcee9..960a617eac 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
@@ -12,7 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// ignore_for_file: public_member_api_docs
+@internal
+library amplify_api.graphql.ws.web_socket_message_stream_transformer;
 
 import 'dart:async';
 import 'dart:convert';
@@ -24,9 +25,11 @@ import 'package:meta/meta.dart';
 import '../graphql_response_decoder.dart';
 import 'web_socket_types.dart';
 
-@internal
+/// Top-level transformer.
 class WebSocketMessageStreamTransformer
     extends StreamTransformerBase<dynamic, WebSocketMessage> {
+  /// Transforms raw web socket response (String) to `WebSocketMessage` for all input
+  /// from channel.
   const WebSocketMessageStreamTransformer();
 
   @override
@@ -37,13 +40,22 @@ class WebSocketMessageStreamTransformer
   }
 }
 
-@internal
+/// Final level of transformation for converting `WebSocketMessage`s to stream
+/// of `GraphQLResponse` that is eventually passed to public API `Amplify.API.subscribe`.
 class WebSocketSubscriptionStreamTransformer<T>
     extends StreamTransformerBase<WebSocketMessage, GraphQLResponse<T>> {
+  /// request for this stream, needed to properly decode response events
   final GraphQLRequest<T> request;
+
+  /// logs complete messages to better provide visibility to cancels
   final AmplifyLogger logger;
+
+  /// executes when start_ack message received
   final void Function()? onEstablished;
 
+  /// [request] is used to properly decode response events
+  /// [onEstablished] is executed when start_ack message received
+  /// [logger] logs cancel messages when complete message received
   const WebSocketSubscriptionStreamTransformer(
     this.request,
     this.onEstablished, {
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
index 961a433fe4..1a46012387 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
@@ -14,12 +14,14 @@
 
 // ignore_for_file: public_member_api_docs
 
+@internal
+library amplify_api.graphql.ws.web_socket_types;
+
 import 'dart:convert';
 
 import 'package:amplify_core/amplify_core.dart';
 import 'package:meta/meta.dart';
 
-@internal
 class MessageType {
   final String type;
 
@@ -57,7 +59,6 @@ class MessageType {
 }
 
 @immutable
-@internal
 abstract class WebSocketMessagePayload {
   const WebSocketMessagePayload();
 
@@ -95,7 +96,6 @@ class ConnectionAckMessagePayload extends WebSocketMessagePayload {
       };
 }
 
-@internal
 class SubscriptionRegistrationPayload extends WebSocketMessagePayload {
   final GraphQLRequest request;
   final AWSApiConfig config;
@@ -119,7 +119,6 @@ class SubscriptionRegistrationPayload extends WebSocketMessagePayload {
   }
 }
 
-@internal
 class SubscriptionDataPayload extends WebSocketMessagePayload {
   final Map<String, dynamic>? data;
   final Map<String, dynamic>? errors;
@@ -142,7 +141,6 @@ class SubscriptionDataPayload extends WebSocketMessagePayload {
       };
 }
 
-@internal
 class WebSocketError extends WebSocketMessagePayload implements Exception {
   final List<Map> errors;
 
@@ -160,7 +158,6 @@ class WebSocketError extends WebSocketMessagePayload implements Exception {
 }
 
 @immutable
-@internal
 class WebSocketMessage {
   final String? id;
   final MessageType messageType;
@@ -208,13 +205,11 @@ class WebSocketMessage {
   }
 }
 
-@internal
 class WebSocketConnectionInitMessage extends WebSocketMessage {
   WebSocketConnectionInitMessage()
       : super(messageType: MessageType.connectionInit);
 }
 
-@internal
 class WebSocketSubscriptionRegistrationMessage extends WebSocketMessage {
   WebSocketSubscriptionRegistrationMessage({
     required String id,
@@ -222,7 +217,6 @@ class WebSocketSubscriptionRegistrationMessage extends WebSocketMessage {
   }) : super(messageType: MessageType.start, payload: payload, id: id);
 }
 
-@internal
 class WebSocketStopMessage extends WebSocketMessage {
   WebSocketStopMessage({required String id})
       : super(messageType: MessageType.stop, id: id);
diff --git a/packages/api/amplify_api/test/dart_graphql_test.dart b/packages/api/amplify_api/test/dart_graphql_test.dart
index f1d7919bef..b37a2611f3 100644
--- a/packages/api/amplify_api/test/dart_graphql_test.dart
+++ b/packages/api/amplify_api/test/dart_graphql_test.dart
@@ -148,6 +148,30 @@ void main() {
       expect(res.errors, equals(null));
     });
 
+    test('Query returns proper response.data with dynamic type', () async {
+      String graphQLDocument = ''' query TestQuery {
+          listBlogs {
+            items {
+              id
+              name
+              createdAt
+            }
+          }
+        } ''';
+      final req = GraphQLRequest<dynamic>(
+        document: graphQLDocument,
+        variables: {},
+      );
+
+      final operation = Amplify.API.query(request: req);
+      final res = await operation.value;
+
+      final expected = json.encode(_expectedQuerySuccessResponseBody['data']);
+
+      expect(res.data, equals(expected));
+      expect(res.errors, equals(null));
+    });
+
     test('Mutate returns proper response.data', () async {
       String graphQLDocument = ''' mutation TestMutate(\$name: String!) {
           createBlog(input: {name: \$name}) {
@@ -226,21 +250,20 @@ void main() {
 
     test('subscribe() should decode model data', () async {
       Completer<void> establishedCompleter = Completer();
-      Completer<Post> dataCompleter = Completer();
       final subscriptionRequest = ModelSubscriptions.onCreate(Post.classType);
       final subscription = Amplify.API.subscribe(
         subscriptionRequest,
         onEstablished: () => establishedCompleter.complete(),
       );
-
-      final streamSub = subscription.listen(
-        (event) => dataCompleter.complete(event.data),
+      await establishedCompleter.future;
+
+      late StreamSubscription<GraphQLResponse<Post>> streamSub;
+      streamSub = subscription.listen(
+        expectAsync1((event) {
+          expect(event.data, isA<Post>());
+          streamSub.cancel();
+        }),
       );
-      await expectLater(establishedCompleter.future, completes);
-
-      final subscriptionData = await dataCompleter.future;
-      expect(subscriptionData, isA<Post>());
-      streamSub.cancel();
     });
   });
 
diff --git a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
index 632b840781..9a1e3e6545 100644
--- a/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
+++ b/packages/api/amplify_api/test/ws/web_socket_connection_test.dart
@@ -59,13 +59,8 @@ void main() {
 
     test('subscribe() should initialize the connection and call onEstablished',
         () async {
-      Completer<void> establishedCompleter = Completer();
-      connection.subscribe(subscriptionRequest, () {
-        establishedCompleter.complete();
-      });
-
+      connection.subscribe(subscriptionRequest, expectAsync0(() {}));
       expectLater(connection.ready, completes);
-      expectLater(establishedCompleter.future, completes);
     });
 
     test(
@@ -88,19 +83,18 @@ void main() {
     });
 
     test('subscribe() should return a subscription stream', () async {
-      Completer<String> dataCompleter = Completer();
       final subscription = connection.subscribe(
         subscriptionRequest,
         null,
       );
 
-      final streamSub = subscription.listen(
-        (event) => dataCompleter.complete(event.data),
+      late StreamSubscription<GraphQLResponse<String>> streamSub;
+      streamSub = subscription.listen(
+        expectAsync1((event) {
+          expect(event.data, json.encode(mockSubscriptionData));
+          streamSub.cancel();
+        }),
       );
-
-      final subscriptionData = await dataCompleter.future;
-      expect(subscriptionData, json.encode(mockSubscriptionData));
-      streamSub.cancel();
     });
 
     test('cancel() should send a stop message', () async {

From b6358fc842fa1cde6a8ff16acfcfe5b359c32958 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Mon, 22 Aug 2022 12:47:03 -0700
Subject: [PATCH 31/33] change null safety

---
 packages/api/amplify_api/example/ios/Runner/Info.plist      | 2 ++
 .../lib/src/decorators/web_socket_auth_utils.dart           | 2 +-
 .../lib/src/graphql/ws/web_socket_connection.dart           | 6 +-----
 3 files changed, 4 insertions(+), 6 deletions(-)

diff --git a/packages/api/amplify_api/example/ios/Runner/Info.plist b/packages/api/amplify_api/example/ios/Runner/Info.plist
index 7c583a6a81..a41b727111 100644
--- a/packages/api/amplify_api/example/ios/Runner/Info.plist
+++ b/packages/api/amplify_api/example/ios/Runner/Info.plist
@@ -41,5 +41,7 @@
 	</array>
 	<key>UIViewControllerBasedStatusBarAppearance</key>
 	<false/>
+	<key>CADisableMinimumFrameDurationOnPhone</key>
+	<true/>
 </dict>
 </plist>
diff --git a/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
index 1a6b063588..d1520c731e 100644
--- a/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
+++ b/packages/api/amplify_api/lib/src/decorators/web_socket_auth_utils.dart
@@ -106,7 +106,7 @@ Future<Map<String, String>> _generateAuthorizationHeaders(
   //
   // The canonical request URL is a little different depending on if authorizing
   // connection URI or start message (subscription registration).
-  final maybeConnect = isConnectionInit ? '' : '/connect';
+  final maybeConnect = isConnectionInit ? '/connect' : '';
   final canonicalHttpRequest =
       http.Request('POST', Uri.parse('${config.endpoint}$maybeConnect'));
   canonicalHttpRequest.headers.addAll(_requiredHeaders);
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
index bb21665953..939aab96b8 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_connection.dart
@@ -198,10 +198,6 @@ class WebSocketConnection implements Closeable {
   @visibleForTesting
   void send(WebSocketMessage message) {
     final msgJson = json.encode(message.toJson());
-    if (_channel == null) {
-      throw ApiException(
-          'Web socket not connected. Cannot send message $message');
-    }
     _channel!.sink.add(msgJson);
   }
 
@@ -257,6 +253,6 @@ class WebSocketConnection implements Closeable {
 
     // Re-broadcast other message types related to single subscriptions.
 
-    if (!_rebroadcastController.isClosed) _rebroadcastController.add(message);
+    _rebroadcastController.add(message);
   }
 }

From d09da9a160e72c8d1e756635d987b8e1d5aa4776 Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Tue, 23 Aug 2022 14:10:03 -0700
Subject: [PATCH 32/33] undo plist change

---
 packages/api/amplify_api/example/ios/Runner/Info.plist | 2 --
 1 file changed, 2 deletions(-)

diff --git a/packages/api/amplify_api/example/ios/Runner/Info.plist b/packages/api/amplify_api/example/ios/Runner/Info.plist
index a41b727111..7c583a6a81 100644
--- a/packages/api/amplify_api/example/ios/Runner/Info.plist
+++ b/packages/api/amplify_api/example/ios/Runner/Info.plist
@@ -41,7 +41,5 @@
 	</array>
 	<key>UIViewControllerBasedStatusBarAppearance</key>
 	<false/>
-	<key>CADisableMinimumFrameDurationOnPhone</key>
-	<true/>
 </dict>
 </plist>

From 6e384c3c4e57a5109af2742e2eacfe00397d2ecf Mon Sep 17 00:00:00 2001
From: Travis Sheppard <tshepp@amazon.com>
Date: Tue, 23 Aug 2022 14:58:12 -0700
Subject: [PATCH 33/33] make enum messageType

---
 ...web_socket_message_stream_transformer.dart |  2 +
 .../lib/src/graphql/ws/web_socket_types.dart  | 67 ++++++++++---------
 packages/api/amplify_api/pubspec.yaml         |  1 +
 3 files changed, 39 insertions(+), 31 deletions(-)

diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
index 960a617eac..e037bd1ba5 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_message_stream_transformer.dart
@@ -86,6 +86,8 @@ class WebSocketSubscriptionStreamTransformer<T>
         case MessageType.complete:
           logger.info('Cancel succeeded for Operation: ${event.id}');
           return;
+        default:
+          break;
       }
     }
   }
diff --git a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
index 1a46012387..c957b82641 100644
--- a/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
+++ b/packages/api/amplify_api/lib/src/graphql/ws/web_socket_types.dart
@@ -20,42 +20,47 @@ library amplify_api.graphql.ws.web_socket_types;
 import 'dart:convert';
 
 import 'package:amplify_core/amplify_core.dart';
+import 'package:json_annotation/json_annotation.dart';
 import 'package:meta/meta.dart';
 
-class MessageType {
+enum MessageType {
+  @JsonValue('connection_init')
+  connectionInit('connection_init'),
+
+  @JsonValue('connection_ack')
+  connectionAck('connection_ack'),
+
+  @JsonValue('connection_error')
+  connectionError('connection_error'),
+
+  @JsonValue('start')
+  start('start'),
+
+  @JsonValue('start_ack')
+  startAck('start_ack'),
+
+  @JsonValue('connection_error')
+  error('connection_error'),
+
+  @JsonValue('data')
+  data('data'),
+
+  @JsonValue('stop')
+  stop('stop'),
+
+  @JsonValue('ka')
+  keepAlive('ka'),
+
+  @JsonValue('complete')
+  complete('complete');
+
   final String type;
 
-  const MessageType._(this.type);
-
-  factory MessageType.fromJson(dynamic json) =>
-      values.firstWhere((el) => json == el.type);
-
-  static const List<MessageType> values = [
-    connectionInit,
-    connectionAck,
-    connectionError,
-    start,
-    startAck,
-    error,
-    data,
-    stop,
-    keepAlive,
-    complete,
-  ];
-
-  static const connectionInit = MessageType._('connection_init');
-  static const connectionAck = MessageType._('connection_ack');
-  static const connectionError = MessageType._('connection_error');
-  static const error = MessageType._('error');
-  static const start = MessageType._('start');
-  static const startAck = MessageType._('start_ack');
-  static const data = MessageType._('data');
-  static const stop = MessageType._('stop');
-  static const keepAlive = MessageType._('ka');
-  static const complete = MessageType._('complete');
+  const MessageType(this.type);
 
-  @override
-  String toString() => type;
+  factory MessageType.fromJson(dynamic json) {
+    return MessageType.values.firstWhere((el) => json == el.type);
+  }
 }
 
 @immutable
diff --git a/packages/api/amplify_api/pubspec.yaml b/packages/api/amplify_api/pubspec.yaml
index 2142e41417..aa5240a437 100644
--- a/packages/api/amplify_api/pubspec.yaml
+++ b/packages/api/amplify_api/pubspec.yaml
@@ -21,6 +21,7 @@ dependencies:
   flutter:
     sdk: flutter
   http: ^0.13.4
+  json_annotation: ^4.6.0
   meta: ^1.7.0
   plugin_platform_interface: ^2.0.0
   web_socket_channel: ^2.2.0