在一款Ffltter应用程序中,我正在try 将大型音频文件上传到围棋服务器,并将它们保存到Wasabi S3.

我可以看到我的服务器日志(log)为OPTIONS请求返回状态代码200,但没有POST请求和错误.在我的围棋服务器中,我使用chi路由,并添加了处理器头,以允许来自任何地方的CORS.我的应用程序很大,其他一切都运行得很好.授权承载令牌也在请求中传递.

我已经做出了禁用网络安全的更改,并在我的生产Ffltter服务器上测试了上传页面,在那里它也失败了.

OPTIONS请求接口日志(log)

"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

以下是我可以从Fflight中收集到的错误

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: *}

相关接口代码

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
}))

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 上传码

class FileUploadView extends StatefulWidget {
  const FileUploadView({super.key});

  @override
  State<FileUploadView> createState() => _FileUploadViewState();
}

class _FileUploadViewState extends State<FileUploadView> {
  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(
      (_) => showSnackBar(context),
    );
    super.initState();
  }

  FilePickerResult? result;
  PlatformFile? file;
  Response? response;
  String? progress;
  Dio dio = Dio();
  String success = 'Your file was uploaded successfully';
  String failure = 'Your file could not be uploaded';
  bool replaceFile = false;

  selectFile() async {
    FilePickerResult? result = await FilePicker.platform
        .pickFiles(type: FileType.any, withReadStream: true);

    if (result != null) {
      file = result.files.single;
    }

    setState(() {});
  }

  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;
          },
        ),
      );

      if (response!.statusCode == 200) {
        user.snackBarType = SnackBarType.success;

        user.snackBarMessage = success;

        navigator.pushNamedAndRemoveUntil(
            RoutePaths.matterTabs, (route) => false);
      } else {
        user.snackBarType = SnackBarType.failure;

        user.snackBarMessage = failure;

        navigator.pushNamedAndRemoveUntil(
            RoutePaths.matterTabs, (route) => false);
      }
    } on DioException catch (e) {
      if (e.response?.statusCode == 404) {
        print('status code 404');
      } else {
        print(e.message ?? 'no error message available');
        print(e.requestOptions.toString());
        print(e.response.toString());
        print(e.type.toString());
        print(user.fileUrl);
        print(dio.options.baseUrl);
        print(dio.options.headers);
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    User user = Provider.of<User>(context, listen: false);
    
    return Scaffold(
      resizeToAvoidBottomInset: false,
      appBar: AppBar(
        centerTitle: true,
        title: Text(
          'app name',
          maxLines: 1,
          overflow: TextOverflow.ellipsis,
        ),
        automaticallyImplyLeading: false,
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => {
            Navigator.of(context).pushNamedAndRemoveUntil(
                RoutePaths.matterTabs, (route) => false)
          },
        ),
      ),
      body: Container(
        padding: const EdgeInsets.all(12.0),
        child: SingleChildScrollView(
          child: Center(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.center,
              children: <Widget>[
                const SizedBox(height: 12),
                const Text(
                  'Select and Upload File',
                  maxLines: 4,
                  overflow: TextOverflow.ellipsis,
                  textAlign: TextAlign.center,
                  softWrap: true,
                ),
                const SizedBox(height: 24),

                Container(
                  margin: const EdgeInsets.all(10),
                  //show file name here
                  child: progress == null
                      ? const Text("Progress: 0%")
                      : Text(
                          "Progress: $progress",
                          textAlign: TextAlign.center,
                        ),
                  //show progress status here
                ),
                const SizedBox(height: 24),
                Container(
                  margin: const EdgeInsets.all(10),
                  //show file name here
                  child: file == null
                      ? const Text(
                          'Choose File',
                        )
                      : Text(
                          file!.name,
                        ),
                  //basename is from path package, to get filename from path
                  //check if file is selected, if yes then show file name
                ),
                const SizedBox(height: 24),
                ElevatedButton.icon(
                  onPressed: () async {
                    selectFile();
                  },
                  icon: const Icon(Icons.folder_open),
                  label: const Text(
                    "CHOOSE FILE",
                  ),
                ),
                const SizedBox(height: 24),

                //if selectedfile is null then show empty container
                //if file is selected then show upload button
                file == null
                    ? Container()
                    : ElevatedButton.icon(
                        onPressed: () async {
                          if (user.fileExists) {
                            _replaceExistingFile(context, user);
                          } else {
                            uploadFile(context, user);
                          }
                        },
                        icon: const Icon(Icons.upload),
                        label: const Text(
                          "UPLOAD FILE",
                        ),
                      ),
              ],
            ),
          ),
        ),
      ),
    );
  }

  _replaceExistingFile(BuildContext context, User user) {
    bool firstPress = true;

    return showDialog<bool>(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('File Exists'),
          content: Text("Do you want to replace ${user.uploadFileName}?"),
          actions: <Widget>[
            TextButton(
              child: const Text('Cancel'),
              onPressed: () {
                {
                  Navigator.of(context).pop(false);
                }
              },
            ),
            TextButton(
              child: const Text('Replace'),
              onPressed: () async {
                if (firstPress) {
                  firstPress = false;
                  {
                    uploadFile(context, user);
                  }
                } else {}
              },
            )
          ],
        );
      },
    );
  }
}

