Flutter集成GetStream偶发1000未授权token未定义报错排查
问题现象
- 偶发报错,复现路径不固定,调试难度高,错误信息为
StreamChatNetworkError(code: 1000. message: Unauthorised, token not defined) - 临时解决方案为登出后重新登录,强制重连GetStream服务重新获取token,但该方案严重影响用户体验
- 报错固定在打开双人聊天消息页时触发
- 业务逻辑要求用户必须先在首页完成Stream连接流程才能进入消息页,理论上打开消息页时不会出现token未授权问题
相关代码片段
main.dart 客户端初始化代码
void main() async { WidgetsFlutterBinding.ensureInitialized(); await Firebase.initializeApp(); final client = StreamChatClient( STREAM_API_KEY, logLevel: Level.OFF, ); runApp(MyApp( client: client, )); } class MyApp extends StatelessWidget { const MyApp({Key? key, required this.client}) : super(key: key); final StreamChatClient client; @override Widget build(BuildContext context) { return Provider<BaseAuth>( create: (context) => Auth(), child: MaterialApp( title: 'App', theme: ThemeData( primarySwatch: Colors.indigo, ), home: const SplashscreenWidget(), debugShowCheckedModeBanner: false, builder: (context, widget) => StreamChat( client: client, child: widget!, ), ), ); } }
首页Stream连接逻辑代码
void initState() { super.initState(); // 仅从注册/登录页进入时才连接用户 if(widget.fromSignUpOrLogin) { connectUser = _connectUser(); } } _connectUser() async { final client = StreamChatCore.of(context).client; FireStoreDatabase db = FireStoreDatabase(uid: FirebaseAuth.instance.currentUser!.uid); AppUser appUser = await db.readUser(); final user = su.User(id: uid, extraData: { "name": appUser.username, "image": appUser.photoUrl, } ); await client.connectUser( user, appUser.streamToken ); }
双人聊天消息页代码
late Channel globalChannel; initMessageSystem() async { globalChannel = widget.client.channel('messaging', id: widget.channelId); await globalChannel.watch(); } @override void initState() { super.initState(); initMessageSystem(); } @override Widget build(BuildContext context) { return StreamChat( client: widget.client, child: StreamChannel( child: _buildBody(), channel: globalChannel, ), streamChatThemeData: StreamChatThemeData( colorTheme: StreamColorTheme.light( accentPrimary: Colors.purpleAccent, textHighEmphasis: Colors.purpleAccent, ), ownMessageTheme: const StreamMessageThemeData( messageBackgroundColor: Colors.purpleAccent, messageTextStyle: TextStyle( color: Colors.white ) ) ) ); }
解决方案
问题根因
- 首页连接逻辑仅靠
fromSignUpOrLogin标记触发,冷启动APP、应用从后台恢复、websocket意外断开、token过期等场景下不会自动重连,此时进入消息页直接调用channel.watch()就会触发未授权错误 - 消息页重复嵌套
StreamChat根组件,会重置客户端上下文状态,偶发导致上层已建立的连接状态丢失,是触发偶现报错的核心原因 - 缺少连接状态前置校验和异常重试逻辑,没有对token失效场景做兜底处理
1. 客户端连接状态校验方法
GetStream Dart客户端内置了连接状态判断属性,无需自行维护标记位:
// 校验当前是否存在有效用户连接 bool isValidConnected = client.state.currentUser != null && client.wsConnectionStatus == ConnectionStatus.connected;
client.state.currentUser:非空代表已执行过connectUser流程,用户身份信息已写入客户端client.wsConnectionStatus:连接状态枚举,ConnectionStatus.connected代表与Stream服务的websocket连接正常,connecting代表正在连接,disconnected代表连接断开
2. 具体修复步骤
- 第一步:移除消息页build方法中嵌套的
StreamChat根组件,全局仅保留main.dart中MaterialApp builder内的StreamChat实例即可。页面级主题配置可以通过StreamChatTheme组件单独传入,重复包裹根组件会生成新的状态上下文,导致无法读取上层已建立的连接信息。 - 第二步:修改首页连接逻辑,不再仅靠
fromSignUpOrLogin标记判断是否需要连接,每次进入首页都先校验连接状态,未连接则自动执行连接流程:
@override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { final client = StreamChatCore.of(context).client; if (client.state.currentUser == null || client.wsConnectionStatus != ConnectionStatus.connected) { await _connectUser(); } }); }
- 第三步:在消息页
initMessageSystem方法中增加前置校验和异常捕获,避免直接调用watch触发未授权错误:
initMessageSystem() async { final client = widget.client; // 前置校验连接状态,无效则先重连 if (client.state.currentUser == null || client.wsConnectionStatus != ConnectionStatus.connected) { FireStoreDatabase db = FireStoreDatabase(uid: FirebaseAuth.instance.currentUser!.uid); AppUser appUser = await db.readUser(); final user = su.User( id: FirebaseAuth.instance.currentUser!.uid, extraData: { "name": appUser.username, "image": appUser.photoUrl, } ); await client.connectUser(user, appUser.streamToken); } globalChannel = client.channel('messaging', id: widget.channelId); // 增加异常重试逻辑 try { await globalChannel.watch(); } on StreamChatNetworkError catch (e) { if (e.code == 1000) { // token失效场景,拉取新token后重试一次 final newToken = await 你封装的获取Stream用户Token方法(); await client.connectUser(client.state.currentUser!, newToken); await globalChannel.watch(); } else { rethrow; } } }
3. 额外优化建议
- 可配置全局token过期监听,统一处理token刷新逻辑,无需在每个页面单独实现:
client.on(EventType.tokenExpired).listen((event) async { // 拉取新token后调用client.connectUser更新连接即可 });
- 调试阶段不要将
logLevel设置为Level.OFF,调整为Level.INFO可以从日志中看到websocket断开、token失效的具体时机,大幅降低偶现问题的定位成本。
内容的提问来源于stack exchange,提问作者youcantgetridofmestackoverflow
相关产品推荐
相关产品推荐

