I have a problem with the API Client using the Dio libarary, where when I try to send a text-only field I will send it using Map or Object, but when I try to send a file I use FormData, but the problem is that when I use FormData my request is not read by Laravel.
Future<dynamic> _processRequestBody(dynamic body) async {
if (body == null) return null;
if (body is! Map<String, dynamic>) return body;
bool hasFiles = false;
final formMap = <String, dynamic>{};
Future<List<MultipartFile>> processMultipleFiles(List<PlatformFile> files) async {
return Future.wait(files.map((file) => MultipartFile.fromFile(
file.path!,
filename: file.name,
)));
}
Future<MultipartFile> processSingleFile(PlatformFile file) async {
return MultipartFile.fromFile(
file.path!,
filename: file.name,
);
}
for (var entry in body.entries) {
final value = entry.value;
try {
if (value is List<PlatformFile>) {
hasFiles = true;
if (value.length == 1) {
formMap[entry.key] = await processSingleFile(value.first);
} else {
formMap[entry.key] = await processMultipleFiles(value);
}
} else if (value is PlatformFile) {
hasFiles = true;
formMap[entry.key] = await processSingleFile(value);
} else if (value != null) {
formMap[entry.key] = value.toString();
}
} catch (e) {
print('Error processing field ${entry.key}: $e');
rethrow;
}
}
print(formMap);
return hasFiles ? FormData.fromMap(formMap) : body;
}
Handling PUT request
Future<Response> put(
String path, {
dynamic body,
Map<String, dynamic>? queryParameters,
Options? options,
bool unusedAuth = false,
}) async {
try {
final requestOptions = options ?? Options();
requestOptions.extra = {
...requestOptions.extra ?? {},
'unusedAuth': unusedAuth,
};
final processedBody = await _processRequestBody(body);
if (processedBody is FormData) {
requestOptions.headers = {
...requestOptions.headers ?? {},
'Content-Type': 'multipart/form-data',
};
}
final response = await _dio.put(
path,
data: processedBody,
queryParameters: queryParameters,
options: requestOptions,
);
return response;
} on DioException catch (e) {
throw _handleError(e);
}
}
Using API Client example
Future<BaseResponse> updateProfile(UserProfileData formData) async {
final response = await _apiClient.put(
ApiPath.updateProfile,
body: formData.toJson(),
);
}
In Laravel
public function editProfileUser(Request $request)
{
\Log::info('All request data:', $request->all());
\Log::info('Files:', $request->allFiles());
\Log::info('Has file test:', [
'hasFile' => $request->hasFile('your_file_field_name'),
'files' => $request->files->all()
]);
return $this->response(500, "Error", null);
}
Results Log
\[2024-11-03 00:01:04\] local.INFO: All request data:
\[2024-11-03 00:01:04\] local.INFO: Files:
\[2024-11-03 00:01:04\] local.INFO: Has file test: {"hasFile":false,"files":\[\]}
Everything is empty when using FormData.fromMap. But when my input contains all text then it can
Additional information: Laravel version: 10.48.22 Flutter version: 3.24.0 Dio version: ^5.7.0 My return files are all in the form of List<PlatformFile>
You can see the full API Client code on my drive https://drive.google.com/file/d/1Jhk1gLhCySvfCrRlgqNNc4NDdJu9uNzk/view?usp=drive_link
It appears that this is a well-known issue not with Laravel but with PHP. The solution appears to be to send a POST instead of a PUT or PATCH with a _method: PUT
in the form body. You can add this additional field in the body while processing or later while making the request.
post
instead of put
: final response = await _dio.post(
path,
data: processedBody,
queryParameters: queryParameters,
options: requestOptions,
);
_processRequestBody
:return hasFiles
? FormData.fromMap(formMap..['_method'] = 'PUT')
: {
...body,
"_method": "PUT",
};
Note: the ..['method']
syntax is the casacade operator and is equivalent to:
formMap['method'] = 'PUT'
FormData.fromMap(formMap)
_processRequestBody
You can leverage collection-for and Dart 3's pattern matching to refactor _processRequestBody
to a more concise version:
Future<dynamic> _processRequestBody(dynamic body) async {
bool hasFiles = false;
Future<List<MultipartFile>> processMultipleFiles(
List<PlatformFile> files) async {
hasFiles = true;
return Future.wait(files.map((file) => MultipartFile.fromFile(
file.path!,
filename: file.name,
)));
}
Future<MultipartFile> processSingleFile(PlatformFile file) async {
hasFiles = true;
return MultipartFile.fromFile(
file.path!,
filename: file.name,
);
}
switch (body) {
case null:
return null;
case Map<String, dynamic> map:
try {
final formMap = {
for (var entry in map.entries)
entry.key: switch (entry.value) {
PlatformFile file => processSingleFile(file),
[PlatformFile file] => processSingleFile(file),
[...List<PlatformFile> files] => processMultipleFiles(files),
final value => value?.toString(),
},
"_method": "PUT",
};
return hasFiles
? FormData.fromMap(formMap)
: {
...map,
"_method": "PUT",
};
} catch (e) {
debugPrint('Error processing map: $e');
rethrow;
}
default:
return body;
}
}