Delphi TRestClient/TRestRequest错误解码gzip响应

Delphi TRestClient/TRestRequest错误解码gzip响应,delphi,rest,utf-8,gzip,delphi-xe5,Delphi,Rest,Utf 8,Gzip,Delphi Xe5,我试图阅读一个RESTAPI,它是gzip编码的。确切地说,我试图阅读StackExchange API 我已经找到了这个问题,但由于某种原因,这个答案并不能解决我的问题 测试设置 在XE5中,我添加了一个TresClient、一个TresRequest和一个具有以下相关属性的TresResponse。我设置了客户端的BaseURL、请求的资源和参数,并将请求的AcceptEncoding设置为gzip,deflate,这将使它自动解码gzip响应 object RESTClient1: T

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

我已经找到了这个问题,但由于某种原因,这个答案并不能解决我的问题

测试设置

在XE5中,我添加了一个TresClient、一个TresRequest和一个具有以下相关属性的TresResponse。我设置了客户端的BaseURL、请求的资源和参数,并将请求的
AcceptEncoding
设置为
gzip,deflate
,这将使它自动解码gzip响应

  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
如果我在浏览器中调用该url,我会得到一个正确的结果,这是一个json对象,其中包含一些我的用户信息

问题

然而,在Delphi中,我没有得到JSON响应。事实上,我得到了一堆字节,这似乎是一个损坏的gzip响应。我试图用
TIdCompressorZlib.DecompressGZipStream()
对其进行解压缩,但由于
ZLib错误(-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);
FClient是链接到请求的TRESTClient,LResponseStream是一个TMemoryStream。我在手表中添加了
LResponseStream.SaveToFile(“…”)
,这样它就可以保存这个未经处理的结果,等等,它给了我一个有效的gz文件,我可以将其解压缩以获得JSON

工作中的一个bug?

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

  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;
根据该块上面的注释,之所以这样做是因为内存流的内容“没有根据可能存在的编码或内容类型字符集参数进行编码”,这被该VCL代码的编写者认为是Indy中的错误

基本上,这里发生了什么:原始响应被视为字符串,并转换为“正确”编码。FClient.HTTPClient.Response.CharSet是“UTF-8”,这确实是JSON的编码,但不幸的是,此转换只能在解压流后完成,而解压流尚未完成。所以我认为这是一个bug

我试着挖得更深,但我找不到减压应该发生的地方。实际请求由IIPHTTP实例执行,该实例是IPPeerAPI.dcu,我没有该实例的源代码

所以…

所以我的问题有两个:

  • 为什么会发生这种情况?当您将AcceptEncoding设置为“gzip,deflate”时,TRestClient应该自动解码gzip流。我错过了什么场景?或者这在XE5中还不受支持吗
  • 如何防止gzip流的这种错误翻译?我不介意自己解码响应,只要它有效,尽管理想情况下,其余组件应该自动解码
  • 我的设置:VCL窗体应用程序、Windows 8.1、Delphi XE5 professional Update 2

    更新

    • 找到了变通方法(请参见我的答案)
    • 质量中心提交的缺陷报告
    • 它被认为是在Delphi10.1(柏林)中修复的,但我还没有对此进行测试
    AcceptEncoding='gzip,deflate'

    这是你问题的根源。您正在手动告诉服务器允许对响应进行gzip编码,但就我在REST源代码中所见,
    TRESTClient
    内部使用的底层
    TIdHTTP
    对象没有为其分配gzip解压器(即使它有一个,手动分配
    AcceptEncoding
    仍然是错误的,因为
    TIdHTTP
    设置了自己的
    AcceptEncoding
    头(如果分配了解压器).I在您链接到的中对此进行了评论。因此
    TIdHTTP
    最终返回原始gzip字节而不进行解码,然后
    TRESTClient
    将其原样转换为已解码的字符集
    UnicodeString
    (因为您正在读取
    内容
    属性).这就是为什么你会看到字节变得混乱的原因

    您需要摆脱
    AcceptEncoding
    分配

    为什么会发生这种情况

    因为
    TRestClient
    没有将gzip解压器分配给其内部
    TIdHTTP
    对象,但是您正在欺骗服务器,使其认为它是这样做的

    应该自动
      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;
    
    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);