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?
CodePudding user response:
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.