skip to Main Content

In a Flutter app I’m trying to upload large audio files to a go server and save them to Wasabi s3.

I can see my server log returning a status code 200 for the OPTIONS request but no post request and no errors. In my go server I am using the chi router and have added handler header to allow CORS from anywhere. My app is large and everything else works fine. The authorization bearer token is also being passed in the request.

I have already made the changes to disable web security and also tested the upload page on my production flutter server where it also fails.

Api log for the options request

"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

Here are the errors I can glean from 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: *}

Relevant api code

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 upload code

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

2

Answers


  1. I suppose you are talking about dio 5.3.2, repository cfug/dio

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

    You are setting the Content-Type to 'application/octet-stream' in Dio’s options. That might not be correct if you are uploading a multipart form data. In the context of file uploads, the Content-Type typically has to be 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"

    Also, if your request contains custom headers, they should be added to the AllowedHeaders in your server’s CORS configuration.

    Remember that some headers may cause a request to be preflighted, meaning that an OPTIONS request will be made before the actual request, to check that the server will accept the request according to its CORS headers.

    Since you are modifying the headers, ensure that the server is set up to handle preflight requests correctly, and that the headers you are setting in your client code are all included in the server’s AllowedHeaders list.

    See also Flutter Web: Some Notes / How to enable CORS on the server? from Vinay Shankri.


    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.

    Login or Signup to reply.
  2. The error message "XMLHttpRequest onError callback was called" is often encountered in web development when making HTTP requests using XMLHttpRequest, a built-in browser API for making network requests. This error message usually indicates that an error occurred during the processing of the HTTP request, and the onError callback associated with the request was triggered.

    If you are encountering this error in the context of a Flutter web application and you’re using the Dio package for making HTTP requests, there are a few things you can check to troubleshoot the issue:

    Please check this link:- https://protocoderspoint.com/flutter-web-xmlhttprequest-error-dio-library-web-issue-fixed/#google_vignette

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search