invoke method

Future<FunctionsResponse> invoke(
  1. String functionName, {
  2. Map<String, String>? headers,
  3. Object? body,
  4. Iterable<MultipartFile>? files,
  5. Map<String, Object?>? queryParameters,
  6. HttpMethod method = HttpMethod.post,
  7. FunctionsRegion? region,
  8. Future<void>? abortSignal,
  9. Duration? requestTimeout,
})

Invokes a function

functionName is the name of the function to invoke

headers to send with the request

body of the request when files is null and can be of type String, Uint8List, or an Object that is encodable to JSON with jsonEncode. If files is not null, body represents the fields of the MultipartRequest and must be a map of strings to strings. A body that cannot be encoded throws a FunctionsFetchException before anything is sent.

files to send in a MultipartRequest. body is used for the fields.

queryParameters are added to the URL. A value is sent as its toString(), an Iterable value as one parameter per element, and a null value is left out. A query string in functionName is kept, and a parameter here with the same name replaces it.

region optionally specify the region to invoke the function in. When specified and not FunctionsRegion.any, adds both the x-region header and the forceFunctionRegion query parameter.

abortSignal cancels the in-flight request when the provided Future completes. It must not complete with an error. On abort, a RequestAbortedException is thrown. This is useful for cancelling a request in response to an event or for setting a request timeout:

// Event based
final abortSignal = Completer<void>();
abortSignal.complete(); // Call in some event handler to abort the request

try {
  final response = await supabase.functions.invoke(
    'hello-world',
    abortSignal: abortSignal.future,
  );
} on RequestAbortedException catch (error) {
  print('Request was aborted: $error');
}

// Timer based
final response = await supabase.functions.invoke(
  'hello-world',
  abortSignal: Future.delayed(Duration(seconds: 5)),
);

requestTimeout bounds how long the invocation may go without progress, overriding the timeout the client was created with. The timer restarts whenever a chunk of the request body is sent or a chunk of the response arrives, also on a streamed response while it is being listened to. A timed-out invocation is cancelled and throws a FunctionsFetchException caused by a TimeoutException, or emits the TimeoutException on the stream of a streamed response.

// Call a standard function
final response = await supabase.functions.invoke('hello-world');
print(response.data);

// Listen to Server Sent Events
final response = await supabase.functions.invoke('sse-function');
response.data
    .transform(const Utf8Decoder())
    .listen((value) {
      print(value);
    });

Streaming also works on the web, where the default HTTP client is fetch based.

Implementation

