基于flutter_bloc的Stripe支付集成出现异步挂起错误
Flutter Stripe支付集成问题:Bloc异步处理中出现JSON解析格式错误
几周前Flutter Stripe支付功能可正常完成,现在使用flutter_bloc处理Event时出现异步挂起错误,已严格遵循Bloc官方文档使用async/await,但问题仍未解决。
错误信息
[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: FormatException: Unexpected character (at line 2, character 1) <html><head> ^ #0 _ChunkedJsonParser.fail (dart:convert-patch/convert_patch.dart:1383:5) #1 _ChunkedJsonParser.parseNumber (dart:convert-patch/convert_patch.dart:1250:9) #2 _ChunkedJsonParser.parse (dart:convert-patch/convert_patch.dart:915:22) #3 _parseJson (dart:convert-patch/convert_patch.dart:35:10) #4 JsonDecoder.convert (dart:convert/json.dart:612:36) #5 JsonCodec.decode (dart:convert/json.dart:216:41) #6 PaymentBloc._callPayEndpointMethodId package:flutter_app_social/…/paymentbloc/payment_bloc.dart:136 <asynchronous suspension> #7 PaymentBloc._onPaymentCreateIntent package:flutter_app_social/…/paymentbloc/payment_bloc.dart:38 <asynchronous suspension> #8 Bloc.on.<anonymous closure>.handleEvent package:bloc/src/bloc.dart:226 <asynchronous suspension>
源码文件
payment_state.dart
part of 'payment_bloc.dart'; enum PaymentStatus { initial, loading, success, failure } class PaymentState extends Equatable { final PaymentStatus status; final CardFieldInputDetails inputDetails; const PaymentState({ this.status = PaymentStatus.initial, this.inputDetails = const CardFieldInputDetails(complete: false), }); PaymentState copyWith({ PaymentStatus? status, CardFieldInputDetails? inputDetails, }) { return PaymentState( status: status ?? this.status, inputDetails: inputDetails ?? this.inputDetails, ); } @override List<Object> get props => [status, inputDetails]; }
payment_event.dart
part of 'payment_bloc.dart'; abstract class PaymentEvent extends Equatable { const PaymentEvent(); @override List<Object> get props => []; } class PaymentStart extends PaymentEvent {} class PaymentCreateIntent extends PaymentEvent { final BillingDetails billingDetails; final List<Map<String, dynamic>> items; const PaymentCreateIntent({ required this.billingDetails, required this.items, }); @override List<Object> get props => [billingDetails, items]; } class PaymentConfirmIntent extends PaymentEvent { final String clientSecret; const PaymentConfirmIntent({ required this.clientSecret, }); @override List<Object> get props => [clientSecret]; }
payment_bloc.dart
import 'dart:async'; import 'dart:convert'; import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_stripe/flutter_stripe.dart'; import 'package:http/http.dart' as http; part 'payment_event.dart'; part 'payment_state.dart'; class PaymentBloc extends Bloc<PaymentEvent, PaymentState> { PaymentBloc() : super(const PaymentState()) { on<PaymentStart>(_onPaymentStart); on<PaymentCreateIntent>(_onPaymentCreateIntent); on<PaymentConfirmIntent>(_onPaymentConfirmIntent); } void _onPaymentStart( PaymentStart event, Emitter<PaymentState> emit, ) async { emit(state.copyWith(status: PaymentStatus.initial)); } void _onPaymentCreateIntent( PaymentCreateIntent event, Emitter<PaymentState> emit, ) async { emit(state.copyWith(status: PaymentStatus.loading)); final paymentMethod = await Stripe.instance.createPaymentMethod( params: PaymentMethodParams.card( paymentMethodData: PaymentMethodData(billingDetails: event.billingDetails), ), ); final paymentIntentResults = await _callPayEndpointMethodId( useStripeSdk: true, paymentMethodId: paymentMethod.id, currency: 'usd', items: event.items, ); print(paymentIntentResults); if (paymentIntentResults['error'] != null) { emit( state.copyWith(status: PaymentStatus.failure), ); } if (paymentIntentResults['clientSecret'] != null && paymentIntentResults['requiresAction'] == null) { emit( state.copyWith(status: PaymentStatus.success), ); } if (paymentIntentResults['clientSecret'] != null && paymentIntentResults['requiresAction'] == null) { final String clientSecret = paymentIntentResults['clientSecret']; add( PaymentConfirmIntent(clientSecret: clientSecret), ); } } void _onPaymentConfirmIntent( PaymentConfirmIntent event, Emitter<PaymentState> emit, ) async { try { final paymentIntent = await Stripe.instance.handleNextAction(event.clientSecret); if (paymentIntent.status == PaymentIntentsStatus.RequiresConfirmation) { Map<String, dynamic> results = await _callPayEndpointIntentId( paymentIntentId: paymentIntent.id, ); if (results['error'] != null) { emit(state.copyWith(status: PaymentStatus.failure)); } else { emit(state.copyWith(status: PaymentStatus.success)); } } } catch (e) { print(e); emit( state.copyWith(status: PaymentStatus.failure), ); } } Future<Map<String, dynamic>> _callPayEndpointIntentId({ required String paymentIntentId, }) async { final url = Uri.parse( 'https://us-central1-evental-acdgnf69.cloudfunctions.net/StripePayEndpointIntentId', ); final response = await http.post( url, headers: {'Content-Type': 'application/json'}, body: json.encode( { 'paymentIntentId': paymentIntentId, }, ), ); return json.decode(response.body); } Future<Map<String, dynamic>> _callPayEndpointMethodId({ required bool useStripeSdk, required String paymentMethodId, required String currency, required List<Map<String, dynamic>> items, }) async { final url = Uri.parse( 'https://us-central1-evental-acdgnf69.cloudfunctions.net/StripePayEndpointMethodId', ); final response = await http.post( url, headers: {'Content-Type': 'application/json'}, body: json.encode( { 'useStripeSdk': useStripeSdk, 'paymentMethodId': paymentMethodId, 'currency': currency, 'items': items, }, ), ); print(response.body); return json.decode(response.body); } }
stripe_form_page.dart 按钮触发代码
ElevatedButton( onPressed: () { (controller.details.complete) ? context.read<PaymentBloc>().add( const PaymentCreateIntent(...
问题分析与解决方案
问题根源
从错误栈可以明确,错误发生在_callPayEndpointMethodId方法的json.decode(response.body)步骤,原因是后端接口返回的不是预期的JSON数据,而是HTML内容(错误提示中的<html><head>)。这说明你的Stripe后端云函数(StripePayEndpointMethodId)出现了异常,可能是部署故障、权限问题、Stripe密钥配置错误或业务逻辑报错,导致返回了错误页面而非正常JSON响应。
解决步骤
检查后端云函数状态
- 确认
StripePayEndpointMethodId和StripePayEndpointIntentId云函数是否正常部署并可访问 - 检查云函数中的Stripe API密钥是否有效,是否有足够的权限完成支付操作
- 查看云函数的日志,定位具体的后端错误原因
- 确认
增加HTTP响应合法性校验
在两个接口调用方法中,先判断响应状态码是否正常,再尝试解析JSON,避免直接解析错误内容:Future<Map<String, dynamic>> _callPayEndpointMethodId({ required bool useStripeSdk, required String paymentMethodId, required String currency, required List<Map<String, dynamic>> items, }) async { final url = Uri.parse( 'https://us-central1-evental-acdgnf69.cloudfunctions.net/StripePayEndpointMethodId', ); final response = await http.post( url, headers: {'Content-Type': 'application/json'}, body: json.encode( { 'useStripeSdk': useStripeSdk, 'paymentMethodId': paymentMethodId, 'currency': currency, 'items': items, }, ), ); print(response.body); // 新增状态码校验 if (response.statusCode >= 400) { throw Exception('请求失败,状态码:${response.statusCode},响应内容:${response.body}'); } try { return json.decode(response.body); } catch (e) { throw Exception('JSON解析失败:$e,响应内容:${response.body}'); } }另一个
_callPayEndpointIntentId方法做同样修改。完善Bloc事件处理的异常捕获
在_onPaymentCreateIntent方法外层增加全局try-catch,避免未处理的异常导致应用崩溃:void _onPaymentCreateIntent( PaymentCreateIntent event, Emitter<PaymentState> emit, ) async { try { emit(state.copyWith(status: PaymentStatus.loading)); final paymentMethod = await Stripe.instance.createPaymentMethod( params: PaymentMethodParams.card( paymentMethodData: PaymentMethodData(billingDetails: event.billingDetails), ), ); final paymentIntentResults = await _callPayEndpointMethodId( useStripeSdk: true, paymentMethodId: paymentMethod.id, currency: 'usd', items: event.items, ); print(paymentIntentResults); if (paymentIntentResults['error'] != null) { emit(state.copyWith(status: PaymentStatus.failure)); return; } if (paymentIntentResults['clientSecret'] != null) { if (paymentIntentResults['requiresAction'] == null) { emit(state.copyWith(status: PaymentStatus.success)); } else { final String clientSecret = paymentIntentResults['clientSecret']; add(PaymentConfirmIntent(clientSecret: clientSecret)); } } else { emit(state.copyWith(status: PaymentStatus.failure)); } } catch (e) { print(e); emit(state.copyWith(status: PaymentStatus.failure)); } }校验事件触发参数
确认按钮触发PaymentCreateIntent时,传递的billingDetails和items数据格式正确,没有空值或不符合后端要求的结构。
内容的提问来源于stack exchange,提问作者Alex Chalakov
相关产品推荐
相关产品推荐

