【问题标题】:TRestClient/TRestRequest incorrectly decodes gzip responseTRESTClient/TRESTRequest 错误地解码 gzip 响应
【发布时间】:2015-02-20 20:04:48
【问题描述】:

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

我已经找到了问题Automatically Decode GZIP In TRESTResponse?,但由于某种原因,该答案并不能解决我的问题。

测试设置

在 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

这会导致网址:

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 标头也不正确。
  • 为 TRESTClient.AcceptEncoding、TRESTResponse.AcceptEncoding 和它们的组合分配了值“gzip, deflate”。还尝试将其附加到每个组件的预填充 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;

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

所以基本上,这里发生了什么:原始响应被视为字符串并转换为“正确”编码。 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 Professional Update 2。

更新

  • 找到了解决方法(请参阅我的回答)
  • 错误报告RSP-9855 已提交质量中心
  • 据说它已在 Delphi 10.1(柏林)中修复,但我尚未对此进行测试。

【问题讨论】:

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


    【解决方案1】:

    Remy Lebeau 对这个问题的回答以及他对问题Automatically Decode GZIP In TRESTResponse? 的回答的评论让我走上了正轨。

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

    不过,在这种情况下,将 AcceptEncoding 留空也不起作用,因为无论您是否指定接受 gzip,API 即 StackExchange API 都是 always compressed

    因此,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 响应(至少作为文本)。

    【讨论】:

      【解决方案2】:

      AcceptEncoding = 'gzip,放气'

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

      您需要摆脱 AcceptEncoding 分配。

      为什么会这样?

      因为TRestClient 没有为其内部的TIdHTTP 对象分配gzip 解压缩器,但您正在欺骗服务器使其认为它确实如此。

      当您将 AcceptEncoding 设置为 'gzip, deflate' 时应该自动解码 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}
      

      【讨论】:

      • 谢谢,但不幸的是,这并不完全正确。也许我对此不是很清楚,但是设置 AcceptEncoding 已经是解决问题的一种尝试。起初,我没有这样做,但仍然有同样的问题。我发布的 sn-p 总是试图翻译结果流(Content 属性和RawBytes 属性). The fact that encoding`都是'gzip'被完全忽略,并且总是在分配它之前处理结果流响应,因此这也会影响 RawBytes。实际的、未处理的响应已经在 Execute 方法中被丢弃。
      • 听起来像 TRESTClient 逻辑错误(不是 TIdHTTP 错误)。你向Embarcadero报告了吗?在任何情况下,如果AcceptEncoding 未设置,那么服务器应该发送实际未编码的JSON,TRESTClient 然后将解码为String。如果解码不正确,那么指定的charset 可能是错误的。您能否显示服务器正在传输的实际 REST 响应?
      • 我同意这似乎是一个 TRESTClient 错误。我还没有(还)报告它,我不确定它有多大用处。我认为他们不会为 XE5 进行更新。但我会考虑它,因为新版本中也可能存在问题。
      • REST 响应与我在问题中给出的 url 的响应相同。 The StackExchange API responses are always compressed,无论您是否指定 AcceptEncoding,所以如果我将 AcceptEncoding 保持为空,我就不会得到纯 JSON。明天(这里已经过了午夜),我将打开我的技巧框并尝试将解压缩器添加到 TRESTClient 内部使用的 IdHTTP 组件。这将是一个 hack,但希望我可以在不修改实际 VCL 的情况下制作一个。 :-)
      • 不是开箱即用的,没有。但这让我想到了,现在 TIdHTTP 支持 HTTPS 而无需显式地将 SSLIOHandler 组件分配给 IOHandler 属性(SVN rev 5224,尽管您仍然需要在 uses 子句中包含相关的 SSLIOHandler 单元来激活它功能),让我觉得我可以扩展该机制以允许 TIdHTTP 在需要时在内部创建默认的 Compressor
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-03-27
      • 1970-01-01
      • 2019-12-23
      相关资源
      最近更新 更多