Flutter Resumable Upload to Google Drive Through HTTP - rest

Based off the documentation on Google Drive API I'm trying to upload a file to the root folder of a Google Drive. I have authentication of the user through Google Sign In, and that hasn't been an issue. I keep getting a 411 Error returned from the server that says
"POST requests require a Content-length header. That’s all we know.".
I have a Content-length header but it seems to not be accepted. Here's the code I have,
Uri uri = Uri.parse('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable');
http.MultipartRequest request = new http.MultipartRequest('POST', uri);
request.headers["Authorization"] = header['Authorization'];
request.headers['content-type'] = "application/json; charset=UTF-8";
request.headers['X-Upload-Content-Type'] ='video/mp4';
request.headers['X-Upload-Content-Length'] = lengthInBytes.toString();
request.headers['name'] = fileName;
request.headers['content-length'] = (request.contentLength).toString();
//request.files.add(await http.MultipartFile.fromPath('$fileName', file.path,));
print("request.toString: " + request.toString());
http.StreamedResponse response = await request.send();
print('ok: ' + response.statusCode.toString());
response.stream.transform(utf8.decoder).listen((value) {
print(value);
});
The only line that I know looks off to me is the fileName, as the documentation on the API site is slightly different and I'm not sure if I'm encoding it correctly. Here's the API example on the Google site,
POST https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable HTTP/1.1
Authorization: Bearer [YOUR_AUTH_TOKEN]
Content-Length: 38
Content-Type: application/json; charset=UTF-8
X-Upload-Content-Type: image/jpeg
X-Upload-Content-Length: 2000000
{
"name": "myObject"
}
I can do a multipart upload for a file about 5MB in size, but I need to be able to upload larger ones and resumable is the only option. I can post the multipart code that works if needed.

I solved the issue by using the http StreamedRequest class. The code posted below works with Google Drive V3 to upload a mp4 video.
Future handleUploadData(Map headers, String filename, String path) async {
final file = new File(path);
final fileLength = file.lengthSync().toString();
String sessionUri;
Uri uri = Uri.parse('https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable');
String body = json.encode({ 'name' : filename });
final initialStreamedRequest =
new http.StreamedRequest('POST', uri)
..headers.addAll({
'Authorization': headers['Authorization'],
'Content-Length' : utf8.encode(body).length.toString(),
'Content-Type' : 'application/json; charset=UTF-8',
'X-Upload-Content-Type' : 'video/mp4',
'X-Upload-Content-Length' : fileLength
});
initialStreamedRequest.sink.add(utf8.encode(body));
initialStreamedRequest.sink.close();
http.StreamedResponse response = await initialStreamedRequest.send();
print("response: " + response.statusCode.toString());
response.stream.transform(utf8.decoder).listen((value) {
print(value);
});
if (response.statusCode == 200) {
sessionUri = response.headers['location'];
print(sessionUri);
}
Uri sessionURI = Uri.parse(sessionUri);
final fileStreamedRequest =
new http.StreamedRequest('PUT', sessionURI)
..headers.addAll({
'Content-Length' : fileLength,
'Content-Type' : 'video/mp4',
});
fileStreamedRequest.sink.add(file.readAsBytesSync());
fileStreamedRequest.sink.close();
http.StreamedResponse fileResponse = await fileStreamedRequest.send();
print("file response: " + fileResponse.statusCode.toString());
fileResponse.stream.transform(utf8.decoder).listen((value) {
print(value);
});
}
The initial StreamRequest sends a request to GDrive with meta data about the file that will be uploaded, and receives a location URI that is used in the second file StreamRequest to upload the actual file data. Currently this is done in one upload action, but it could be split up into chunks.

