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
I suppose you are talking about dio 5.3.2, repository
cfug/dio
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, theContent-Type
typically has to bemultipart/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 favorMultipartFile.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.
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