我尝试读取一个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字符)标记。
尝试(肤浅)
我做了很多实验,比如
不幸的是它仍然不起作用,我仍然得到了错误的回应。
尝试(深入了解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,其中没有源代码。
因此...
所以我的问题有两个:
我的设置:VCL Forms应用程序,Windows 8.1,Delphi XE5专业版更新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}