GoogleCloudPlatform / functions-framework-dart

FaaS (Function as a service) framework for writing portable Dart functions
https://pub.dev/packages/functions_framework
Apache License 2.0
533 stars 54 forks source link

"Not compatible with a supported function shape" when using RequestContext parameter #464

Open jimmyff opened 2 months ago

jimmyff commented 2 months ago

Hey, I've recently updated this package (0.4.3+1) and I'm having difficulty getting my async pub sub endpoints working again.

The issue seems to be with using async modifier, giving me Not compatible with a supported function shape error, this is my function decleration:

@CloudFunction()
Future<void> function(PubSub pubSub, RequestContext context) async {

Where as this works, but then I can't use all the async conveniences.

@CloudFunction()
FutureOr<void> function(PubSub pubSub, RequestContext context) {

I'm sure I am missing something simple here? 😅 (apologies in advance...)

Error ``` [SEVERE] functions_framework_builder:function_framework_builder on lib/functions.dart: Not compatible with a supported function shape: HandlerWithLogger [FutureOr Function(Request, RequestLogger)] from package:functions_framework/functions_framework.dart Handler [FutureOr Function(Request)] from package:shelf/shelf.dart CloudEventWithContextHandler [FutureOr Function(CloudEvent, RequestContext)] from package:functions_framework/functions_framework.dart CloudEventHandler [FutureOr Function(CloudEvent)] from package:functions_framework/functions_framework.dart JsonHandler [FutureOr Function(RequestType request, RequestContext context)] from package:functions_framework/functions_framework.dart JsonHandler [FutureOr Function(RequestType request)] from package:functions_framework/functions_framework.dart package:test_pubsub/functions.dart:10:14 | | Future function(PubSub pubSub, RequestContext context) async { | ^^^^^^^^ | [INFO] Running build completed, took 21.1s [INFO] Caching finalized dependency graph... [INFO] Caching finalized dependency graph completed, took 165ms [SEVERE] Failed after 21.3s ```
bin/server.dart ```dart import 'package:functions_framework/serve.dart'; import 'package:test_pubsub/functions.dart' as function_library; Future main(List args) async { await serve(args, _nameToFunctionTarget); } FunctionTarget? _nameToFunctionTarget(String name) => switch (name) { 'function' => JsonWithContextFunctionTarget.voidResult( function_library.function, (json) { if (json is Map) { try { return function_library.PubSub.fromJson(json); } catch (e, stack) { throw BadRequestException( 400, 'There was an error parsing the provided JSON data.', innerError: e, innerStack: stack, ); } } throw BadRequestException( 400, 'The provided JSON is not the expected type ' '`Map`.', ); }, ), _ => null }; ```
lib/functions.dart ```dart @CloudFunction() Future function(PubSub pubSub, RequestContext context) async { await Future.delayed(Duration(milliseconds: 1)); context.logger.info('[Pub Sub] subscription: ${pubSub.subscription}'); context.logger.info('[Pub Sub] message: ${pubSub.message.dataDecoded()}'); context.responseHeaders['subscription'] = pubSub.subscription; } ```

Thanks

jimmyff commented 2 months ago

Just been digging through the source and I can't understand why JsonWithContextFunctionTarget<PubSub, void> isn't working for me. build_runner is correctly generating bin/server.dart, it's only when it's trying to deploy to cloud functions that it throws this error.

Wondering if I need to make my own FunctionTarget but I think there should be a simple way to get JsonWithContextFunctionTarget working?

kevmoo commented 2 months ago

Does PubSub have a fromJson constructor and a toJson function?

kevmoo commented 2 months ago

And/or have you tried typing your function FutureOr and marking it async?

jimmyff commented 1 month ago

Hey @kevmoo thanks for the suggestions. Yeah pub sub class should be fine, this is it:

pub_sub_types.dart ```dart import 'dart:convert'; import 'dart:typed_data'; import 'package:json_annotation/json_annotation.dart'; part 'pub_sub_types.g.dart'; @JsonSerializable() class PubSub { final PubSubMessage message; final String subscription; PubSub(this.message, this.subscription); factory PubSub.fromJson(Map json) { try { return _$PubSubFromJson(json); } catch (e, s) { print('PubSub: Failed to parse json: $json'); rethrow; } } Map toJson() => _$PubSubToJson(this); } @JsonSerializable() class PubSubMessage { final String? data; final Map? attributes; final String messageId; final DateTime publishTime; bool hasData() => data != null; Map jsonDecoded() => json.decode(dataDecoded()); String dataDecoded() => utf8.decode(dataBytes()); Uint8List dataBytes() => data != null ? base64Decode(data!) : throw Exception('Data excected'); PubSubMessage(this.data, this.messageId, this.publishTime, this.attributes); factory PubSubMessage.fromJson(Map json) { try { return _$PubSubMessageFromJson(json); } catch (e, s) { print('PubSubMessage: Failed to parse json: $json'); rethrow; } } Map toJson() => _$PubSubMessageToJson(this); } // This was a test to see if returning this instead of void would help, it didn't @JsonSerializable() class PubSubResponse { final bool success; PubSubResponse(this.success); factory PubSubResponse.fromJson(Map json) { try { return _$PubSubResponseFromJson(json); } catch (e, s) { print('PubSubResponse: Failed to parse json: $json'); rethrow; } } Map toJson() => _$PubSubResponseToJson(this); } ```

I've tried every combination return types and async keyword but I get the not compatible shape error if I use any sort of Future / async. I tried creating a PubSubResponse class for ResponseType but that hasn't helped.

Should I look at registering my own function target? If so is there any advice where to start with that? I've had a quick scout around but I couldn't see where they're registered.

Thanks

jimmyff commented 1 month ago

A little progress, it doesn't seem to be related to the Future/async. If I alter the package example to include the RequestContext parameter then it fails to deploy too:

@CloudFunction()
GreetingResponse function(GreetingRequest request, RequestContext context) {
  final name = request.name ?? 'World';
  final json = GreetingResponse(salutation: 'Hello', name: name);
  return json;
}

With the same error:

Not compatible with a supported function shape:
  HandlerWithLogger [FutureOr<Response> Function(Request, RequestLogger)] from package:functions_framework/functions_framework.dart
  Handler [FutureOr<Response> Function(Request)] from package:shelf/shelf.dart
  CloudEventWithContextHandler [FutureOr<void> Function(CloudEvent, RequestContext)] from package:functions_framework/functions_framework.dart
  CloudEventHandler [FutureOr<void> Function(CloudEvent)] from package:functions_framework/functions_framework.dart
  JsonHandler [FutureOr<ResponseType> Function(RequestType request, RequestContext context)] from package:functions_framework/functions_framework.dart
  JsonHandler [FutureOr<ResponseType> Function(RequestType request)] from package:functions_framework/functions_framework.dart

package:test_pubsub/functions.dart:55:18
   â•·
55 │ GreetingResponse function(GreetingRequest request, RequestContext context) {
   │                  ^^^^^^^^
   ╵
[INFO] Running build completed, took 15.9s

My function & the package example both work without RequestContext with Future return type and async keyword. They also work with a FutureOr<void> return type.

Why it builds on my local environment yet fails on docker & cloud run remains a mystery for now. All I can think was a dependency version discrepancy but I've pretty much locked all the dependency versions down to mirror cloud run and local.

kevmoo commented 1 month ago

have you tried printing out the pubspec.lock file in CI? Comparing the versions of Dart?

The only example I can think of is if the analyzer version has changed...

kevmoo commented 1 month ago
diff --git a/examples/json/lib/functions.dart b/examples/json/lib/functions.dart
index 4213df4..802a158 100644
--- a/examples/json/lib/functions.dart
+++ b/examples/json/lib/functions.dart
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.

+import 'dart:async';
+
 import 'package:functions_framework/functions_framework.dart';
 import 'package:json_annotation/json_annotation.dart';

@@ -59,7 +61,7 @@ class GreetingResponse {
 }

 @CloudFunction()
-GreetingResponse function(GreetingRequest request) {
+Future<GreetingResponse> function(GreetingRequest request) async {
   final name = request.name ?? 'World';
   final json = GreetingResponse(salutation: 'Hello', name: name);
   return json;

I tried changing up the example in the repro to make it work...and it's fine.

I might could change this to support FutureOr but that doesn't seem to be blocking you

jimmyff commented 1 month ago

Hey @kevmoo the issue only occurs when adding a RequestContext context parameter.

I've since modified my code not to require the context so this issue is not blocking me. I left the issue open because as far as I can tell, you should be able to use RequestContext context (Expected it to get picked up by the FunctionTarget: JsonHandler [FutureOr<ResponseType> Function(RequestType request, RequestContext context)]). The example code demonstrates the issue too with the additional RequestContext parameter.

Happy for this issue to be closed or left open.

kevmoo commented 1 month ago

Yep. Let's call it an enhancement!

kevmoo commented 1 month ago

I tried modifying the integration test file and it works perfectly.

I'm going to close this out.

If you can modify the integartion_test/lib/src/json_handlers.dart to reproduce the error, let me know.

diff --git a/integration_test/bin/server.dart b/integration_test/bin/server.dart
index 6b155d0..ad074d1 100644
--- a/integration_test/bin/server.dart
+++ b/integration_test/bin/server.dart
@@ -61,6 +61,29 @@ FunctionTarget? _nameToFunctionTarget(String name) => switch (name) {
             );
           },
         ),
+      'pubSubHandlerWithReplyAndContext' =>
+        JsonWithContextFunctionTarget.voidResult(
+          function_library.pubSubHandlerWithReplyAndContext,
+          (json) {
+            if (json is Map<String, dynamic>) {
+              try {
+                return function_library.PubSub.fromJson(json);
+              } catch (e, stack) {
+                throw BadRequestException(
+                  400,
+                  'There was an error parsing the provided JSON data.',
+                  innerError: e,
+                  innerStack: stack,
+                );
+              }
+            }
+            throw BadRequestException(
+              400,
+              'The provided JSON is not the expected type '
+              '`Map<String, dynamic>`.',
+            );
+          },
+        ),
       'jsonHandler' => JsonWithContextFunctionTarget(
           function_library.jsonHandler,
           (json) {
diff --git a/integration_test/lib/src/json_handlers.dart b/integration_test/lib/src/json_handlers.dart
index 7e7e9db..016d5d6 100644
--- a/integration_test/lib/src/json_handlers.dart
+++ b/integration_test/lib/src/json_handlers.dart
@@ -24,6 +24,17 @@ void pubSubHandler(PubSub pubSub, RequestContext context) {
   context.responseHeaders['multi'] = ['item1', 'item2'];
 }

+@CloudFunction()
+Future<void> pubSubHandlerWithReplyAndContext(
+  PubSub pubSub,
+  RequestContext context,
+) async {
+  print('subscription: ${pubSub.subscription}');
+  context.logger.info('subscription: ${pubSub.subscription}');
+  context.responseHeaders['subscription'] = pubSub.subscription;
+  context.responseHeaders['multi'] = ['item1', 'item2'];
+}
+
 @CloudFunction()
 FutureOr<bool> jsonHandler(
   Map<String, dynamic> request,
kevmoo commented 1 week ago

I guess this IS a problem. Investigating more!

https://github.com/GoogleCloudPlatform/functions-framework-dart/pull/472

kevmoo commented 1 week ago

It appears that pkg:analyzer TypeSystem.isSubtypeOf is broken in v6.5.2