send method

Send request and return a GatewayHandoffResponse. Throws GatewayError for any transport-level failure.

Implementation

@override
Future<GatewayHandoffResponse> send(
  GatewayTransactionRequest request,
) async {
  if (_closed) {
    throw const GatewayError(
      code: GatewayErrorCode.internalError,
      message: 'DefaultGatewayClient.send called after close()',
    );
  }
  final baseUrl = config.backendBaseUrl;
  if (baseUrl == null || baseUrl.isEmpty) {
    throw const GatewayError(
      code: GatewayErrorCode.notConfigured,
      message:
          'GatewayConfig.backendBaseUrl is required in gatewayHandoff mode.',
    );
  }
  final url = _joinUrl(baseUrl, path);

  final headers = <String, String>{
    'Content-Type': 'application/json',
    ...config.extraHeaders,
  };
  final tokenProvider = config.authTokenProvider;
  if (tokenProvider != null) {
    final token = await tokenProvider(GatewayTokenScope.transactionHandoff);
    if (token != null && token.isNotEmpty) {
      headers['Authorization'] = 'Bearer $token';
    }
  }
  final body = jsonEncode(request.toJson());

  final policy = config.retryPolicy;
  final maxAttempts = policy.maxAttempts < 1 ? 1 : policy.maxAttempts;
  var attempt = 0;
  GatewayError? lastError;
  while (attempt < maxAttempts) {
    attempt++;
    try {
      final response = await transport.send(
        GatewayTransportRequest(
          method: 'POST',
          url: url,
          headers: headers,
          body: body,
          timeout: config.requestTimeout,
        ),
      );
      return _decode(response, attempt: attempt);
    } on GatewayError catch (err) {
      // A backend-error / response-invalid is non-retryable;
      // only transport-level errors retry below.
      if (err.code != GatewayErrorCode.transportError &&
          err.code != GatewayErrorCode.transportTimeout) {
        rethrow;
      }
      lastError = err;
    } on GatewayTransportError catch (te) {
      lastError = GatewayError(
        code: te.isTimeout
            ? GatewayErrorCode.transportTimeout
            : GatewayErrorCode.transportError,
        message: te.message,
        attempt: attempt,
        recoverable: true,
      );
    } catch (e) {
      // Wrap unknown exceptions in a transport error so
      // the retry policy can apply uniformly.
      lastError = GatewayError(
        code: GatewayErrorCode.transportError,
        message: 'Unexpected transport exception: ${e.runtimeType}',
        attempt: attempt,
        recoverable: true,
      );
    }

    if (attempt < maxAttempts) {
      final backoff = _backoffFor(attempt, policy);
      await Future<void>.delayed(backoff);
    }
  }
  throw lastError ??
      const GatewayError(
        code: GatewayErrorCode.internalError,
        message: 'DefaultGatewayClient exhausted retries with no error.',
      );
}