Flutter中Image.network()请求图片链接时返回403解决方案

Flutter中Image.network()请求图片链接时返回403解决方案

问题复现

我有一个链接为 p2.music.126.net/5CJeYN35Lnz...图片,现在我通过调用 Image.network("https://p2.music.126.net/5CJeYN35LnzRDsv5Lcs0-Q==/109951165374966765.jpg")这个方法,控制台报错输出403

dart 复制代码
======== Exception caught by image resource service ================================================
The following NetworkImageLoadException was thrown resolving an image codec:
HTTP request failed, statusCode: 403, https://p2.music.126.net/5CJeYN35LnzRDsv5Lcs0-Q==/109951165374966765.jpg

When the exception was thrown, this was the stack: 
#0      NetworkImage._loadAsync (package:flutter/src/painting/_network_image_io.dart:152:9)
<asynchronous suspension>
Image provider: NetworkImage("https://p2.music.126.net/5CJeYN35LnzRDsv5Lcs0-Q==/109951165374966765.jpg", scale: 1.0)
Image key: NetworkImage("https://p2.music.126.net/5CJeYN35LnzRDsv5Lcs0-Q==/109951165374966765.jpg", scale: 1.0)
====================================================================================================

问题产生原因分析

浏览器请求发现是正常的

Postman也能正常获取图片二进制数据

而我测试的时候通过第三方dio库加上请求头也能获取到正常二进制数据

dart 复制代码
// 获取二进制数据
Future<ResultData> getBytes(
  String path, {
  Map<String, dynamic>? queryParameters,
  MyOptions? myOptions,
}) async {
  myOptions ??= MyOptions(); // 为空则创建
  myOptions.method = "GET";
  myOptions.responseType = ResponseType.bytes; // 设置返回类型为二进制格式
  myOptions.headers = {"User-Agent": bytesUserAgent};
  Response response = await _dio.get(path,
      queryParameters: queryParameters, options: myOptions);
  return response.data as ResultData;
}

但是通过Image.network()中添加请求头却发现还是报错,所以我初步怀疑FlutterImage.network()网络请求部分出了问题

我们通过查看Image.network()源码

dart 复制代码
  Image.network(
    String src, {
    super.key,
    double scale = 1.0,
    this.frameBuilder,
    this.loadingBuilder,
    this.errorBuilder,
    this.semanticLabel,
    this.excludeFromSemantics = false,
    this.width,
    this.height,
    this.color,
    this.opacity,
    this.colorBlendMode,
    this.fit,
    this.alignment = Alignment.center,
    this.repeat = ImageRepeat.noRepeat,
    this.centerSlice,
    this.matchTextDirection = false,
    this.gaplessPlayback = false,
    this.filterQuality = FilterQuality.low,
    this.isAntiAlias = false,
    Map<String, String>? headers,
    int? cacheWidth,
    int? cacheHeight,
  }) : image = ResizeImage.resizeIfNeeded(cacheWidth, cacheHeight, NetworkImage(src, scale: scale, headers: headers)),
       assert(cacheWidth == null || cacheWidth > 0),
       assert(cacheHeight == null || cacheHeight > 0);

可知Image.network()方法中创建了一个NetworkImage的对象,我们跟进查看其中源码

dart 复制代码
abstract class NetworkImage extends ImageProvider<NetworkImage> {
  /// Creates an object that fetches the image at the given URL.
  ///
  /// The arguments [url] and [scale] must not be null.
  const factory NetworkImage(String url, { double scale, Map<String, String>? headers }) = network_image.NetworkImage;

  /// The URL from which the image will be fetched.
  String get url;

  /// The scale to place in the [ImageInfo] object of the image.
  double get scale;

  /// The HTTP headers that will be used with [HttpClient.get] to fetch image from network.
  ///
  /// When running flutter on the web, headers are not used.
  Map<String, String>? get headers;

  @override
  ImageStreamCompleter load(NetworkImage key, DecoderCallback decode);

  @override
  ImageStreamCompleter loadBuffer(NetworkImage key, DecoderBufferCallback decode);

  @override
  ImageStreamCompleter loadImage(NetworkImage key, ImageDecoderCallback decode);
}

我们可以发现这段代码定义了一个名为NetworkImage的工厂函数,它接受一个字符串类型的url和一个可选的scale参数(双精度浮点型),还有一个可选的headers参数(字符串映射),因这里是一个抽象类,所以我们还得继续跟进查看,点击network_image.NetworkImageNetworkImage

通过阅读源代码,我们可以发现

