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

Flutter Web用Dio上传大文件遇XMLHttpRequest连接错误排查

问题

在Flutter应用中上传大型音频文件至Go服务器(最终存储到Wasabi S3)时,服务器日志显示OPTIONS请求返回200状态码,但未收到POST请求,且无明确错误提示。具体情况:

  • Go服务器使用chi路由,已配置允许任意来源的CORS,其他功能运行正常,请求携带Bearer授权令牌
  • 已禁用Web安全设置,生产环境测试同样失败

相关日志与错误信息

服务器OPTIONS请求日志

"OPTIONS http://api.mydomain.com/transcript/upload/audio/file/2 HTTP/1.1" from 123.12.0.1:48558 - 200 0B in 39.061µs

Flutter端错误信息

DioExceptionType.connectionError
https://api.mydomain.com/transcript/upload/audio/file/2
https://api.mydomain.com/transcript/upload/audio/file/2
{Content-Type: application/octet-stream, Authorization: Bearer my-token, Access-Control-Allow-Origin: *}

相关代码片段

Go服务器CORS与路由配置

r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Use(cors.Handler(cors.Options{
    AllowedOrigins:   []string{"*"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
    ExposedHeaders:   []string{"Link"},
    AllowCredentials: false,
    MaxAge:           300, // Maximum value not ignored by any of major browsers
}))

Go服务器上传处理代码

const maxUploadSize = 500 * 1024 * 1024 // 500 MB

id := chi.URLParam(r, "transcriptId")

transcriptId, err := strconv.ParseInt(id, 10, 64)

if err := r.ParseMultipartForm(32 << 20); err != nil {
    log.Println(err)
    http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
    return
}

file, handler, err := r.FormFile("file")

Flutter上传核心代码

Future<void> uploadFile(BuildContext context, User user) async {
    final navigator = Navigator.of(context);

    const storage = FlutterSecureStorage();

    String? token = await storage.read(key: 'jwt');

    dio.options.headers['Content-Type'] = 'application/octet-stream';
    dio.options.headers["Authorization"] = "Bearer $token";
    dio.options.headers['Access-Control-Allow-Origin'] = '*';
    dio.options.baseUrl = user.fileUrl;

    final uploader = ChunkedUploader(dio);

    try {
      response = await uploader.upload(
        fileKey: 'file',
        method: 'POST',
        fileName: file!.name,
        fileSize: file!.size,
        fileDataStream: file!.readStream!,
        maxChunkSize: 32000000,
        path: user.fileUrl,
        onUploadProgress: (progress) => setState(
          () {
            progress;
          },
        ),
      );

      // 后续状态处理...
    } on DioException catch (e) {
      // 错误处理...
    }
}

排查与解决方案

1. 修正Flutter端请求头配置

  • Content-Type错误:服务器用FormFile解析的是multipart/form-data类型请求,当前设置的application/octet-stream不匹配,需移除或修改:
    // 删除这行:dio.options.headers['Content-Type'] = 'application/octet-stream';
    // 让ChunkedUploader自动处理multipart/form-data,或手动设置:
    dio.options.headers['Content-Type'] = 'multipart/form-data';
    
  • 多余响应头:Access-Control-Allow-Origin是服务器返回的响应头,无需在请求中携带,直接删除:
    dio.options.headers.remove('Access-Control-Allow-Origin');
    

2. 调整Go服务器请求大小与超时限制

  • 虽然定义了maxUploadSize,但ParseMultipartForm(32 << 20)仅限制内存存储的表单数据,需在服务器全局层面设置匹配的大小和超时:
    srv := &http.Server{
        Addr:         ":8080",
        Handler:      r,
        ReadTimeout:  30 * time.Minute,  // 大文件上传需要更长超时
        WriteTimeout: 30 * time.Minute,
        MaxHeaderBytes: maxUploadSize,   // 匹配最大上传大小
    }
    
  • 在上传处理函数开头添加请求大小校验:
    r.Body = http.MaxBytesReader(w, r.Body, maxUploadSize)
    if err := r.ParseMultipartForm(32 << 20); err != nil {
        log.Println(err)
        http.Error(w, "文件过大或请求格式错误", http.StatusBadRequest)
        return
    }
    

3. 修复ChunkedUploader路径重复问题

同时设置baseUrl和path为完整URL会导致请求地址重复,二选一即可:

// 方案一:设置baseUrl为域名,path用相对路径
dio.options.baseUrl = "https://api.mydomain.com";
path: "/transcript/upload/audio/file/${user.transcriptId}"

// 方案二:清空baseUrl,path用完整URL
dio.options.baseUrl = "";
path: user.fileUrl;

4. 配置Dio超时时间

大文件分片上传需要更长的超时,避免连接中断:

dio.options.connectTimeout = const Duration(minutes: 5);
dio.options.receiveTimeout = const Duration(minutes: 10);

5. 验证服务器请求接收情况

在上传路由处理函数开头添加日志,确认请求是否到达服务器:

log.Printf("收到POST请求,路径:%s", r.RequestURI)

如果无日志输出,需排查CDN、防火墙或负载均衡是否拦截了大请求。

6. 测试基础上传流程

暂时移除分片上传,用普通Dio multipart上传小文件,确认基础流程正常:

FormData formData = FormData.fromMap({
  "file": MultipartFile.fromStream(
    file!.readStream!,
    file!.size,
    filename: file!.name,
  ),
});
response = await dio.post(
  user.fileUrl,
  data: formData,
  options: Options(headers: {"Authorization": "Bearer $token"}),
);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 18:05:07