You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Web无法通过代码保存跨子域API下载的文件

解决Flutter Web跨域下载文件静默失败的方案

问题根源分析

当前方案存在两个核心问题:

  1. 用Base64编码处理14MB的大文件会导致体积膨胀30%,部分浏览器对data URI长度有限制,可能触发静默失败;
  2. API响应缺少Content-Disposition头,且CORS配置未暴露该头,浏览器对a.download属性的兼容性支持受限。

方案1:改用Blob + URL.createObjectURL替代Base64

Base64大文件处理效率低且易触发浏览器限制,换成Blob方式更稳定:

修改Flutter Web的downloadFile函数:

/// web.dart
import 'dart:typed_data';
import 'package:logger/logger.dart';
import 'package:http/http.dart' as http;
import 'package:universal_html/html.dart' as universal_html;

var logger = Logger(
  printer: PrettyPrinter(),
);

Future<void> downloadFile({
  required String token,
  required String url,
  required String fileName,
  required String dataType,
}) async {
  try {
    final Map<String, String> headers = {
      "Authorization": "Bearer $token",
      'Accept': dataType,
    };

    final http.Response r = await http.get(Uri.parse(url), headers: headers);
    final Uint8List data = r.bodyBytes;

    // 创建Blob对象存储文件字节
    final universal_html.Blob blob = universal_html.Blob([data], dataType);
    // 生成临时访问URL
    final String blobUrl = universal_html.Url.createObjectUrlFromBlob(blob);

    final a = universal_html.AnchorElement(href: blobUrl);
    a.download = fileName;
    a.click();

    // 清理临时URL释放内存
    universal_html.Url.revokeObjectUrl(blobUrl);
    a.remove();
  } catch (e) {
    logger.d(e);
  }
}

方案2:优化API配置,完善响应头与CORS

让API返回规范的下载响应头,同时调整CORS配置暴露必要字段:

1. 修改Go API的文件下载接口

在返回文件的处理函数中添加以下响应头:

// 替换成实际的文件名,比如从数据库或文件元数据中获取
fileName := "audio_6.mp3"
// 设置强制下载的Content-Disposition头
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fileName))
// 设置正确的媒体类型,替代默认的application/octet-stream
w.Header().Set("Content-Type", "audio/mpeg")

2. 更新CORS配置,暴露Content-Disposition

修改CORS的ExposedHeaders字段,添加Content-Disposition:

r.Use(cors.Handler(cors.Options{
    AllowedOrigins:   []string{"*"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Origin", "Authorization", "Content-Type", "X-CSRF-Token", "X-Requested-With", "User-Agent", "Cache-Control", "Pragma", "Content-Disposition"},
    ExposedHeaders:   []string{"Link", "Content-Length", "Content-Disposition"},
    AllowCredentials: false,
    MaxAge:           300,
}))

3. 前端适配读取API返回的文件名(可选)

如果API返回了Content-Disposition,前端可以自动解析文件名,不需要手动传入:

// 在获取http.Response后添加这段代码
String? apiFileName;
final contentDisposition = r.headers['content-disposition'];
if (contentDisposition != null) {
  final match = RegExp(r'filename="([^"]+)"').firstMatch(contentDisposition);
  if (match != null) {
    apiFileName = match.group(1);
  }
}
// 优先使用API返回的文件名,没有则用传入的参数
final useFileName = apiFileName ?? fileName;
// 创建a标签时使用useFileName
a.download = useFileName;

方案3:超大文件优化(可选)

如果后续涉及更大的文件,建议让API生成带有效期的临时签名URL,前端直接跳转该URL下载,完全绕开前端处理字节流,避免内存溢出问题。


额外注意事项

  • 检查浏览器弹窗拦截设置,确保下载弹窗能正常弹出;
  • 测试Chrome、Firefox、Safari等主流浏览器的兼容性;
  • 避免在请求头中强制设置Content-Type: application/octet-stream,让API返回实际的媒体类型(如audio/mpeg)。

内容的提问来源于stack exchange,提问作者markhorrocks

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 12:35:25