actionHandler function

GenkitHttpHandler actionHandler(
  1. Action action, {
  2. ContextProvider? contextProvider,
  3. bool sendLegacyErrorFrame = false,
})

Serves a single action (flow, model, tool, ...) as a framework-neutral GenkitHttpHandler.

Speaks the Genkit client protocol: POST {"data": ..., "init": ...}, answered with {"result": ...}, or streamed with ?stream=true or Accept: text/event-stream. So defineRemoteAction, defineRemoteModel and remoteAgent from package:genkit/client.dart can call it.

This is the building block for HTTP framework adapters. To serve actions directly, use GenkitRouter or ioHandler instead.

sendLegacyErrorFrame makes a failed stream end with an error: {"error": ...} frame instead of data: {"error": ...}. Only Dart clients from package:genkit 0.17 and earlier need it: they don't recognize the data: form and report a generic "stream finished" error instead of the server's. Enable it while such clients (typically shipped Flutter apps) are still in use, then remove it.

Implementation

GenkitHttpHandler actionHandler(
  Action action, {
  ContextProvider? contextProvider,
  bool sendLegacyErrorFrame = false,
}) {
  return (GenkitHttpRequest request) async {
    if (request.method != 'POST') {
      return GenkitHttpResponse(
        statusCode: 405,
        headers: const {'allow': 'POST'},
      );
    }

    final isStreaming =
        request.headers['accept'] == 'text/event-stream' ||
        request.queryParameters['stream'] == 'true';

    String bodyStr;
    try {
      bodyStr = await utf8.decodeStream(request.body);
    } catch (_) {
      return _errorResponse(
        StatusCode.invalidArgument,
        'Failed to read request body',
      );
    }

    Object? input;
    Object? init;
    try {
      if (bodyStr.isNotEmpty) {
        final jsonBody = jsonDecode(bodyStr);
        if (jsonBody is! Map || !jsonBody.containsKey('data')) {
          return _errorResponse(
            StatusCode.invalidArgument,
            'Request body must be a JSON object with a "data" field.',
          );
        }
        input = jsonBody['data'];
        init = jsonBody['init'];
      }
      if (action.inputSchema != null && input != null) {
        input = action.inputSchema!.parse(input);
      }
      if (action.initSchema != null && init != null) {
        init = action.initSchema!.parse(init);
      }
    } catch (e) {
      return _errorResponse(StatusCode.invalidArgument, 'Invalid input: $e');
    }

    Map<String, dynamic>? context;
    if (contextProvider != null) {
      try {
        context = await contextProvider(
          RequestData(
            method: request.method,
            headers: request.headers,
            input: input,
          ),
        );
      } on GenkitException catch (e) {
        return _errorResponse(e.status, e.message);
      } catch (_) {
        // Same rule as _clientError: only GenkitException messages are meant
        // for the client. Others may come from JWT or database libraries and
        // carry key ids, hosts or queries.
        return _errorResponse(StatusCode.permissionDenied, 'Permission denied');
      }
    }

    if (isStreaming) {
      return _runStreaming(
        action,
        input,
        init,
        context,
        sendLegacyErrorFrame: sendLegacyErrorFrame,
      );
    }
    try {
      final result = await action.run(input, context: context, init: init);
      return GenkitHttpResponse(
        statusCode: 200,
        headers: {
          'content-type': 'application/json',
          ..._traceHeaders(result.traceId, result.spanId),
        },
        body: Stream.value(utf8.encode(jsonEncode({'result': result.result}))),
      );
    } catch (e) {
      final (status, message) = _clientError(e);
      return _errorResponse(status, message);
    }
  };
}