2014-12-22 8 views
5

Ho provato a leggere un'API REST, che è codificata con gzip. Per essere precisi, ho provato a leggere l'API di StackExchange.TRestClient/TRestRequest decodifica in modo errato la risposta gzip

Ho già trovato la domanda Automatically Decode GZIP In TRESTResponse?, ma quella risposta non risolve il mio problema per qualche motivo.

configurazione di prova

In XE5, ho aggiunto un TRestClient, un TRestRequest e un TRestResponse con le seguenti proprietà rilevanti. Ho impostato BaseURL del client, la risorsa e i parametri della richiesta e ho impostato AcceptEncoding della richiesta su gzip, deflate, che dovrebbe consentire la decodifica automatica delle risposte gzipped.

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 

Il risultato è l'url:

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

invoco la richiesta in questo modo, con due finestre di messaggio per mostrare l'url e l'esito della richiesta:

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

Se chiamo quell'URL in un browser, ottengo una r corretta esult, che è un oggetto json con alcune delle mie informazioni utente in esso.

Problema

Tuttavia, a Delfi, non ottengo la risposta JSON. In effetti, ottengo un mucchio di byte che sembra essere una risposta gzip smodata. Ho provato a decomprimerlo con TIdCompressorZlib.DecompressGZipStream(), ma non riesce con uno ZLib Error (-3). Quando controllo i byte della risposta, vedo che inizia con # 1F # 3F # 08. Questo è particolarmente strano, poiché l'intestazione gzip dovrebbe essere # 1F # 8B # 08, quindi # 8B viene trasformato in # 3F, che è un punto interrogativo.

Quindi mi sembra che RESTClient abbia tentato di decodificare lo stream gzip come se fosse una risposta UTF-8 e ha sostituito sequenze non valide (# 8B non è un carattere UTF-8 valido) con un punto interrogativo.

tentativi (superficiali)

che ho fatto un po 'di sperimentazione, come

  • Usa RESTResponse.RawBytes e cercare di decodificarlo. Ho notato che i byte in questo array di byte sono già non validi. I commenti nella fonte di TRESTResponse mi hanno insegnato che 'RawBytes' è già decodificato, quindi è logico.
  • RESTResponse.RawBytes salvato in un file e provato a decomprimerlo con 7zip e un paio di decompressori gzip online. Hanno fallito tutti, naturalmente, dal momento che anche l'intestazione gzip non è corretta.
  • Assegnato il valore 'gzip, deflate' a TRESTClient.AcceptEncoding, TRESTResponse.AcceptEncoding e una combinazione di questi. Ho anche provato ad aggiungerlo alla proprietà Accept pre-compilata di ciascuno di questi componenti.
  • Passaggio da autenticazione a richiesta non autenticata. Ho avuto l'intera parte di oAuth al lavoro, ma pensavo che ciò avrebbe reso la domanda troppo complessa.L'API anonima che ho usato in questa domanda ha lo stesso problema, però.

Sfortunatamente non funziona ancora e ho ancora una risposta maciullata.

tentativi (scavare nel VCL)

Infine, ho scavato un po 'più profondo, e dove in TRestRequest.Execute. Non voglio incollare tutto il codice qui, ma alla fine si effettua la richiesta chiamando

FClient.HTTPClient.Get(LURL, LResponseStream); 

FClient è il TRESTClient che è legata alla richiesta e LResponseStream è un TMemoryStream. Ho aggiunto LResponseStream.SaveToFile('...') agli orologi, in modo da salvare questo risultato non elaborato, et voilá, mi ha dato un file gz valido, che ho potuto decomprimere per ottenere il mio JSON.

Un bug nel lavoro?

Ma poi, un paio di righe in basso, vedo questo pezzo di codice:

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; 

accordo con il commento di sopra di questo blocco, questo è fatto perché i contenuti del flusso di memoria "non sono codificati di conseguenza ad un eventuale parametro Encoding o tipo di contenuto Charset ", che è considerato un bug in Indy dal writer di questo codice VCL.

Quindi, in pratica, cosa succede qui: la risposta non elaborata viene considerata una stringa e convertita nella codifica "giusta". FClient.HTTPClient.Response.CharSet è 'UTF-8', che è in effetti la codifica del JSON, ma sfortunatamente, questa conversione dovrebbe essere eseguita solo dopo la decompressione del flusso, che non è ancora stato fatto. Quindi questo è considerato un bug da me. ;)

Ho provato a scavare più a fondo, ma non sono riuscito a trovare il punto in cui questa decompressione avrebbe dovuto aver luogo. La richiesta effettiva viene eseguita da un'istanza IIPHTTP, che è IPPeerAPI.dcu di cui non ho l'origine.

Quindi ...

Quindi la mia domanda è duplice:

  1. Perché accade questo? TRestClient dovrebbe decodificare automaticamente il flusso gzip quando si imposta AcceptEncoding su "gzip, deflate". Che ambiente mi è mancato? O non è ancora supportato in XE5?
  2. Come si impedisce questa traduzione errata del flusso gzip? Non mi interessa decodificare la risposta da solo, finché funziona, anche se idealmente i componenti REST dovrebbero farlo automaticamente.

