delphimicrosoft-graph-apiindyidhttp

Problem with content range with Indy TIdHttp put command against MS Graph API


I am trying to use TIdHttp.Put() for Microsoft's Graph API, but it is not possible to use the Content-Range header. If I use the Ranges property then I get an error about a missing Content-Range, and if I use this header in the CustomHeaders property then I get an error about an invalid Content-Range.

Here is the code:

sUploadSession := jsnUSession.Get('uploadUrl').JsonValue.Value;
Form1.htp2.Request.ContentType := 'application/octet-stream';
Form1.htp2.Request.ContentLength := iSize;          //  3820753
Form1.htp2.Request.CustomHeaders.Clear;
//Form1.htp.Request.CustomHeaders.Add('Content-Length: ' + IntToStr(iSize));
Form1.htp2.Request.CustomHeaders.Add('Content-Range: bytes 0-' + IntToStr(iSize - 1) + '/' + IntToStr(iSize));   //  'Content-Range: bytes 0-3820750/3820751'
{with Form1.htp2.Request.Ranges.Add do
begin
  StartPos := 0;
  EndPos := iSize;
  s := Text; 
end;   }
fs := TStringStream.Create('{' + TEncoding.Default.GetString(TFile.ReadAllBytes(sFile)) + '}');
bLog := True;
try
  Form1.htp2.Put(sUploadSession, fs);
except
  on E: EIdHTTPProtocolException do
    Form1.RichEdit1.Lines.Add(E.Message + #13#10 + E.ErrorMessage);
end;

The message when I use the Ranges property is:

HTTP/1.1 400 Bad Request
{"error":{"code":"MissingContentRangeHeader","message":"Content-Range header is required."}}

The message with Content-Range in CustomHeaders is:

HTTP/1.1 400 Bad Request
{"error":{"code":"InvalidContentRangeHeader","message":"Invalid Content-Range header."}}

Is the PUT command in Indy compatible with the standard in HTTP, or is it necessary to make tweaks to have it work?


Solution

  • The Range and Content-Range HTTP headers are two completely different things. See Difference between Content-Range and Range headers?

    Content-Range is for specifying the range of bytes that are in the body of the same message that contains the Content-Range header.

    The Range header is for requesting a range of bytes from the server. The response message will indicate, via its own Content-Range, the actual range being sent in the response body.

    So, that explains why you are getting a "missing Content-Range" error when using the TIdHTTP.Ranges property. That property is simply not intended for the purpose you are using it for.

    As for using the TIdHTTP.Request.CustomHeaders property to send a Content-Range header, that is the correct way to go (technically, TIdEntityHeaderInfo has ContentRange... properties, but they are currently only used by TIdHTTP.Response, not by TIdHTTP.Request - that needs to be fixed).

    The problem with your custom Content-Range header is that the server is simply rejecting it as bad. Which most likely means that the iSize value you are using doesn't actually match the number of bytes you are actually sending.

    Try something more like this instead:

    sUploadSession := jsnUSession.Get('uploadUrl').JsonValue.Value;
    fs := TStringStream.Create('{' + TEncoding.Default.GetString(TFile.ReadAllBytes(sFile)) + '}');
    try
      bLog := True;
      try
        iSize := fs.Size; // <-- the TStringStream constructor internally converted the String to TBytes...
        Form1.htp2.Request.ContentType := 'application/octet-stream';
        Form1.htp2.Request.ContentLength := iSize;
        Form1.htp2.Request.CustomHeaders.Clear;
        Form1.htp2.Request.CustomHeaders.Add('Content-Range: bytes 0-' + IntToStr(iSize - 1) + '/' + IntToStr(iSize));
    
        Form1.htp2.Put(sUploadSession, fs);
      except
        on E: EIdHTTPProtocolException do
          Form1.RichEdit1.Lines.Add(E.Message + #13#10 + E.ErrorMessage);
      end;
    finally
      fs.Free;
    end;
    

    However, there is no good reason to read a file into a decoded UTF-16 string just to convert it back to bytes for transmission, so just send the original file bytes as-is instead, eg:

    sUploadSession := jsnUSession.Get('uploadUrl').JsonValue.Value;
    fs := TFileStream.Create(sFile, fmOpenRead or fmShareDenyWrite);
    try
      bLog := True;
      try
        iSize := fs.Size;
        Form1.htp2.Request.ContentType := 'application/octet-stream';
        Form1.htp2.Request.ContentLength := iSize;
        Form1.htp2.Request.CustomHeaders.Clear;
        Form1.htp2.Request.CustomHeaders.Add('Content-Range: bytes 0-' + IntToStr(iSize - 1) + '/' + IntToStr(iSize));
    
        Form1.htp2.Put(sUploadSession, fs);
      except
        on E: EIdHTTPProtocolException do
          Form1.RichEdit1.Lines.Add(E.Message + #13#10 + E.ErrorMessage);
      end;
    finally
      fs.Free;
    end;
    

    Alternatively:

    sUploadSession := jsnUSession.Get('uploadUrl').JsonValue.Value;
    fs := TBytesStream.Create(TFile.ReadAllBytes(sFile));
    try
      bLog := True;
      try
        iSize := fs.Size;
        Form1.htp2.Request.ContentType := 'application/octet-stream';
        Form1.htp2.Request.ContentLength := iSize;
        Form1.htp2.Request.CustomHeaders.Clear;
        Form1.htp2.Request.CustomHeaders.Add('Content-Range: bytes 0-' + IntToStr(iSize - 1) + '/' + IntToStr(iSize));
    
        Form1.htp2.Put(sUploadSession, fs);
      except
        on E: EIdHTTPProtocolException do
          Form1.RichEdit1.Lines.Add(E.Message + #13#10 + E.ErrorMessage);
      end;
    finally
      fs.Free;
    end;
    

    Alternatively:

    sUploadSession := jsnUSession.Get('uploadUrl').JsonValue.Value;
    fs := TMemoryStream.Create;
    try
      bLog := True;
      try
        fs.LoadFromFile(sFile);
        fs.Position := 0;
        iSize := fs.Size;
        Form1.htp2.Request.ContentType := 'application/octet-stream';
        Form1.htp2.Request.ContentLength := iSize;
        Form1.htp2.Request.CustomHeaders.Clear;
        Form1.htp2.Request.CustomHeaders.Add('Content-Range: bytes 0-' + IntToStr(iSize - 1) + '/' + IntToStr(iSize));
    
        Form1.htp2.Put(sUploadSession, fs);
      except
        on E: EIdHTTPProtocolException do
          Form1.RichEdit1.Lines.Add(E.Message + #13#10 + E.ErrorMessage);
      end;
    finally
      fs.Free;
    end;
    

    Either way, do be aware that Microsoft recommends NOT uploading more than 4MB at a time when using this PUT API. Your example file is 3.6MB, so it will just fit inside a single PUT request, but just know that for larger files, you are going to have to break them up into multiple 4MB uploads, paying attention to the NextExpectedRanges field in each successful response.