Delphi System.net.HTTPClient: 读取数据时出错 (12002) 操作超时

Delphi System.net.HTTPClient: Error reading data (12002) The operation timed out

我使用 System.net.HTTPClient 在 Berlin Update 2 上使用此设备从 AWS S3 下载大文件(>500 MB):

unit AcHTTPClient;

interface

uses
  System.Net.URLClient, System.net.HTTPClient;

type
  TAcHTTPProgress = procedure(const Sender: TObject; AStartPosition : Int64; AEndPosition: Int64; AContentLength: Int64; AReadCount: Int64; ATimeStart : Int64; ATime : Int64; var Abort: Boolean) of object;
  TAcHTTPClient = class
    private
      FOnProgress:     TAcHTTPProgress;
      FHTTPClient:     THTTPClient;
      FTimeStart:      cardinal;
      FCancelDownload: boolean;
      FStartPosition:  Int64;
      FEndPosition:    Int64;
      FContentLength:  Int64;
    private
      procedure   SetProxySettings(AProxySettings: TProxySettings);
      function    GetProxySettings : TProxySettings;
      procedure   OnReceiveDataEvent(const Sender: TObject; AContentLength: Int64; AReadCount: Int64; var Abort: Boolean);
    public
      constructor Create;
      destructor  Destroy; override;
      property    ProxySettings : TProxySettings read FProxySettings write SetProxySettings;
      property    OnProgress : TAcHTTPProgress read FOnProgress write FOnProgress;
      property    CancelDownload : boolean read FCancelDownload write FCancelDownload;
      function    Download(const ASrcUrl : string; const ADestFileName : string): Boolean;
  end;

implementation

uses
  System.Classes, System.SysUtils, Winapi.Windows;

constructor TAcHTTPClient.Create;
// -----------------------------------------------------------------------------
// Constructor
begin
  inherited Create;

  // create an THTTPClient
  FHTTPClient := THTTPClient.Create;
  FHTTPClient.OnReceiveData := OnReceiveDataEvent;

  // setting the timeouts
  FHTTPClient.ConnectionTimeout :=  5000;
  FHTTPClient.ResponseTimeout   := 15000;

  // initialize the class variables
  FCancelDownload := false;
  FOnProgress     := nil;
  FEndPosition    := -1;
  FStartPosition  := -1;
  FContentLength  := -1;
end;


destructor TAcHTTPClient.Destroy;
// -----------------------------------------------------------------------------
// Destructor
begin
  FHTTPClient.free;

  inherited Destroy;
end;


procedure TAcHTTPClient.SetProxySettings(AProxySettings: TProxySettings);
// -----------------------------------------------------------------------------
// Set FHTTPClient.ProxySettings with AProxySettings
begin
  FHTTPClient.ProxySettings := AProxySettings;
end;


function TAcHTTPClient.GetProxySettings : TProxySettings;
// -----------------------------------------------------------------------------
// Get FHTTPClient.ProxySettings
begin
  Result := FHTTPClient.ProxySettings;
end;


procedure TAcHTTPClient.OnReceiveDataEvent(const Sender: TObject; AContentLength: Int64; AReadCount: Int64; var Abort: Boolean);
// -----------------------------------------------------------------------------
// HTTPClient.OnReceiveDataEvent become OnProgress
begin
  Abort := CancelDownload;

  if Assigned(OnProgress) then
    OnProgress(Sender, FStartPosition, FEndPosition, AContentLength, AReadCount, FTimeStart, GetTickCount,  Abort);
end;


function TAcHTTPClient.Download(const ASrcUrl : string; const ADestFileName : string): Boolean;
// -----------------------------------------------------------------------------
// Download a file from ASrcUrl and store to ADestFileName
var
  aResponse:           IHTTPResponse;
  aFileStream:         TFileStream;
  aTempFilename:       string;
  aAcceptRanges:       boolean;
  aTempFilenameExists: boolean;