dart 复制代码
Future<ui.Codec> _loadAsync(
  NetworkImage key,
  StreamController<ImageChunkEvent> chunkEvents, {
  image_provider.ImageDecoderCallback? decode,
  image_provider.DecoderBufferCallback? decodeBufferDeprecated,
  image_provider.DecoderCallback? decodeDeprecated,
}) async {
  try {
    assert(key == this);
    final Uri resolved = Uri.base.resolve(key.url);
    final HttpClientRequest request = await _httpClient.getUrl(resolved);
    headers?.forEach((String name, String value) {
      request.headers.add(name, value);
    });
    final HttpClientResponse response = await request.close();
    if (response.statusCode != HttpStatus.ok) {
      // The network may be only temporarily unavailable, or the file will be
      // added on the server later. Avoid having future calls to resolve
      // fail to check the network again.
      await response.drain<List<int>>(<int>[]);
      throw image_provider.NetworkImageLoadException(
          statusCode: response.statusCode, uri: resolved);
    }
    final Uint8List bytes = await consolidateHttpClientResponseBytes(
      response,
      onBytesReceived: (int cumulative, int? total) {
        chunkEvents.add(ImageChunkEvent(
          cumulativeBytesLoaded: cumulative,
          expectedTotalBytes: total,
        ));
      },
    );
    if (bytes.lengthInBytes == 0) {
      throw Exception('NetworkImage is an empty file: $resolved');
    }
    if (decode != null) {
      final ui.ImmutableBuffer buffer =
          await ui.ImmutableBuffer.fromUint8List(bytes);
      return decode(buffer);
    } else if (decodeBufferDeprecated != null) {
      final ui.ImmutableBuffer buffer =
          await ui.ImmutableBuffer.fromUint8List(bytes);
      return decodeBufferDeprecated(buffer);
    } else {
      assert(decodeDeprecated != null);
      return decodeDeprecated!(bytes);
    }
  } catch (e) {
    // Depending on where the exception was thrown, the image cache may not
    // have had a chance to track the key in the cache at all.
    // Schedule a microtask to give the cache a chance to add the key.
    scheduleMicrotask(() {
      PaintingBinding.instance.imageCache.evict(key);
    });
    rethrow;
  } finally {
    chunkEvents.close();
  }
}

Future<ui.Codec> _loadAsync()这个函数是Image.network()请求网络数据的地方,根据前面分析,此报错大概率是由于请求的某个参数出现问题导致的

通过阅读

dart 复制代码
if (response.statusCode != HttpStatus.ok) {
  // The network may be only temporarily unavailable, or the file will be
  // added on the server later. Avoid having future calls to resolve
  // fail to check the network again.
  await response.drain<List<int>>(<int>[]);
  throw image_provider.NetworkImageLoadException(
      statusCode: response.statusCode, uri: resolved);
}

我们可以知道,控制栏报错403是因为在这个抛出了一个异常,我们可以断定 final HttpClientResponse response = await request.close(); 这段代码是发起网络请求,我们试着在前面打印一下请求头

输出

dart 复制代码
I/flutter (31164): user-agent: Dart/3.1 (dart:io)
I/flutter (31164): accept-encoding: gzip
I/flutter (31164): content-length: 0
I/flutter (31164): host: p2.music.126.net

这跟我们分析的一样,user-agent里面多了一个Dart/3.1 (dart:io),是问题产生的原因

解决方案

我们尝试着除去这个用户标识体

添加如下代码

dart 复制代码
// 去除user-agent中的 dart/3.1
request.headers.remove("user-agent", "Dart/3.1 (dart:io)");

此时再次运行发现图片可以正常显示了

相关推荐
负责的蛋挞1 小时前
异步HttpModule的实现方式
java·服务器·前端
丹宇码农4 小时前
把 HLS 字幕玩出花:zwPlayer 如何让 M3U8 视频支持全文搜索、翻译与码率自适应
前端·javascript·音视频·hls·视频播放器
2501_943782354 小时前
【共创季稿事节】猜数字游戏:二分法思维与交互式反馈
前端·游戏·microsoft·harmonyos·鸿蒙·鸿蒙系统
GV191rLvq4 小时前
基于Socket实现的最简单的Web服务器【ASP.NET原理分析】
服务器·前端·asp.net
吠品4 小时前
LangChain 里 tool_call_id 为空?一次 MCP 工具集成的排查记录
前端
微信开发api-视频号协议4 小时前
企业微信二次开发中的文件系统设计:媒体资源、临时文件与业务附件
前端·微信·企业微信·媒体·ipad·微信开放平台
柒和远方4 小时前
Phase 7.4 学习博客:为什么多 API 项目需要 Swagger / OpenAPI
前端·后端·架构
张龙6874 小时前
拼多多开放平台对接踩坑实录:从 CLIENT_ID 配置到 MD5 签名算法的完整填坑指南
前端
GuWenyue5 小时前
提示词彻底过时?一套上下文工程方案,3步让LLM落地生产,代码直接复用
前端·javascript·人工智能