Flutter - 解决Connection closed before full header was received
 # 一、概述
我们的 App 是集成了 Sentry 进行错误跟踪和性能监控,在最近几个月里经常看到如下错误
ClientException: Connection closed before full header was received, uri=https://xxx.png
加载图片的时候报错了,但是尝试了很多次,自己始终无法复现出来~
# 二、摸索
后面进行搜索找到了 2019 年 12 月的一个 issue
- https://github.com/flutter/flutter/issues/41573
 
不太一样的是人家是请求接口的时候出现的报错。
提出 issue 的作者当时是通过 Method Channel 的方式由原生端去请求来解决这个问题,即避开了使用 http 这个包。
有人说这种情况,改用 dio 即可~

也有人说这种情况,加个延迟就可以解决~

但我们这种加载图片场景哪有机会加延迟呢,当然用这种方式也不靠谱~
时隔两年后(2021 年 12 月),根据 issue 的种种描述,有人确定了问题,并提了一个 Dart 的 issue
- https://github.com/dart-lang/sdk/issues/47841
 
大致上就是说 Dart 端拒绝了服务器发起的 TLS 重协商导致。
重协商是指在已经协商好的SSL/TLS TCP连接上重新协商, 用以更换算法、更换数字证书、重新验证对方身份、更新共享密钥等。 --摘自《SSL/TLS攻击介绍--重协商漏洞攻击 (opens new window)》
我想 Dart 不允许 TLS 重协商应该也是出于安全考虑吧~
# 三、解决
提 issue 的作者也为此,于 2022 年 2 月份提了 PR,给 SecurityContext 添加了 allowLegacyUnsafeRenegotiation 属性以允许重协商,在同年 4 月份得到合并了,但依旧出于安全考虑,默认为关闭状态,是否开启由开发者自己决定。
附上该属性的相关说明:https://api.flutter.dev/flutter/dart-io/SecurityContext/allowLegacyUnsafeRenegotiation.html
以及相关修复:Allow sockets to enable TLS renegotiation. · dart-lang/sdk@c286b76 (github.com) (opens new window)
注:该属性从 Dart 2.17.0 开始存在,对应的 Flutter 版本是 3.0.0。
加了 allowLegacyUnsafeRenegotiation 属性之后,我们可以按如下代码去使用
void main() async {
  final context = SecurityContext.defaultContext;
  context.allowLegacyUnsafeRenegotiation = true;
  final httpClient = HttpClient(context: context);
  final client = IOClient(httpClient);
  await client.get(Uri.parse('https://your_uri.net'));
}
代码取自:https://stackoverflow.com/a/73287131/8577739
# 四、调整 CachedNetworkImage
在我们的项目中,所使用的图片加载库为 cached_network_image,接下来我们就一起来看看如何调整以开启 TLS 重协商功能。
在 CachedNetworkImage 类中有一个 cacheManager 属性,如果我们不设置的话则后续默认使用的是 DefaultCacheManager 实例
class DefaultCacheManager extends CacheManager with ImageCacheManager {
  static const key = 'libCachedImageData';
  static final DefaultCacheManager _instance = DefaultCacheManager._();
  factory DefaultCacheManager() {
    return _instance;
  }
  DefaultCacheManager._() : super(Config(key));
}
可以看到其继承于 CacheManager
class CacheManager implements BaseCacheManager {
  ....
  CacheManager(Config config)
      : _config = config,
        _store = CacheStore(config) {
    _webHelper = WebHelper(_store, config.fileService);
  }
  ...
}
CacheManager 接收一个 Config 对象
abstract class Config {
  ...
  /// [fileService] defines where files are fetched, for example online.
  factory Config(
    String cacheKey, {
    Duration stalePeriod,
    int maxNrOfCacheObjects,
    CacheInfoRepository repo,
    FileSystem fileSystem,
    FileService fileService,
  }) = impl.Config;
  ...
  FileService get fileService;
}
这里我们就可以利用 fileService 来指定 httpClient,以此帮我们解决上述报错问题
static HttpClient httpClient() {
  final context = SecurityContext.defaultContext;
  context.allowLegacyUnsafeRenegotiation = true;
  return HttpClient(context: context);
}
// 自定义 CacheManager
class _LXFCachedNetworkImageManager extends CacheManager
    with ImageCacheManager {
  // 为了不影响之前的缓存,保持使用相同的 key
  static String get key => DefaultCacheManager.key;
  static final _LXFCachedNetworkImageManager _instance =
      _LXFCachedNetworkImageManager._();
  factory _LXFCachedNetworkImageManager() {
    return _instance;
  }
  _LXFCachedNetworkImageManager._()
      : super(
          Config(
            key,
            // 指定 httpClient
            fileService: HttpFileService(
              httpClient: IOClient(httpClient()),
            ),
          ),
        );
}
使用时配置上 cacheManager 即可,如下所示
CachedNetworkImage(
  imageUrl: '', 
  cacheManager: _LXFCachedNetworkImageManager(),
);
但每个地方都这样写实在是太不优雅了,所以大家自行进行封装吧~
本篇到此结束,感谢大家的支持,我们下次再见! 👋

- 01
 - Flutter 多仓库本地 Monorepo 方案与体验优化10-25
 
- 02
 - Flutter webview 崩溃率上升怎么办?我的分析与解决方案10-19
 
- 03
 - Flutter - Melos Pub workspaces 实践10-12