http-pipeline: передача данных с помощью gzip

Я намереваюсь получить большой объем данных по HTTP / HTTPS, используя http-conduit. Чтобы сделать это эффективно, я хочу использовать заголовок Accept-Encoding: deflate,gzip, чтобы позволить серверу (если он поддерживается) передавать данные в сжатом виде.

Однако некоторые из серверов, с которых я хочу получить данные, похоже, неправильно отвечают заголовком Content-Encoding: gzip, не возвращая данные gzip.

Поэтому мне нужно разобраться со следующими случаями:

  • Сервер не поддерживает сжатие -> Вернуть текст простого ответа
  • Сервер возвращает сжатое / сжатое содержимое -> Вернуть распакованное тело ответа
  • Сервер говорит (в заголовках ответов он возвращает сжатый контент, но декодирование gzip не выполняется -> Вернуть простое тело ответа

В третьем случае (в данном конкретном случае) можно смело предположить, что текстовые несжатые данные не похожи на данные gzip, поэтому заголовки ответов говорят, что они сжаты с помощью gzip && un-gzip fails полностью эквивалентен Данные не сжимаются.

Как я могу сделать это с помощью http-conduit?

Примечание. В этом вопросе намеренно не отражены исследовательские усилия, потому что на него сразу же был дан ответ в стиле вопросов и ответов.


person Uli Köhler    schedule 15.02.2014    source источник


Ответы (1)


Чтобы сделать этот ответ более кратким, мы будем использовать код и концепции из некоторых из моих предыдущих сообщений:

  • simpleHttpWithManager из здесь
  • Терпимое декодирование gzip / deflate можно найти здесь

Чтобы избежать дублирования, я сначала объясню основные шаги, а затем приведу полный пример.

Сначала мы обработаем отправку заголовков. Обратите внимание, что если http-types содержит hContentEncoding, hAcceptEncoding не определено заранее. Кроме того, это тривиальная задача.

После отправки запроса нам нужно проверить, есть ли Content-Encoding. Если его нет, мы будем считать несжатый открытый текст, иначе нам нужно проверить, gzip или deflate. Какой именно, не имеет значения в данном контексте, поскольку [zlib] поддерживает автоматическое определение по заголовку.

В этом довольно простом примере мы просто предполагаем, что если сервер возвращает значение Content-Encoding, которое не отсутствует, ни gzip, ни deflate, ответ не сжимается. Поскольку мы не разрешили (на Accept-Encoding) другие сжатия, такие как sdch, сервер нарушит стандарт HTTP, действуя таким образом.

Если мы обнаруживаем сжатую кодировку, мы пытаемся распаковать и вернуть ее. Если это не удается или данные вообще не сжимаются, мы возвращаем простое тело ответа.

Вот пример:

{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Conduit
import Network.Connection
import Codec.Compression.Zlib.Internal
import Data.Maybe
import Data.Either
import Network.HTTP.Types
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Lazy.Char8 as LB

myurl :: String
myurl = "http://stackoverflow.com"

hAcceptEncoding :: HeaderName
hAcceptEncoding = "Accept-Encoding"

-- | The Accept-Encoding HTTP header value for allowing gzip or deflated responses
gzipDeflateEncoding :: ByteString
gzipDeflateEncoding = "gzip,deflate"

-- HTTP header list that allows gzipped/deflated response
compressionEnabledHeaders :: RequestHeaders
compressionEnabledHeaders = [(hAcceptEncoding, gzipDeflateEncoding)]

-- | Give an encoding string and a HTTP response object,
--   Checks if the Content-Encoding header value of the response object
--   is equal to the given encoding. Returns false if no ContentEncoding
--   header exists in the given response, or if the value does not match
--   the encoding parameter.
hasResponseEncoding :: ByteString -> Response b -> Bool
hasResponseEncoding encoding response =
    let responseEncoding = lookup hContentEncoding headers
        headers = responseHeaders response
    in maybe False (== encoding) responseEncoding

-- | Convert the custom error format from zlib to a Either
decompressStreamToEither :: DecompressStream -> Either String LB.ByteString
decompressStreamToEither (StreamError _ errmsg) = Left errmsg
decompressStreamToEither stream@(StreamChunk _ _) = Right $ fromDecompressStream stream
decompressStreamToEither StreamEnd = Right $ ""

-- | Decompress with explicit error handling
safeDecompress :: LB.ByteString -> Either String LB.ByteString
safeDecompress bstr = decompressStreamToEither $ decompressWithErrors gzipOrZlibFormat defaultDecompressParams bstr

-- | Decompress gzip, if it fails, return uncompressed String
decompressIfPossible :: LB.ByteString -> LB.ByteString
decompressIfPossible bstr =
    let conv (Left a) = bstr
        conv (Right a) = a
    in (conv . safeDecompress) bstr

-- | Tolerantly decompress response body. As some HTTP servers set the header incorrectly,
--   just return the plain response text if the compression fails
decompressResponseBody :: Response LB.ByteString -> LB.ByteString
decompressResponseBody res
    | hasResponseEncoding "gzip" res = decompressIfPossible $ responseBody res
    | hasResponseEncoding "deflate" res = decompressIfPossible $ responseBody res
    | otherwise = responseBody res

-- | Download like with simpleHttp, but using an existing manager for the task
--   and automatically requesting & handling gzipped data
simpleHttpWithAutoGzip :: Manager -> String -> IO LB.ByteString
simpleHttpWithAutoGzip manager url = do req <- parseUrl url
                                        let req' = req {requestHeaders = compressionEnabledHeaders}
                                        fmap decompressResponseBody $ httpLbs req' manager

-- Example usage
main :: IO ()
main = do manager <- newManager conduitManagerSettings -- Create a simple manager
          content <- simpleHttpWithAutoGzip manager "http://stackoverflow.com"
          -- Print the uncompressed content
          print $ content
person Uli Köhler    schedule 15.02.2014