-- -- Copyright © 2015 Insurance Group Australia -- -- The code in this file, and the program it is a part of, is -- made available to you by its authors as open source software: -- you can redistribute it and/or modify it under the terms of -- the 3-clause BSD licence. -- {-# LANGUAGE RankNTypes #-} -- | Adapter code to interface with the http-client and http-client-tls -- packages. -- -- With this module you can streaming the request, response, or both. -- -- To stream the request, simply replace your 'Request' 'requestBody' with one -- created via either 'makeChunkedRequestBody' or 'makeFixedRequestBody'. You -- have the option of either chunked encoding or a fixed size streaming -- request. You probably want chunked encoding if that is supported. -- -- To stream the response, use streamHTTP and provide a continuation that -- returns a 'Consumer'. Your function will be passed the response, which it is -- free to ignore. -- -- To stream the request and response, simply do both. -- -- Below is an example of a streaming response, with a streaming request. -- -- > {-# LANGUAGE RankNTypes #-} -- > {-# LANGUAGE OverloadedStrings #-} -- > -- > module Main where -- > -- > import Control.Quiver.HTTP -- > import Control.Quiver -- > import Control.Quiver.SP -- > import Control.Monad -- > import Data.ByteString (ByteString) -- > import qualified Data.ByteString.Char8 as BS -- > main :: IO () -- > main = do -- > req <- parseUrl "http://httpbin.org/post" -- > let req' = req { method = "POST", requestBody = makeChunkedRequestBody input} -- > -- > manager <- newManager defaultManagerSettings -- > streamHTTP req' manager out -- > where -- > out :: Response () -> Consumer () ByteString IO () -- > out _ = loop -- > where -- > loop = do -- > x <- fetch () -- > case x of -- > Just bs -> do -- > qlift $ BS.putStrLn bs -- > loop -- > Nothing -> return () -- > -- > input :: Producer ByteString () IO () -- > input = void $ decouple ("chunk1" >:> "chunk2" >:> deliver SPComplete) module Control.Quiver.HTTP ( module Network.HTTP.Client, module Network.HTTP.Client.TLS, -- * Response streaming streamHTTP, -- * Request streaming makeChunkedRequestBody, makeFixedRequestBody ) where import Control.Monad (unless) import Control.Quiver import Control.Quiver.Internal import Data.ByteString (ByteString) import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L import Data.Int (Int64) import Data.IORef (newIORef, readIORef, writeIORef) import Network.HTTP.Client import Network.HTTP.Client.TLS -- | Make a HTTP 'Request' and stream the response. streamHTTP :: Request -- ^ The http-client 'Request', this need not be streaming. -> Manager -- ^ The http-client 'Manager', make sure you use the tls manager if you -- need SSL. -> (Response () -> Consumer x ByteString IO a) -- ^ Your quiver 'Consumer' continuation. Feel free to ignore the Response. -> IO a streamHTTP request manager k = withResponse request manager handleBody where handleBody response = do let br = responseBody response let scrubbed_resp = response { responseBody = () } runEffect (loop br >->> k scrubbed_resp >&> snd) loop br = do bs <- qlift (L.toStrict <$> brReadSome' br chunkSize) unless (S.null bs) $ do emit_ bs loop br -- Minimum chunk size to emit, set to some reasonable page size chunkSize = 4096 -- Stolen from the internals of http-client brReadSome' :: IO ByteString -> Int -> IO L.ByteString brReadSome' brRead' = loop id where loop front remainder | remainder <= 0 = return $ L.fromChunks $ front [] | otherwise = do bs <- brRead' if S.null bs then return $ L.fromChunks $ front [] else loop (front . (bs:)) (remainder - S.length bs) -- | Build a 'RequestBody' by chunked transfer encoding a 'Producer'. -- -- Each 'ByteString' produced will be sent as a seperate chunk. makeChunkedRequestBody :: Producer ByteString () IO () -> RequestBody makeChunkedRequestBody p = RequestBodyStreamChunked (givePopper p) -- | Build a 'RequestBody' by sending a Content-Length header and then -- streaming a 'Producer'. -- -- You should probably use 'makeChunkedRequestBody' if it is supported by the -- server. makeFixedRequestBody :: Int64 -> Producer ByteString () IO () -> RequestBody makeFixedRequestBody len p = RequestBodyStream len (givePopper p) -- | http-client is weird, it wants us to make poppers and give them to it. -- -- In summary, we take a Producer and turn it into a function which: given a -- continuation which takes an IO action that -- when called, will produce -- successive chunks -- returns an IO () and does some super strange things. givePopper :: Producer ByteString () IO () -> (IO ByteString -> IO ()) -> IO () givePopper producer k = do ref <- newIORef producer k (doRead ref) where doRead ref = do current_producer <- readIORef ref x <- qnext current_producer () case x of Left () -> do writeIORef ref (return ()) return S.empty Right (bs, next_producer) -> do writeIORef ref next_producer return bs -- | This is not a part of quiver because it only behaves how you might -- expect if you know that p is a Producer. There doesn't seem to be a good -- way of encoding this invariant in the types without nasty extensions. qnext p b' = loop p where loop (Consume _ _ q) = loop q loop (Produce b k' _) = return (Right (b, k' b')) loop (Deliver r) = return (Left r) loop (Enclose f) = f >>= loop