iiminov
iiminov

Reputation: 979

Malformed multipart body when uploading file to Google Drive

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

RFC 2387

Upvotes: 0

Views: 5475

Answers (4)

Antonin GAVREL
Antonin GAVREL

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

Linda Lawton - DaImTo
Linda Lawton - DaImTo

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;
      }

Upload files to google drive

Upvotes: 0

iiminov
iiminov

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

Tanaike
Tanaike

Reputation: 201368

I think that the structure of the request body for multipart/related might not be correct. So how about modifying as follows?

Modified request body:

--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--
  • Please be careful the line breaks for the request body.
  • Please add name for each part of Content-Disposition.

Note:

  • Now I could confirm that when above modified request body is used for the endpoint of 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.

References:

If this didn't work, I apologize.

Upvotes: 2

Related Questions