Search code examples
delphiindyindy10onedrive

Indy 10 Multipart upload to OneDrive Error


I am trying to do Multipart upload to OneDrive using POST and get 'HTTP/1.1 400 Bad Request'. IdLogFile:

Stat Connected.
Sent 10.02.2017 12:50:08: POST /v1.0/drive/root::/children HTTP/1.0`<EOL>`Content-Type: multipart/related; boundary="Boundary"`<EOL>`Content-Length: 254`<EOL>`Authorization: Bearer EwA...%3d`<EOL>`Host: api.onedrive.com`<EOL>`Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8`<EOL>`Accept-Encoding: identity`<EOL>`User-Agent: Mozilla/3.0 (compatible; Indy Library)`<EOL><EOL>`
Sent 10.02.2017 12:50:08: --Boundary`<EOL>`Content-ID: <metadata>`<EOL>`Content-Type: application/json`<EOL>`{"name":"Dest.txt", "file":{}, "@content.sourceUrl":"cid:content"}`<EOL>`--Boundary`<EOL>`Content-ID: <content><EOL>Content-Type: application/octet-stream<SourceContent>--Boundary--
Recv 10.02.2017 12:50:08: H
Recv 10.02.2017 12:50:08: TTP/1.1 400 Bad Request`<EOL>`Via: 1.1 DM5SCH102210409 (wls-colorado)`<EOL>`Content-Length: 60`<EOL>`Content-Type: application/json`<EOL>`Server: Microsoft-IIS/8.5`<EOL>`P3P: CP="BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo"`<EOL>`X-WLSPROXY: DM5SCH102210409`<EOL>`X-MSNSERVER: DM5SCH102231823`<EOL>`Strict-Transport-Security: max-age=31536000; includeSubDomains`<EOL>`X-QosStats: {"ApiId":0,"ResultType":2,"SourcePropertyId":0,"TargetPropertyId":42}`<EOL>`X-ThrowSite: 1479.b891`<EOL>`X-AsmVersion: UNKNOWN; 16.0.0.0`<EOL>`X-MSEdge-Ref: Ref A: A9918FA26FAF469EB3797E9DAEA3172E Ref B: FRAEDGE0409 Ref C: Fri Feb 10 01:50:09 2017 PST`<EOL>`Date: Fri, 10 Feb 2017 09:50:09 GMT`<EOL>`Connection: close`<EOL><EOL>`{"error":{"code":"invalidRequest","message":"Bad Argument"}}
Stat Disconnected.

The code:

procedure TSaveFilter.UploadTest;
const
  URL = 'https://api.onedrive.com/v1.0/drive/root::/children';
  Boundary = 'Boundary';
var
  IdHTTP: TIdHTTP;
  MemoryStream: TMemoryStream;
  FileStream: TFileStream;

  procedure WriteLnString(str: AnsiString; CRLF: Boolean = True);
  begin
    if CRLF then str := str + #13#10;
    MemoryStream.Write(str[1], Length(str));
  end;
begin
  IdHTTP := TIdHTTP.Create(nil);
  try
    IdHTTP.HandleRedirects := True;
    IdHTTP.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(IdHTTP);
    IdHTTP.Request.BasicAuthentication := False;
    IdHTTP.Request.CustomHeaders.Values['Authorization'] := 'Bearer ' + FAccessToken;
    IdHTTP.Request.ContentType := Format('multipart/related; boundary="%s"', [Boundary]);

    MemoryStream := TMemoryStream.Create;
    try
      WriteLnString('--' + Boundary);
      WriteLnString('Content-ID: <metadata>');
      WriteLnString('Content-Type: application/json');
      WriteLnString('{"name":"Dest.txt", "file":{}, "@content.sourceUrl":"cid:content"}');
      WriteLnString('--' + Boundary);
      WriteLnString('Content-ID: <content>');
      WriteLnString('Content-Type: application/octet-stream', False);
      FileStream := TFileStream.Create('Source.txt', fmOpenRead);
      try
        MemoryStream.CopyFrom(FileStream, FileStream.Size);
      finally
        FileStream.Free;
      end;
      WriteLnString('--' + Boundary + '--', False);
      IdHTTP.Post(URL, MemoryStream);
    finally
      MemoryStream.Free;
    end;
  finally
    IdHTTP.Free;
  end;
end;

What am I doing wrong?

This is a working request (List children):

Stat Connected.
Sent 10.02.2017 20:52:42: GET /v1.0/drive/root::/children?select=name,folder,file HTTP/1.1`<EOL>`Authorization: Bearer EwA...%3d`<EOL>`Host: api.onedrive.com`<EOL>`Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8`<EOL>`Accept-Encoding: identity`<EOL>`User-Agent: Mozilla/3.0 (compatible; Indy Library)`<EOL>``<EOL>`
Recv 10.02.2017 20:52:43: H
Recv 10.02.2017 20:52:43: TTP/1.1 200 OK`<EOL>`Via: 1.1 BN2BAP4ED8CB55D (wls-colorado)`<EOL>`Content-Length: 213`<EOL>`Content-Type: application/json; odata.metadata=minimal`<EOL>`Server: Microsoft-IIS/8.5`<EOL>`P3P: CP="BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo"`<EOL>`X-WLSPROXY: BN2BAP4ED8CB55D``X-MSNSERVER: DM5SCH102231619`<EOL>`Strict-Transport-Security: max-age=31536000; includeSubDomains`<EOL>`OData-Version: 4.0`<EOL>`X-AsmVersion: UNKNOWN; 16.0.0.0`<EOL>`X-MSEdge-Ref: Ref A: ECB06A4BE05B478AB36611C892C36CC7 Ref B: AM1EDGE0419 Ref C: Fri Feb 10 09:52:43 2017 PST``Date: Fri, 10 Feb 2017 17:52:43 GMT`<EOL>``<EOL>`{"@odata.context":"https://api.onedrive.com/v1.0/$metadata#drives('me')/items('root')/children(name,folder,file)","value":[{"name":"AB","folder":{"childCount":7}},{"name":"ArecaBackup","folder":{"childCount":6}}]}
Stat Disconnected.