La mia configurazione: VCL Form, Windows 8.1, Delphi XE5 aggiornamento professionale 2.

Aggiornamento

  • work-around è stato trovato (vedi la mia risposta)
  • Segnalazione di bug RSP-9855 archiviato nella centrale di qualità
  • È presumibilmente corretto in Delphi 10.1 (Berlino), ma devo ancora verificarlo.

risposta

4
ingresso di

Remy Lebeau nella sua risposta a questa domanda, così come il suo commento alla risposta della domanda Automatically Decode GZIP In TRESTResponse? mi ha messo su la pista giusta.

Come ha detto, impostare AcceptEncoding non è sufficiente, poiché il TIdHTTP che esegue la richiesta effettiva non ha un decompressore collegato, quindi non può decomprimere la risposta gzip. Basandomi sulle scarse risorse, ho avuto l'idea che l'impostazione di AcceptEncoding avrebbe automaticamente decompresso anche la risposta, ma quell'idea era sbagliata.

Tuttavia, lasciare AcceptEncoding vuoto non funziona neanche in questo caso, poiché l'API è tutto questo, ovvero l'API StackExchange, è always compressed, indipendentemente dal fatto che si specifichi di accettare o meno gzip.

Quindi la combinazione di a) una risposta sempre compressa, b) un client HTTP che non può decomprimere ec) un oggetto TRESTRequest che - in modo errato - presuppone che la risposta sia già correttamente decompresso insieme portano a questa situazione.

Vedo solo due soluzioni, la prima è quella di scartare completamente TRESTClient e basta eseguire la richiesta con un semplice TIdHTTP.Un vero peccato, poiché il mio obiettivo era esplorare le possibilità dei nuovi componenti REST per vedere come possono rendere la vita più facile.

Quindi l'altra soluzione è assegnare un compressore al TIdHTTP utilizzato internamente.

Sono riuscito a riuscirci, anche se sfortunatamente esso annulla molte delle astrazioni che i componenti di TREST stanno cercando di introdurre. Questo è il codice che lo risolve:

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

Dopo questo, posso utilizzare il componente RESTRequest1 per recuperare con successo la risposta JSON (almeno come testo).

3

AcceptEncoding = 'gzip, sgonfiare'

Questa è la radice del problema. Si sta dicendo manualmente al server che la risposta può essere codificata con gzip, ma per quanto posso vedere nel codice sorgente REST, l'oggetto TIdHTTP sottostante che TRESTClient utilizza internamente non ha un decompressore gzip assegnato ad esso (anche se esso ne aveva uno, assegnando manualmente AcceptEncoding sarebbe ancora sbagliato, perché TIdHTTP imposta la propria intestazione Accept-Encoding se è stato assegnato un decompressore). Ho commentato questo nello other question a cui ti sei collegato. Quindi, TIdHTTP restituisce i byte gzip non decodificati, quindi TRESTClient li converte così com'è in un set di caratteri decodificato UnicodeString (poiché stai leggendo la proprietà Content). Questo è il motivo per cui stai vedendo i byte incasinati.

È necessario eliminare l'assegnazione AcceptEncoding.

Perché succede?

Perché TRestClient non assegna un decompressore gzip al suo TIdHTTP oggetto interno, ma si stanno ingannando il server a pensare che ha fatto.

dovrebbe decodificare automaticamente il flusso gzip quando si imposta AcceptEncoding a 'gzip, sgonfiare'

No, perché non c'è decompressore assegnato.

Aggiornamento: detto questo, probabilmente dovrei semplicemente inserire TRESTClient e utilizzare TIdHTTP direttamente. I seguenti lavori per me quando ho provato:

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; 

Displays:

{"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} 
+0

Grazie, ma purtroppo non del tutto vero. Forse non ero troppo chiaro a riguardo, ma impostare AcceptEncoding era già un tentativo di risolvere il problema. All'inizio, non l'ho fatto e ho avuto lo stesso problema. Lo snippet che ho postato cerca sempre di tradurre il flusso dei risultati (sia la proprietà 'Content' che la proprietà' RawBytes'). Il fatto che 'encoding' sia 'gzip' è completamente ignorato, e il flusso dei risultati viene sempre processato prima di assegnarlo a Response, in modo che influenzi anche RawBytes. La risposta effettiva non elaborata è già stata elaborata nel metodo Execute. – GolezTrol

+0

Sembra un bug di logica 'TRESTClient' (non un bug' TIdHTTP'). Lo hai segnalato ad Embarcadero? In ogni caso, se 'AcceptEncoding' non è impostato, il server deve inviare l'effettivo JSON non codificato, che' TRESTClient' quindi decodificherebbe in 'String'. Se la decodifica non è corretta, probabilmente il 'charset' specificato è probabilmente sbagliato. Puoi mostrare la risposta REST effettiva che viene trasmessa dal server? –

+0

Concordo sul fatto che sembra essere un bug TRESTClient. Non l'ho ancora (ancora) segnalato, e non sono sicuro di quanto sia utile. Non penso che faranno aggiornamenti per XE5. Ma lo prenderò in considerazione, poiché il problema potrebbe esistere anche nelle versioni più recenti. – GolezTrol

Problemi correlati