I had roughly the same problem except I was trying to upload a text file and I wanted a single atomic request in order to be able to use the "If-Match" header with the file etag (When I'll write "update" code, I'm doing sync and I don't want to overwrite the file if it was changed by somewhere else during my sync).
I was really struggling with the http.post function and I was getting the "411 length required" error even though I was properly setting the "Content-Length" header.
The solution from Sean Coutinho using http.StreamedRequest gave me working code I could work from to get my request working, so thank you!
I'll post my code here in case it helps other people:
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:google_sign_in/google_sign_in.dart';
enum RemoteFileType {
FOLDER,
FILE,
}
class RemoteFile {
final RemoteFileType fileType;
final String fileId;
final String fileName;
RemoteFile(
this.fileType,
this.fileId,
this.fileName,
);
}
// The boundary string
const String MULTIPART_REQUESTS_BOUNDARY_STRING = 'foo_bar_baz';
Map<String, String> _authHeaders;
String _createMultiPartRequestBodyString(
final Map<String, dynamic> requestMetaData,
final String fileContentString,
) {
return '\r\n--$MULTIPART_REQUESTS_BOUNDARY_STRING\r\n' +
'Content-Type: application/json; charset=UTF-8\r\n\r\n' +
jsonEncode(requestMetaData) +
'\r\n--$MULTIPART_REQUESTS_BOUNDARY_STRING\r\nContent-Type: text/plain\r\n\r\n' +
fileContentString +
'\r\n--$MULTIPART_REQUESTS_BOUNDARY_STRING--';
}
Future<RemoteFile> createNewTextFile(
final RemoteFile parentFolder,
final String fileName,
final String fileTextContent,
) async {
final Map<String, dynamic> requestMetaData = {
'mimeType': 'application/json',
'title': fileName,
'parents': [
{'id': parentFolder.fileId}
],
};
final String multiPartRequestBodyString = _createMultiPartRequestBodyString(requestMetaData, fileTextContent);
final http.StreamedRequest fileStreamedRequest = http.StreamedRequest(
'POST',
Uri.parse('https://www.googleapis.com/upload/drive/v2/files?uploadType=multipart'),
);
fileStreamedRequest.headers.addAll({
'Authorization': _authHeaders['Authorization'],
'Accept': 'application/json',
'Content-Type': 'multipart/related; boundary=$MULTIPART_REQUESTS_BOUNDARY_STRING',
'Content-Length': multiPartRequestBodyString.length.toString(),
//'If-Match': 'my_etag_here_when_updating_existing_file_with_put',
});
fileStreamedRequest.sink.add(utf8.encode(multiPartRequestBodyString));
fileStreamedRequest.sink.close();
final http.StreamedResponse httpPostResponse = await fileStreamedRequest.send();
// Do what you want with the response too
//...
}

Related

Content-Type header is text/html when uploading a file to the server using a multipart request only in iOS

I'm sending a multipart request to a server in order to upload a file, however this is working only on Android, when it comes to iOS I get an Internal Server Error with a status code 500. When I print the headers on Android the content type is application json, but on iOS the content type is text/html. Is this a server-side problem? or should I set any other header in the multipart request besides authorization header?
This is the code of the request
Future<Map<String, dynamic>> uploadFileToEvent(File file, int eventId) async {
var stream = http.ByteStream(file.openRead());
stream.cast();
var length = await file.length();
String url = '$_url/event/$eventId/media/';
final _header = <String, String>{
'Authorization': 'Bearer ' + _prefs.token,
};
var uri = Uri.parse(url);
var request = http.MultipartRequest("POST", uri);
var multipartFileSign = http.MultipartFile('file', stream, length, filename: basename(file.path));
request.files.add(multipartFileSign);
request.headers.addAll(_header);
var response = await request.send();
final resp = await http.Response.fromStream(response);
}

Unable to Upload Image but Server response indicates success

Each time I try to upload an image using http multipart, I get success response in less than a second, which is weird and indicates that the image wasn't uploaded. How do I get over this issue please:
Future<UpdatedUserDataModel> updateUserData(
{String accessToken, String field, String value}) async {
String params = '?access_token=$accessToken';
String url = AppStrings.apiBase + AppStrings.updateUserData + params;
Map<String, String> headers = {"Content-type": "multipart/form-data"};
//Prepare Multipart request
http.MultipartRequest request = http.MultipartRequest(
'POST',
Uri.parse(url),
);
request.fields['api_key'] = AppStrings.apiKey;
request.headers.addAll(headers);
request.files.add(
await http.MultipartFile.fromPath(
field,
value,
filename: 'cover_image',
contentType: MediaType('image', value.split('.').last),
),
);
//Send request
final response = await request.send();
//Decode response stream here...
}

Google Drive API: Uploading and creating folders with http requests, for example in DIO with Flutter

I'm trying to create a simple Flutter app which interacts with the Google Drive API.
Authentication works great via the Google Sign In package, so I have access to the correct headers and auth tokens.
What I don't understand however, despite trying different approaches and reading the Drive Documentation up and down - how can I interact with the API via http requests, for example via Dio or via the "standard" way in dart/flutter?
To state one example: I want to upload an an image the user picked. I have everything figured out (the file path, the auth token, etc.), but how does a http request look like?
Here is the "bare" http request:
Map headers = await user.currentUser.authHeaders;
var formData = FormData.fromMap({
'name': filePath,
'file': MultipartFile.fromBytes(fileData, filename: filePath)
});
var response = await Dio().post(
'https://www.googleapis.com/upload/drive/v3/files?uploadType=media',
data: formData,
options: Options(headers: headers));
print(response);
It's probably a very mundane/trivial question, but I just can't figure it out ..
Thanks in advance for your help!
Christian
You need to create the File first then upload the file data into it.
I'll using the http plugin and not DIO. But the same process should work for dio.
Step one: Create the file metadata in a folder
Future<String> createFile({File image, String folderId}) async {
String accessToken = await Prefs.getToken();
Map body = {
'name': 'name.jpg',
'description': 'Newly created file',
'mimeType': 'application/octet-stream',
'parents': ['$folderId']
};
var res = await http.post(
'https://www.googleapis.com/drive/v3/files',
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json; charset=UTF-8'
},
body: jsonEncode(body),
);
if (res.statusCode == 200) {
// Extract the ID of the file we just created so we
// can upload file data into it
String fileId = jsonDecode(res.body)['id'];
// Upload the content into the empty file
await uploadImageToFile(image, fileId);
// Get file (downloadable) link and use it for anything
String link = await getFileLink(fileId);
return link;
} else {
Map json = jsonDecode(res.body);
throw ('${json['error']['message']}');
}
}
Step two: Upload image data into empty file
Future uploadImageToFile(File image, String id) async {
String accessToken = await Prefs.getToken();
String mimeType = mime(basename(image.path).toLowerCase());
print(mimeType);
var res = await http.patch(
'https://www.googleapis.com/upload/drive/v3/files/$id?uploadType=media',
body: image.readAsBytesSync(),
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': '$mimeType'
},
);
if (res.statusCode == 200) {
return res.body;
} else {
Map json = jsonDecode(res.body);
throw ('${json['error']['message']}');
}
}
Step three: Get downloadable file link(to store in database or use for anything)
Future getFileLink(String id) async {
String accessToken = await Prefs.getToken();
var res = await http.get(
'https://www.googleapis.com/drive/v3/files/$id?fields=webContentLink',
headers: {
'Authorization': 'Bearer $accessToken',
'Content-Type': 'application/json; charset=UTF-8'
},
);
if (res.statusCode == 200) {
Map json = jsonDecode(res.body);
String link = json['webContentLink'];
return link.split('&')[0];
} else {
Map json = jsonDecode(res.body);
throw ('${json['error']['message']}');
}
}

