Reputation: 979
Having a bit of trouble trying to resolve the issue with file upload to Google Drive using their /upload endpoint. I keep getting Malformed multipart body.
error even when I try to upload simple plain text as a file.
The following .net c# code is used to create the request:
string fileName = "test.txt";
string fileContent = "The quick brown fox jumps over the lazy dog";
var fileStream = GenerateStreamFromString(fileContent); // simple text string to Stream conversion
var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
var multiPartFormDataContent = new MultipartFormDataContent("not_so_random_boundary");
// rfc2387 headers with boundary
multiPartFormDataContent.Headers.Remove("Content-Type");
multiPartFormDataContent.Headers.TryAddWithoutValidation("Content-Type", "multipart/related; boundary=" + "not_so_random_boundary");
// metadata part
multiPartFormDataContent.Add(new StringContent("{\"name\":\"" + fileName + "\",\"mimeType\":\"text/plain\",\"parents\":[\"" + folder.id + "\"]}", Encoding.UTF8, "application/json"));
// media part (file)
multiPartFormDataContent.Add(streamContent);
var response_UploadFile = await httpClient.PostAsync(string.Format("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"), multiPartFormDataContent);
I log the following Request:
Method: POST,
RequestUri: 'https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart',
Version: 1.1,
Content: System.Net.Http.MultipartFormDataContent,
Headers: { Authorization: Bearer /*snip*/ Content-Type: multipart/related; boundary=not_so_random_boundary }
with following request content (pretified):
--not_so_random_boundary
Content-Type: application/json; charset=utf-8
Content-Disposition: form-data
{"name":"test.txt","mimeType":"text/plain","parents":["/*snip*/"]}
--not_so_random_boundary
Content-Type: text/plain
Content-Disposition: form-data
The quick brown fox jumps over the lazy dog
--not_so_random_boundary--
I've spent the entire day on this and it got me to this point. I have a hunch that issue is something silly but I just can't figure it out.
Could someone throw their eyes over this perhaps you can spot where I made a mistake that would be very helpful?
###ref: Send a multipart upload request
Upvotes: 0
Views: 5475
Reputation: 11219
Really disappointing that the documentation is lacking the accuracy expected on the backend side.
The API is actually very picky about the line returns. I will provide an example that works so you don't waste a full day like I did. Please pay attention the the line breaks (also you need to add -- after the last boundary):
--subrequest_boundary
Content-Type: application/json; charset=UTF-8
{"mimeType":"image/png","parents":["1Ai7THUlwvkgb_I3u7SO-ilyI8elPdaHr"],"name":"1000000333.png"}
--subrequest_boundary
Content-Type: image/png
Content-Transfer-Encoding: base64
{replace with base64 image}
--subrequest_boundary--
Failing to have some line returns here and there will make the request fail.
On top of that the API states that you should use "title" in the metadata json. NO, you should use "name" else you will have "Untitled" as the default name.
Last (but not least) Content-Length is specified as being mandatory but the request will proceed without specifying it. (so don't bother calculating and adding it to the headers).
The Content-Disposition (marked as answer) is also not required. As stated by the API metadata must come first, and actual file second.
PS: my request headers (in case you haven't guessed):
Content-Type: multipart/related; boundary=subrequest_boundary
Authentication: Bearer {bearer token}
Upvotes: 0
Reputation: 116918
Another option would be to use the Google .net client library and let it handel the upload for you.
// Upload file Metadata
var fileMetadata = new Google.Apis.Drive.v3.Data.File()
{
Name = "Test hello uploaded.txt",
Parents = new List() {"10krlloIS2i_2u_ewkdv3_1NqcpmWSL1w"}
};
string uploadedFileId;
// Create a new file on Google Drive
await using (var fsSource = new FileStream(UploadFileName, FileMode.Open, FileAccess.Read))
{
// Create a new file, with metadata and stream.
var request = service.Files.Create(fileMetadata, fsSource, "text/plain");
request.Fields = "*";
var results = await request.UploadAsync(CancellationToken.None);
if (results.Status == UploadStatus.Failed)
{
Console.WriteLine($"Error uploading file: {results.Exception.Message}");
}
// the file id of the new file we created
uploadedFileId = request.ResponseBody?.Id;
}
Upvotes: 0
Reputation: 979
Thanks to @Tanaike suggestion we found the problem with my code.
Turns out while it is not specifically mentioned in the documentation (or any code examples) but adding Content-Disposition: form-data; name="metadata"
to the StringContent
part of the request body makes all the difference.
The final request can be rewritten as follows:
// sample file (controlled test example)
string fileName = "test.txt";
string fileType = "text/plain";
string fileContent = "The quick brown fox jumps over the lazy dog";
var fileStream = GenerateStreamFromString(fileContent); // test file
// media part (file)
//var fileStream = File.Open(path_to_file, FileMode.Open, FileAccess.Read); // you should read file from disk
var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");
streamContent.Headers.ContentDisposition.Name = "\"file\"";
// metadata part
var stringContent = new StringContent("{\"name\":\"" + fileName + "\",\"mimeType\":\"" + fileType + "\",\"parents\":[\"" + folder.id + "\"]}", Encoding.UTF8, "application/json");
stringContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data");
stringContent.Headers.ContentDisposition.Name = "\"metadata\"";
var boundary = DataTime.Now.Ticks.ToString(); // or hard code a string like in my previous code
var multiPartFormDataContent = new MultipartFormDataContent(boundary);
// rfc2387 headers with boundary
multiPartFormDataContent.Headers.Remove("Content-Type");
multiPartFormDataContent.Headers.TryAddWithoutValidation("Content-Type", "multipart/related; boundary=" + boundary);
// request body
multiPartFormDataContent.Add(stringContent); // metadata part - must be first part in request body
multiPartFormDataContent.Add(streamContent); // media part - must follow metadata part
var response_UploadFile = await httpClient.PostAsync(string.Format("https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"), multiPartFormDataContent);
Note that normally one would add file name and content type as part of the StreamContent
but these headers are ignored by Google Drive API. This is done deliberatly because the API expects to recieve a metadata object with relevant properties. (the following headers were removed from above code example but will be retained here for future reference)
streamContent.Headers.ContentDisposition.FileName = "\"" + fileName + "\"";
streamContent.Headers.ContentType = new MediaTypeHeaderValue(fileType);
Note that you only need to specify "parents":["{folder_id}"]
property if you want to upload file to a subfolder in Google Drive.
Hope this helps someone else in the future.
Upvotes: 5
Reputation: 201368
I think that the structure of the request body for multipart/related
might not be correct. So how about modifying as follows?
--not_so_random_boundary
Content-Type: application/json; charset=utf-8
Content-Disposition: form-data; name="metadata"
{"name":"test.txt","mimeType":"text/plain","parents":["/*snip*/"]}
--not_so_random_boundary
Content-Type: text/plain
Content-Disposition: form-data; name="file"
The quick brown fox jumps over the lazy dog
--not_so_random_boundary--
name
for each part of Content-Disposition
.https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart
as POST method, a text file of test.txt
which has the content of The quick brown fox jumps over the lazy dog
is created.If this didn't work, I apologize.
Upvotes: 2