actionHandler function
- Action action, {
- ContextProvider? contextProvider,
- 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);
}
};
}