begin
  Result         := false;
  FEndPosition   := -1;
  FStartPosition := -1;
  FContentLength := -1;

  aResponse   := nil;
  aFileStream := nil;
  try
    // raise an exception if the file already exists on ADestFileName 
    if FileExists(ADestFileName) then
      raise Exception.Create(Format('the file %s alredy exists', [ADestFileName]));

    // reset the CancelDownload property
    CancelDownload := false;

    // set the time start of the download
    FTimeStart := GetTickCount;

    // until the download is incomplete the ADestFileName has *.parts extension 
    aTempFilename := ADestFileName + '.parts';

    // get the header from the server for aSrcUrl
    aResponse := FHTTPClient.Head(aSrcUrl);

    // checks if the response StatusCode is 2XX (aka OK) 
    if (aResponse.StatusCode < 200) or (aResponse.StatusCode > 299) then
      raise Exception.Create(Format('Server error %d: %s', [aResponse.StatusCode, aResponse.StatusText]));

    // checks if the server accept bytes ranges 
    aAcceptRanges := SameText(aResponse.HeaderValue['Accept-Ranges'], 'bytes');

    // get the content length (aka FileSize)
    FContentLength := aResponse.ContentLength;

    // checks if a "partial" download already exists
    aTempFilenameExists := FileExists(aTempFilename);

    // if a "partial" download already exists
    if aTempFilenameExists then
    begin
      // re-utilize the same file stream, with position on the end of the stream
      aFileStream := TFileStream.Create(aTempFilename, fmOpenWrite or fmShareDenyNone);
      aFileStream.Seek(0, TSeekOrigin.soEnd);
    end else begin
      // create a new file stream, with the position on the beginning of the stream
      aFileStream := TFileStream.Create(aTempFilename, fmCreate);
      aFileStream.Seek(0, TSeekOrigin.soBeginning);
    end;

    // if the server doesn't accept bytes ranges, always start to write at beginning of the stream
    if not(aAcceptRanges) then
      aFileStream.Seek(0, TSeekOrigin.soBeginning);

    // set the range of the request (from the stream position to server content length)
    FStartPosition := aFileStream.Position;
    FEndPosition   := FContentLength;

    // if the range is incomplete (the FStartPosition is less than FEndPosition)
    if (FEndPosition > 0) and (FStartPosition < FEndPosition) then
    begin
      // ... and if a starting point is present
      if FStartPosition > 0 then
      begin
        // makes a bytes range request from FStartPosition to FEndPosition
        aResponse := FHTTPClient.GetRange(aSrcUrl, FStartPosition, FEndPosition, aFileStream);
      end else begin
        // makes a canonical GET request
        aResponse := FHTTPClient.Get(aSrcUrl, aFileStream);
      end;

      // check if the response StatusCode is 2XX (aka OK) 
      if (aResponse.StatusCode < 200) or (aResponse.StatusCode > 299) then
        raise Exception.Create(Format('Server error %d: %s', [aResponse.StatusCode, aResponse.StatusText]));
    end;

    // if the FileStream.Size is equal to server ContentLength, the download is completed!
    if (aFileStream.Size > 0) and (aFileStream.Size = FContentLength) then begin

      // free the FileStream otherwise doesn't renames the "partial file" into the DestFileName
      FreeAndNil(aFileStream);

      // renames the aTempFilename file into the ADestFileName 
      Result := RenameFile(aTempFilename, ADestFileName);

      // What?
      if not(Result) then
        raise Exception.Create(Format('RenameFile from %s to %s: %s', [aTempFilename, ADestFileName, SysErrorMessage(GetLastError)]));
    end;
  finally
    if aFileStream <> nil then aFileStream.Free;
    aResponse := nil;
  end;
end;

end.

有时我会看到这个异常:

Error reading data: (12002) The operation timed out

我在 System.NetConsts.pas:

中找到了这个错误字符串
SNetHttpRequestReadDataError = 'Error reading data: (%d) %s';

并且错误被引发到 System.Net.HttpClient.Win.pas(参见 @SNetHttpRequestReadDataError):

procedure TWinHTTPResponse.DoReadData(const AStream: TStream);
var
  LSize: Cardinal;
  LDownloaded: Cardinal;
  LBuffer: TBytes;
  LExpected, LReaded: Int64;
  LStatusCode: Integer;
  Abort: Boolean;
begin
  LReaded := 0;
  LExpected := GetContentLength;
  if LExpected = 0 then
    LExpected := -1;
  LStatusCode := GetStatusCode;
  Abort := False;
  FRequestLink.DoReceiveDataProgress(LStatusCode, LExpected, LReaded, Abort);
  if not Abort then
    repeat
      // Get the size of readed data in LSize
      if not WinHttpQueryDataAvailable(FWRequest, @LSize) then
        raise ENetHTTPResponseException.CreateResFmt(@SNetHttpRequestReadDataError, [GetLastError, SysErrorMessage(GetLastError, FWinHttpHandle)]);

      if LSize = 0 then
        Break;

      SetLength(LBuffer, LSize + 1);

      if not WinHttpReadData(FWRequest, LBuffer[0], LSize, @LDownloaded) then
        raise ENetHTTPResponseException.CreateResFmt(@SNetHttpRequestReadDataError, [GetLastError, SysErrorMessage(GetLastError, FWinHttpHandle)]);

      // This condition should never be reached since WinHttpQueryDataAvailable
      // reported that there are bits to read.
      if LDownloaded = 0 then
        Break;

      AStream.WriteBuffer(LBuffer, LDownloaded);
      LReaded := LReaded + LDownloaded;
      FRequestLink.DoReceiveDataProgress(LStatusCode, LExpected, LReaded, Abort);
    until (LSize = 0) or Abort;
end;

是什么导致了这个错误?

您可以尝试将 ConnectTimeout、SendTimeout 和 ReceiveTimeout 增加到 15000 以上吗?例如说 300000(5 分钟)

即:

  FHTTPClient.ConnectionTimeout := 300000;
  FHTTPClient.ResponseTimeout   := 300000;