推荐答案

我想你说的是dio 5.3.2,cfug/dio号储存库

dio.options.headers['Content-Type'] = 'application/octet-stream';

您在Dio的选项中将Content-Type设置为'application/octet-stream'.如果您上载的是多部分表单数据,这可能是不正确的.在文件上传的上下文中,Content-Type通常必须是multipart/form-data.

Try to remove the manual setting of the Content-Type, or change it to 'multipart/form-data'.
Especially since 5.3.1: "Deprecate MultipartFile constructor in favor MultipartFile.fromStream"

此外,如果您的请求包含自定义头,则应将它们添加到服务器的CORS配置中的AllowedHeaders.

请记住,一些标头可能会导致请求被预检,这意味着OPTIONS请求将在实际请求之前发出,以判断服务器是否会根据其CORS标头接受请求.

由于您正在修改标头,因此请确保服务器设置为正确处理印前判断请求,并且您在客户端代码中设置的标头都包含在服务器的AllowedHeaders列表中.

另请参见Vinay Shankri中的Flutter Web: Some Notes / How to enable CORS on the server?.


You could also get back to a simple HTTP Request with Dio (see "Mastering HTTP Requests in Flutter with Dio Package" from Abdou Aziz NDAO), and check it is working.
Then, add back your code little by little, to see at what point that would fail.

Flutter相关问答推荐

具有可变高度小部件的SingleChildScrollView内的列

手势捕获的异常:类型';Double';不是类型转换中类型';Double';的子类型

如何在应用程序启动时解析和加载JSON文件?

Flutter 动画列表一次构建所有项目,而不是仅构建可见的项目

Flutter AppBar Animation反向调试

BlocBuilder仅生成一次,并在后续发出时停止重建

Flutter :URI的目标不存在

摆动更改标记可轻敲文本按钮样式

按一下按钮即可更新 fl_chart

Flutter Riverpod 没有捕获 List 的嵌套值变化

运行任何 flutter 命令都会显示无法安装 <版本>

自定义绘画未显示 - Flutter

用户文档 ID 与 Firestore 中的用户 ID 相同

Firebase Auth - 有没有办法为新用户预先生成 UID?

输入处于活动状态时如何设置文本字段标签的样式?

ListView 后台可见

Paypal 支付网关添加带有 magento 2 网络详细信息用户名、密码、签名 Magento 2 的 flutter 应用程序?

避免长单词中断

type '_InternalLinkedHashMap' 不是使用 http 包的'String' 类型的子类型

解密 Modulr 安全令牌