How to send a png file by 'application/octet-stream' in Dart/Flutter to Microsoft Custom Vision?

I know this question could be redundant, but I am trying to send a png file through a POST request to Microsoft Custom Vision in Flutter.
This is my code:
void _makeRequest (File file) async {
String url = "<url>";
Map<String, String> headers = {
"Content-Type": "application/octet-stream",
"Prediction-Key": "<key>",
};
var bytes = file.readAsBytesSync();
var response = await http.post(
url,
headers: headers,
body: bytes,
);
print(response.body);
print(response.statusCode);
}
And when I run this code I get this response:
{"code":"ErrorUnknown","message":"The request entity's media type 'appliction/octet-stream' is not supported for this resource."}
Based on your comment , I think you used the wrong endpoint/URL. Since you're sending image, you have to use the other prediction endpoint that looks like this:
"https://southcentralus.api.cognitive.microsoft.com/customvision/v3.0/Prediction/<Project ID>/classify/iterations/<Iteration number>/image
^ Note ../image
If still can't, please try below code snipper(works for me):
final bytes = file.readAsBytesSync();
var uri = Uri.parse(
"<Prediction endpoint>");
var request = new http.Request("POST", uri)
..headers['Prediction-Key'] = "<Prediction Key>"
..headers['Content-Type'] = "application/octet-stream"
..bodyBytes = bytes;
http.Response response = await http.Response.fromStream(await request.send());
print(request);
print("Result: ${response.statusCode}");
print(response.statusCode);
print(response.body);