Future<FunctionsResponse> invoke(
  String functionName, {
  // ignore: avoid-shadowing
  Map<String, String>? headers,
  Object? body,
  Iterable<http.MultipartFile>? files,
  Map<String, Object?>? queryParameters,
  HttpMethod method = HttpMethod.post,
  FunctionsRegion? region,
  Future<void>? abortSignal,
  Duration? requestTimeout,
}) async {
  final effectiveRegion = region ?? _region;
  final forcedRegion = effectiveRegion == FunctionsRegion.any
      ? null
      : effectiveRegion?.value;
  final idleTimeout = IdleTimeout(requestTimeout ?? _requestTimeout);

  final effectiveQueryParameters = <String, Object>{
    if (queryParameters != null)
      for (final MapEntry(:key, :value) in queryParameters.entries)
        if (value != null)
          key: value is Iterable
              ? [for (final element in value) _queryValue(element)]
              : _queryValue(value),
    'forceFunctionRegion': ?forcedRegion,
  };

  final functionUri = Uri.parse('$_url/$functionName');
  final uri = effectiveQueryParameters.isEmpty
      ? functionUri
      : functionUri.replace(
          queryParameters: {
            ...functionUri.queryParametersAll,
            ...effectiveQueryParameters,
          },
        );

  final finalHeaders = <String, String>{
    ..._headers,
    ...?headers,
    'x-region': ?forcedRegion,
  };

  final http.BaseRequest request;
  if (files != null) {
    final fields = _multipartFields(body);

    // No content type is set here: a multipart request generates its own
    // boundary while it is being finalized and sets the header itself.
    request =
        idleTimeout.multipartRequest(
            method.value,
            uri,
            abortSignal: abortSignal,
          )
          ..headers.addAll(finalHeaders)
          ..fields.addAll(fields)
          ..files.addAll(files);
  } else {
    final bodyRequest = idleTimeout.request(
      method.value,
      uri,
      abortSignal: abortSignal,
    )..headers.addAll(finalHeaders);

    if (body != null) {
      // The content type is set before the body, so that a body given as a
      // string is encoded with the charset the caller asked for, and so that
      // the charset `Request.body` fills in for itself is kept.
      bodyRequest.headers.putIfAbsent(
        HttpHeader.contentType,
        () => switch (body) {
          Uint8List() => 'application/octet-stream',
          String() => 'text/plain',
          _ => 'application/json',
        },
      );

      if (body is String) {
        bodyRequest.body = body;
      } else if (body is Uint8List) {
        bodyRequest.bodyBytes = body;
      } else if (_isJson(bodyRequest.headers.mediaType ?? '') &&
          bodyRequest.encoding == utf8) {
        // `Request.body` leaves a JSON content type as it is, so encoding
        // straight to bytes sends the same request without the intermediate
        // string.
        bodyRequest.bodyBytes = await _encodeJson(
          body,
          _jsonCodec.encodeBytes,
        );
      } else {
        bodyRequest.body = await _encodeJson(body, _jsonCodec.encode);
      }
    }
    request = bodyRequest;
  }

  functionsLogger.finest(
    () => 'Request: ${request.method} ${request.url.loggable}',
  );

  final response = await _guardTransport(
    () => idleTimeout.send(request, _transport),
  );
  final responseType = response.headers.mediaType ?? 'text/plain';

  final isRelayError = response.headers['x-relay-error'] == 'true';
  final isSuccessStatus =
      isSuccessStatusCode(response.statusCode) && !isRelayError;

  if (responseType == 'text/event-stream' && isSuccessStatus) {
    // Only a successful streaming response hands the live stream to the
    // caller. On an error status there is nothing to stream, so the body is
    // drained below to become the exception `details` and the connection
    // isn't left open.
    return FunctionsResponse(
      data: response.stream,
      statusCode: response.statusCode,
      headers: response.headers,
    );
  }
  final Uint8List bodyBytes;
  try {
    bodyBytes = await response.stream.toBytes();
  } on http.RequestAbortedException {
    rethrow;
  } catch (error) {
    // A body that stops arriving on a success status leaves the caller with
    // nothing usable, so it is a transport failure, and so is a timeout on
    // any status. Otherwise, on an error status the status itself is the
    // answer, so it is kept with the read error as the details.
    if (isSuccessStatus || error is TimeoutException) {
      throw _unreadableResponse(error);
    }
    throw _apiException(response, isRelayError, details: error);
  }
  final dynamic data;
  if (_isJson(responseType)) {
    if (bodyBytes.isEmpty) {
      data = null;
    } else {
      dynamic decoded;
      try {
        decoded = await _jsonCodec.decodeBytes(bodyBytes);
      } on FormatException catch (error) {
        // A body labeled JSON that doesn't parse is only tolerated on an
        // error status, where the raw text still needs to reach the caller
        // as the exception `details`. On a success status it's a real
        // anomaly, so surface it instead of handing back a String.
        if (isSuccessStatus) throw _unreadableResponse(error);
        decoded = utf8.decode(bodyBytes, allowMalformed: true);
      }
      data = decoded;
    }
  } else if (responseType == 'application/octet-stream') {
    data = bodyBytes;
  } else if (isSuccessStatus) {
    try {
      data = utf8.decode(bodyBytes);
    } on FormatException catch (error) {
      throw _unreadableResponse(error);
    }
  } else {
    data = utf8.decode(bodyBytes, allowMalformed: true);
  }

  if (isSuccessStatus) {
    return FunctionsResponse(
      data: data,
      statusCode: response.statusCode,
      headers: response.headers,
    );
  }
  throw _apiException(
    response,
    isRelayError,
    details: data,
    body: utf8.decode(bodyBytes, allowMalformed: true),
  );
}