Delphi TRestClient/TRestRequest错误解码gzip响应
我试图阅读一个RESTAPI,它是gzip编码的。确切地说,我试图阅读StackExchange API 我已经找到了这个问题,但由于某种原因,这个答案并不能解决我的问题 测试设置 在XE5中,我添加了一个TresClient、一个TresRequest和一个具有以下相关属性的TresResponse。我设置了客户端的BaseURL、请求的资源和参数,并将请求的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
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也有同样的问题
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,我没有该实例的源代码
所以…
所以我的问题有两个:
- 找到了变通方法(请参见我的答案)
- 质量中心提交的缺陷报告
- 它被认为是在Delphi10.1(柏林)中修复的,但我还没有对此进行测试
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);