How to post x-www-form-urlencoded in Flutter

I am trying to send a POST request to an API to create an account.
The request is working well, it should look like this :
Bulk Edit Mode :
Key-Value Edit mode :
There are also 9 headers that are auto-generated, so I did not show them, but I can take another screen if you need to.
My request looks like this :
import 'dart:convert' as convert ;
import 'package:my_project/requests/utils.dart';
import 'package:http/http.dart' as http;
Future<String> createUser(String firstName, String name, String mail,
String password, String confirmPassword, String birthDate,
String phone) async {
String url = BASE_URL + "createUser" ; // Don't worry about BASE_URL, the final url is correct
Map<String, dynamic> formMap = {
"name": name,
"surname": firstName,
"mail": mail,
"password": password,
"birth": birthDate,
"phone": phone,
"confirmPassword": confirmPassword
} ;
http.Response response = await http.post(
url,
body: convert.jsonEncode(formMap),
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
encoding: convert.Encoding.getByName("utf-8"),
);
print("RESPONSE ${response.statusCode} ; BODY = ${response.body}");
return (response.body) ;
}
Here is my print result :
I/flutter ( 6942): RESPONSE 307 ; BODY =
As you can see, I am getting a 307 error, and the problem does not come from the server, as it worked with Postman.
Am I sending this form-urlencoded POST request correctly ?
I also tried :
http.Response response = await http.post(
url,
body: "name=$name&surname=$firstName&mail=$mail&password=$password&birth=$birthDate&phone=$phone&confirmPassword=$confirmPassword",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
encoding: convert.Encoding.getByName("utf-8"),
);
but with the same results. I tried too :
http.Response response = await http.post(
url,
body: formMap,
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
encoding: convert.Encoding.getByName("utf-8"),
);
with same result again.
What am I doing wrong ?
EDIT :
I tried FoggyDay answer, here is my request now :
final client = HttpClient() ;
final request = await client.postUrl(Uri.parse(url));
request.headers.set(HttpHeaders.contentTypeHeader, "application/x-www_form-urlencoded");
request.followRedirects = true ;
request.write(formMap);
final response = await request.close();
print("STATUS CODE = ${response.statusCode}");
However I still have a 307 error. Did I create the right request ?
EDIT 2 :
As asked, I printed location as follow :
final client = HttpClient() ;
final request = await client.postUrl(Uri.parse(url));
request.headers.set(HttpHeaders.contentTypeHeader, "application/x-www_form-urlencoded");
request.followRedirects = true ;
request.write(formMap);
final response = await request.close();
print("STATUS CODE = ${response.statusCode}");
print("Response headers = ${response.headers}");
And I get :
I/flutter ( 7671): STATUS CODE = 307
I/flutter ( 7671): Response headers = location: /app/createUser/
I/flutter ( 7671): date: Tue, 26 May 2020 09:00:29 GMT
I/flutter ( 7671): content-length: 0
I/flutter ( 7671): server: Apache/2.4.41 (Amazon) OpenSSL/1.0.2k-fips
The thing is I am already making a call on /app/createUser... ('/app/' is in BASE_URL)
For x-www-form-urlencoded parameters, just use this:
Future<String> login(user, pass) async {
final response = await http.post(
Uri.parse('https:youraddress.com/api'),
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
encoding: Encoding.getByName('utf-8'),
body: {"title": "title"},
);
if (response.statusCode == 200) {
// If the server did return a 200 OK response,
// then parse the JSON.
} else {
// If the server did not return a 200 OK response,
// then throw an exception.
}
}
official http package from flutter is buggy with urlencoded type, you can use Dio package instead.
final dio = Dio();
final res = dio.post(
'/info',
data: {'id': 5},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
As you can see, I am getting a 307 error, and the problem does not come from the server, as it worked with Postman.
No, that's NOT necessarily the case. Look here:
MDN: 307 Temporary Redirect
In other words, Postman is following the redirect ... and your Flutter app isn't.
SUGGESTION: Try setting followRedirects to true:
https://api.flutter.dev/flutter/dart-io/HttpClientRequest/followRedirects.html
ADDITIONAL INFO:
The default value for request.followRedirects happens to be "true" anyway. It doesn't hurt to explicitly set it ... but it explains why the behavior didn't change.
Per this post:
The Dart HTTP client won't follow
redirects
for POSTs unless the response code is 303. It follows 302 redirects
for GET or HEAD.
Per this post
The correct way to handle redirects on POST requests is to manually
implement an appropriate strategy for your use case:
var response = await client.post(...);
if (response.statusCode == 301 || response.statusCode == 302) {
// send post request again if appropriate
}
let try with this code, it works well for me.
var headers = {
'Content-Type': 'application/x-www-form-urlencoded'
};
var request = http.Request('POST', Uri.parse('https://oauth2.googleapis.com/token'));
request.bodyFields = {
'client_id': '',
'client_secret': '',
'code': '',
'grant_type': 'authorization_code',
'redirect_uri': ''
};
request.headers.addAll(headers);
http.StreamedResponse response = await request.send();
if (response.statusCode == 200) {
print(await response.stream.bytesToString());
}
else {
print(response.reasonPhrase);
}
If you are using http, you should add the below lines
Android -
android/app/src/main/AndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="first_app"
android:usesCleartextTraffic="true" //this line
android:icon="#mipmap/ic_launcher">
iOS -
ios/Runner/info.plist
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
Be warned that you will need to have an explanation for Apple's review team when enabling this, otherwise, your app will get rejected on submission.
Uninstall the app and Reinstall again if you have the app already in the emulator when you add those lines to avoid confusions.
If you send HTTP GET request, you can use query parameters as follows:
QueryParameters
http://example.com/path/to/page?name=ferret&color=purple
The contents are encoded as query parameters in the URL
application/x-www-form- urlencoded
The contents are encoded as query parameters in the body of the
request instead of the URL.
The data is sent as a long query string. The query string contains
name-value pairs separated by & character
POST example
flutter http package version - http: ^0.13.1
import 'package:http/http.dart' as httpClient;
Future<dynamic> postData() async {
//Uri.https("192.168.1.30:5000", "/api/data")
//Uri.parse("your url");
final Uri uri = Uri.http("192.168.1.30:5000", "/api/data");
final response = await httpClient.post(
uri,
body: {
"name": "yourName",
"age": "yourAge"
},
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
encoding: Encoding.getByName('utf-8'),
);
return response.body;
}