Solution

  • Your MIME data is malformed, that is why the server is rejecting it.

    This is the request you are sending:

    POST /v1.0/drive/root::/children HTTP/1.0
    Content-Type: multipart/related; boundary="Boundary"
    Content-Length: 254
    Authorization: Bearer EwA...%3d
    Host: api.onedrive.com
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Encoding: identity
    User-Agent: Mozilla/3.0 (compatible; Indy Library)
    
    --Boundary
    Content-ID: <metadata>
    Content-Type: application/json
    {"name":"Dest.txt", "file":{}, "@content.sourceUrl":"cid:content"}
    --Boundary
    Content-ID: <content>
    Content-Type: application/octet-stream<SourceContent>--Boundary--
    

    As you can see, the MIME data is all messed up. Specifically, each MIME field is missing some required CRLFs. Just like with the HTTP headers and body, MIME headers and body are separated by a <CRLF><CRLF> sequence, and there needs to be a CRLF between the TFileStream data and the MIME boundary that follows it.

    The request needs to look more like this instead:

    POST /v1.0/drive/root::/children HTTP/1.0
    Content-Type: multipart/related; boundary="Boundary"
    Content-Length: 260
    Authorization: Bearer EwA...%3d
    Host: api.onedrive.com
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Accept-Encoding: identity
    User-Agent: Mozilla/3.0 (compatible; Indy Library)
    
    --Boundary
    Content-ID: <metadata>
    Content-Type: application/json
    
    {"name":"Dest.txt", "file":{}, "@content.sourceUrl":"cid:content"}
    --Boundary
    Content-ID: <content>
    Content-Type: application/octet-stream
    
    <SourceContent>
    --Boundary--
    

    Try this code to populate the TMemoryStream:

    MemoryStream := TMemoryStream.Create;
    try
      WriteLnString('--' + Boundary);
      WriteLnString('Content-ID: <metadata>');
      WriteLnString('Content-Type: application/json');
      WriteLnString(''); // <-- ADD THIS!!!
      WriteLnString('{"name":"Dest.txt", "file":{}, "@content.sourceUrl":"cid:content"}');
      WriteLnString('--' + Boundary);
      WriteLnString('Content-ID: <content>');
      WriteLnString('Content-Type: application/octet-stream'); // <-- REMOVE THE FALSE!!!
      WriteLnString(''); // <-- ADD THIS!!!
    
      FileStream := TFileStream.Create('Source.txt', fmOpenRead);
      try
        MemoryStream.CopyFrom(FileStream, 0);
      finally
        FileStream.Free;
      end;
    
      WriteLnString(''); // <!-- ADD THIS!!!
      WriteLnString('--' + Boundary + '--', False);
      ...
    finally
      MemoryStream.Free;
    end;
    

    That being said, Indy has a TIdMultipartFormDataStream class that is typically used with TIdHTTP when sending multipart/form-data posts. OneDrive does not support multipart/form-data, but what is odd is that OneDrive's documentation clearly specifies the following, which only applies to multipart/form-data and not to multipart/related:

    The request will be rejected if more than two parts are included. Each part must specify a name value in the Content-Disposition header that indicates which part it is. Parts can be in either order, but should specify the metadata part first.

    However, the example given in the same documentation is using multipart/related, just like your code is. There are discussions in Microsoft/OneDrive forums and various blogs regarding whether to use multipart/form-data or multipart/related when uploading to OneDrive. One OneDrive employee did confirm this issue needs some work on their end.

    Just in case OneDrive ever supports multipart/form-data, here is a example using TIdMultipartFormDataStream:

    procedure TSaveFilter.UploadTest;
    const
      URL = 'https://api.onedrive.com/v1.0/drive/root::/children';
    var
      IdHTTP: TIdHTTP;
      PostData: TIdMultipartFormDataStream;
    begin
      IdHTTP := TIdHTTP.Create(nil);
      try
        IdHTTP.HandleRedirects := True;
        IdHTTP.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(IdHTTP);
        IdHTTP.Request.BasicAuthentication := False;
        IdHTTP.Request.CustomHeaders.Values['Authorization'] := 'Bearer ' + FAccessToken;
    
        PostData := TIdMultipartFormDataStream.Create;
        try
          PostData.AddFormField('metadata', '{"name":"Dest.txt", "file":{}}', 'utf-8', 'application/json');
          PostData.AddFile('content', 'Source.txt', 'application/octet-stream').FileName := '';
          IdHTTP.Post(URL, PostData);
        finally
          PostData.Free;
        end;
      finally
        IdHTTP.Free;
      end;
    end;