TRestClient/TRestRequest错误地解码gzip响应

Gol*_*rol 6 delphi rest gzip utf-8 delphi-xe5

我试图读取一个REST API,它是gzip编码的.确切地说,我试图阅读StackExchange API.

我已经在TRESTResponse中找到了自动解码GZIP的问题但由于某些原因,这个答案并没有解决我的问题.

测试设置

在XE5中,我添加了TRestClient,TRestRequest和具有以下相关属性的TRestResponse.我设置了客户端的BaseURL,请求的资源和参数,以及我AcceptEncoding对请求的设置gzip, deflate,这应该使它自动解码gzipped响应.

  object RESTClient1: TRESTClient
    BaseURL = 'https://api.stackexchange.com/2.2'
  end
  object RESTRequest1: TRESTRequest
    AcceptEncoding = 'gzip, deflate'
    Client = RESTClient1
    Params = <
      item
        Kind = pkURLSEGMENT
        name = 'id'
        Options = [poAutoCreated]
        Value = '511529'
      end
      item
        name = 'site'
        Value = 'stackoverflow'
      end>
    Resource = 'users/{id}'
    Response = RESTResponse1
  end
  object RESTResponse1: TRESTResponse
  end
Run Code Online (Sandbox Code Playgroud)

这导致url:

https://api.stackexchange.com/2.2/users/511529?site=stackoverflow

我调用这样的请求,有两个消息框来显示url和请求的结果:

ShowMessage(RESTRequest1.GetFullRequestURL());
RESTRequest1.Execute; // Actual call
ShowMessage(RESTResponse1.Content);
Run Code Online (Sandbox Code Playgroud)

如果我在浏览器中调用该URL,我会得到一个正确的结果,这是一个json对象,其中包含我的一些用户信息.

问题

但是,在Delphi中,我没有得到JSON响应.实际上,我得到了一堆字节,这似乎是一个错误的gzip响应.我尝试用它解压缩TIdCompressorZlib.DecompressGZipStream(),但它失败了ZLib Error (-3).当我自己检查响应的字节时,我看到它从#1F#3F#08开始.这特别奇怪,因为gzip标题应该是#1F#8B#08,所以#8B转换为#3F,这是一个问号.

所以在我看来,RESTClient试图将gzip流解码为好像是UTF-8响应,并且用问号替换了无效序列(#8B本身不是有效的UTF-8字符).

尝试(表面)

我做了很多实验,比如

  • 使用RESTResponse.RawBytes并尝试解码它.我注意到这个字节数组中的字节已经无效.TRESTResponse来源中的评论告诉我'RawBytes'已经被解码,所以这是有道理的.
  • 将RESTResponse.RawBytes保存在一个文件中,并尝试使用7zip和几个在线gzip解压缩程序对其进行解压缩.当然,它们都失败了,因为即使是gzip标头也是错误的.
  • 将值'gzip,deflate'分配给TRESTClient.AcceptEncoding,TRESTResponse.AcceptEncoding以及它们的组合.还尝试将其附加到每个组件的预填充Accept属性.
  • 从经过身份验证的请求切换到未经身份验证的请求.我有整个oAuth部分工作,但我认为这会使问题过于复杂.我在这个问题中使用的匿名API也有同样的问题.

不幸的是它仍然无效,我仍然得到了错误的回应.

尝试(深入VCL)

最后,我挖了一点,然后潜入TRestRequest.Execute.我不会在这里粘贴所有代码,但最终它会通过调用来执行请求

FClient.HTTPClient.Get(LURL, LResponseStream);
Run Code Online (Sandbox Code Playgroud)

FClient是链接到请求的TRESTClient,LResponseStream是TMemoryStream.我添加LResponseStream.SaveToFile('...')到手表,所以它会保存这个未经处理的结果,etvoilá,它给了我一个有效的gz文件,我可以解压缩得到我的JSON.

解决方案中的错误?

但是,接下来几行,我看到这段代码:

  if FClient.HTTPClient.Response.CharSet > '' then
  begin
    LResponseStream.Position := 0;
    S := FClient.HTTPClient.ReadStringAsCharset(LResponseStream, FClient.HTTPClient.Response.CharSet);
    LResponseStream.Free;
    LResponseStream := TStringStream.Create(S);
  end;
Run Code Online (Sandbox Code Playgroud)

根据此块上面的注释,这样做是因为内存流的内容"未根据可能存在的Encoding或Content-Type Charset参数进行编码",这被VCL代码的编写者认为是Indy中的错误.

所以基本上,这里发生了什么:原始响应被视为一个字符串并转换为'正确'编码.FClient.HTTPClient.Response.CharSet是'UTF-8',它确实是JSON的编码,但不幸的是,这种转换只能在解压缩流之后才能完成,但尚未完成.所以我认为这是一个错误.;)

我试图深入挖掘,但我找不到应该进行减压的地方.实际请求由IIPHTTP实例执行,该实例是IPPeerAPI.dcu,我没有源代码.

所以...

所以我的问题是双重的:

  1. 为什么会这样?当您将AcceptEncoding设置为'gzip,deflate'时,TRestClient应自动解码gzip流.我错过了什么设置?或者XE5中是否支持此功能?
  2. 如何防止gzip流的错误转换?我不介意自己解码响应,只要它有效,尽管理想情况下REST组件应该自动执行.

我的设置:VCL Forms应用程序,Windows 8.1,Delphi XE5专业版更新2.

更新

  • 找到了解决方法(参见我的回答)
  • 错误报告RSP-9855在质量中心提交
  • 它应该在Delphi 10.1(柏林)中修复,但我还没有测试过.

Gol*_*rol 5

Remy Lebeau 在回答这个问题时的输入以及他对问题在 TRESTResponse 中自动解码 GZIP?让我走上正轨。

就像他说的,设置 AcceptEncoding 是不够的,因为执行实际请求的 TIdHTTP 没有附加解压缩器,因此它无法解压缩 gzip 响应。基于稀疏的资源,我想到设置 AcceptEncoding 也会自动解压缩响应,但这个想法是错误的。

不过,在这种情况下,将 AcceptEncoding 留空也不起作用,因为这涉及的 API(即 StackExchange API)始终是压缩的,无论您是否指定接受 gzip。

因此,a) 始终压缩的响应,b) 无法解压缩的 HTTP 客户端,以及 c) 错误地假设响应已经正确解压缩的 TRESTRequest 对象组合在一起,导致了这种情况。

我只看到两个解决方案,第一个是完全放弃 TRESTClient 并仅使用普通的 TIdHTTP 执行请求。遗憾的是,因为我的目标是探索新 REST 组件的可能性,看看它们如何让生活变得更轻松。

因此另一个解决方案是为内部使用的 TIdHTTP 分配一个压缩器。

我成功了,尽管不幸的是它取消了 TREST 组件试图引入的许多抽象。这是解决这个问题的代码:

var
  Http: TIdCustomHTTP;
begin
  // Get the TIdHTTP that performs the request.
  Http := (RESTRequest1 // The TRESTRequest object
    .Client // The TRESTClient
    .HTTPClient // A TRESTHTTP object that wraps HTTP communication
    .Peer // An IIPHTTP interface which is obtained through PeerFactory.CreatePeer
    .GetObject // A method to get the object instance of the interface
    as TIdCustomHTTP // The object instance, which is an TIdCustomHTTP.
  );

  // Attach a gzip decompressor to it.
  Http.Compressor := TIdCompressorZLib.Create(Http);
Run Code Online (Sandbox Code Playgroud)

之后,我可以使用 RESTRequest1 组件成功获取 JSON 响应(至少作为文本)。