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
相关产品推荐
相关产品推荐

