TRestClient / TRestRequest错误地解码gzip响应

时间:2014-12-22 17:25:19

标签: delphi rest utf-8 gzip delphi-xe5

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

我已经找到问题Automatically Decode GZIP In TRESTResponse?,但由于某些原因,该答案无法解决我的问题。

测试设置

在XE5中,我添加了TRestClient,TRestRequest和具有以下相关属性的TRestResponse。我设置了客户端的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

这会产生网址:

  

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

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

ShowMessage(RESTRequest1.GetFullRequestURL());
RESTRequest1.Execute; // Actual call
ShowMessage(RESTResponse1.Content);

如果我在浏览器中调用该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);

FClient是链接到请求的TRESTClient,LResponseStream是TMemoryStream。我向手表添加了LResponseStream.SaveToFile('...'),所以它会保存这个未处理的结果,等等,它给了我一个有效的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;

根据上面的评论,这是因为内存流的内容是“未根据可能存在的编码或内容类型字符集参数进行编码”,这被认为是Indy中的一个错误。这个VCL代码。

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

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

因此...

所以我的问题有两个:

  1. 为什么会这样?当您将AcceptEncoding设置为'gzip,deflate'时,TRestClient应自动解码gzip流。我错过了什么设置?或者XE5中是否支持此功能?
  2. 如何防止gzip流的错误翻译?我不介意自己解码响应,只要它有效,尽管理想情况下REST组件应该自动执行。
  3. 我的设置:VCL Forms应用程序,Windows 8.1,Delphi XE5专业版更新2.

    更新

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

2 个答案:

答案 0 :(得分:4)

Remy Lebeau在回答这个问题时的意见,以及他对问题Automatically Decode GZIP In TRESTResponse?中答案的评论使我走上正轨。

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

但是,在这种情况下,将AcceptEncoding留空也不起作用,因为无论您是否指定接受gzip,这都是关于StackExchange API的API,always compressed。 / p>

所以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);

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

答案 1 :(得分:3)

  

AcceptEncoding =&#39; gzip,deflate&#39;

这是你问题的根源。您手动告诉服务器允许响应进行gzip编码,但据我在REST源代码中可以看到,TIdHTTP内部使用的基础TRESTClient对象没有gzip分配给它的解压缩程序(即使它有一个,手动分配AcceptEncoding仍然是错误的,因为如果分配了解压缩程序,TIdHTTP会设置自己的Accept-Encoding标头。我在您链接的other question中对此进行了评论。因此TIdHTTP最终返回原始gzip字节而不解码它们,然后TRESTClient将它们原样转换为字符集解码的UnicodeString(因为您正在阅读Content属性)。这就是为什么你看到字节搞砸了。

你需要摆脱AcceptEncoding任务。

  

为什么会这样?

因为TRestClient没有将gzip解压缩器分配给它的内部TIdHTTP对象,但是你欺骗服务器认为它确实存在。

  当您将AcceptEncoding设置为&#39; gzip,deflate&#39;

时,

会自动解码gzip流

不,因为没有分配解压缩程序。

更新:话虽如此,我可能会放弃TRESTClient并直接使用TIdHTTP。以下在我尝试时适用于我:

var
  HTTP: TIdHTTP;
  JSON: string;
begin
  HTTP := TIdHTTP.Create;
  try
    HTTP.Compressor := TIdCompressorZLib.Create(HTTP);
    // starting with SVN rev 5224, the TIdHTTP.IOHandler property no longer
    // needs to be explicitly set in order to request HTTPS urls.  TIdHTTP
    // now creates a default SSLIOHandler internally if needed.  But if you
    // are using an older release, you will have to assign the IOHandler... 
    //
    // HTTP.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(HTTP);
    //
    JSON := HTTP.Get('https://api.stackexchange.com/2.2/users/511529?site=stackoverflow');
  finally
    Http.Free;
  end;
  ShowMessage(JSON);
end;

显示器:

{"items":[{"badge_counts":{"bronze":96,"silver":53,"gold":4},"account_id":240984,"is_employee":false,"last_modified_date":1419235802,"last_access_date":1419293282,"reputation_change_year":15259,"reputation_change_quarter":2983,"reputation_change_month":1301,"reputation_change_week":123,"reputation_change_day":0,"reputation":61014,"creation_date":1290042241,"user_type":"registered","user_id":511529,"accept_rate":100,"location":"Netherlands","website_url":"http://www.eftepedia.nl","link":"https://stackoverflow.com/users/511529/goleztrol","display_name":"GolezTrol","profile_image":"https://www.gravatar.com/avatar/b07c67edfcc5d1496365503712de5c2a?s=128&d=identicon&r=PG"}],"has_more":false,"quota_max":300,"quota_remaining":295}