http2-client 0.8.0.0 → 0.8.0.1
raw patch · 4 files changed
+22/−476 lines, 4 filesdep −data-default-classdep −optparse-applicativedep ~asyncdep ~basedep ~bytestringPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies removed: data-default-class, optparse-applicative
Dependency ranges changed: async, base, bytestring, http2, time, tls
API changes (from Hackage documentation)
+ Network.HTTP2.Client: data ClientParams
+ Network.HTTP2.Client: data PortNumber
+ Network.HTTP2.Client: type HostName = String
- Network.HTTP2.Client: Http2Client :: (ByteString -> IO (IO (FrameHeader, FramePayload))) -> (SettingsList -> IO (IO (FrameHeader, FramePayload))) -> (ErrorCodeId -> ByteString -> IO ()) -> (forall a. StreamStarter a) -> IncomingFlowControl -> OutgoingFlowControl -> IO PayloadSplitter -> !Http2ClientAsyncs -> IO () -> Http2Client
+ Network.HTTP2.Client: Http2Client :: ByteString -> IO (IO (FrameHeader, FramePayload)) -> SettingsList -> IO (IO (FrameHeader, FramePayload)) -> ErrorCodeId -> ByteString -> IO () -> forall a. StreamStarter a -> IncomingFlowControl -> OutgoingFlowControl -> IO PayloadSplitter -> !Http2ClientAsyncs -> IO () -> Http2Client
- Network.HTTP2.Client: Http2Stream :: (HeaderList -> (FrameFlags -> FrameFlags) -> IO StreamThread) -> (Priority -> IO ()) -> (ErrorCodeId -> IO ()) -> IO StreamEvent -> ((FrameFlags -> FrameFlags) -> ByteString -> IO ()) -> (StreamId -> HeaderList -> PushPromiseHandler -> IO ()) -> Http2Stream
+ Network.HTTP2.Client: Http2Stream :: HeaderList -> (FrameFlags -> FrameFlags) -> IO StreamThread -> Priority -> IO () -> ErrorCodeId -> IO () -> IO StreamEvent -> (FrameFlags -> FrameFlags) -> ByteString -> IO () -> StreamId -> HeaderList -> PushPromiseHandler -> IO () -> Http2Stream
- Network.HTTP2.Client: IncomingFlowControl :: (WindowSize -> IO ()) -> (WindowSize -> IO Int) -> IO Bool -> IncomingFlowControl
+ Network.HTTP2.Client: IncomingFlowControl :: WindowSize -> IO () -> WindowSize -> IO Int -> IO Bool -> IncomingFlowControl
- Network.HTTP2.Client: OutgoingFlowControl :: (WindowSize -> IO ()) -> (WindowSize -> IO WindowSize) -> OutgoingFlowControl
+ Network.HTTP2.Client: OutgoingFlowControl :: WindowSize -> IO () -> WindowSize -> IO WindowSize -> OutgoingFlowControl
- Network.HTTP2.Client: StreamDefinition :: IO StreamThread -> (IncomingFlowControl -> OutgoingFlowControl -> IO a) -> StreamDefinition a
+ Network.HTTP2.Client: StreamDefinition :: IO StreamThread -> IncomingFlowControl -> OutgoingFlowControl -> IO a -> StreamDefinition a
- Network.HTTP2.Client.FrameConnection: Http2FrameClientStream :: (IO [(FrameFlags -> FrameFlags, FramePayload)] -> IO ()) -> StreamId -> Http2FrameClientStream
+ Network.HTTP2.Client.FrameConnection: Http2FrameClientStream :: IO [(FrameFlags -> FrameFlags, FramePayload)] -> IO () -> StreamId -> Http2FrameClientStream
- Network.HTTP2.Client.FrameConnection: Http2FrameConnection :: (StreamId -> Http2FrameClientStream) -> Http2ServerStream -> IO () -> Http2FrameConnection
+ Network.HTTP2.Client.FrameConnection: Http2FrameConnection :: StreamId -> Http2FrameClientStream -> Http2ServerStream -> IO () -> Http2FrameConnection
- Network.HTTP2.Client.RawConnection: RawHttp2Connection :: ([ByteString] -> IO ()) -> (Int -> IO ByteString) -> IO () -> RawHttp2Connection
+ Network.HTTP2.Client.RawConnection: RawHttp2Connection :: [ByteString] -> IO () -> Int -> IO ByteString -> IO () -> RawHttp2Connection
Files
- README.md +5/−5
- app/Main.hs +0/−314
- examples/SimpleGet.lhs +0/−127
- http2-client.cabal +17/−30
README.md view
@@ -57,12 +57,12 @@ Please see some literate Haskell documents in the `examples/` directory. For a more involved usage, we currently provide a command-line example client: `http2-client-exe` which I use as a test client and you could use to test-various flow-control parameters. Read `app/Main.hs` and-`src/Network/HTTP2/Client/Helpers.hs` to look at how a user application is-implemented.+various flow-control parameters. This binary lives in a separate package+at https://github.com/lucasdicioccio/http2-client-exe . -The Haddocks should have plenty implementation details, so please have a look.-Otherwise, you can ask help by creating an Issue on the bug-tracker.+The Haddocks, at https://hackage.haskell.org/package/http2-client, should have+plenty implementation details, so please have a look. Otherwise, you can ask+help by creating an Issue on the bug-tracker. ### Opening a stream
− app/Main.hs
@@ -1,314 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-module Main where--import Control.Concurrent (forkIO, myThreadId, throwTo, threadDelay)-import Control.Concurrent.Async (async, waitAnyCancel)-import Control.Monad (forever, when, void)-import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as ByteString-import Data.Default.Class (def)-import Data.Maybe (fromMaybe)-import Data.Monoid ((<>))-import Data.Time.Clock (diffUTCTime, getCurrentTime)-import qualified Network.HTTP2 as HTTP2-import qualified Network.TLS as TLS-import qualified Network.TLS.Extra.Cipher as TLS-import Options.Applicative-import System.IO--import Network.HTTP2.Client-import Network.HTTP2.Client.Helpers--type Path = ByteString-type Verb = ByteString--data PostData = PostBytestring !ByteString | PostFileContent FilePath- deriving Show--postDataForString :: String -> PostData-postDataForString ('@':filepath) = PostFileContent filepath-postDataForString content = PostBytestring (ByteString.pack content)--data ServerPushSwitch = PushEnabled | PushDisabled- deriving Show--data Verbosity = Verbose | NonVerbose- deriving Show--data UseTLS = UseTLS | PlainText- deriving Show--data QueryArgs = QueryArgs {- _host :: !HostName- , _port :: !PortNumber- , _verb :: !Verb- , _path :: !Path- , _extraHeaders :: ![(ByteString, ByteString)]- , _extraTrailers :: ![(ByteString, ByteString)]- , _postData :: !(Maybe PostData)- , _interPingDelay :: !Int- , _pingTimeout :: !Int- , _interFlowControlUpdates :: !Int- , _settingsMaxConcurrency :: !Int- , _settingsAllowServerPush :: !ServerPushSwitch- , _settingsMaxFrameSize :: !Int- , _settingsMaxHeaderBlockSize :: !Int- , _settingsInitialWindowSize :: !Int- , _initialWindowKick :: !Int- , _concurrentQueriesCount :: !Int- , _numberQueries :: !Int- , _finalDelay :: !Int- , _finalMessage :: !ByteString- , _encoderBufsize :: !Int- , _decoderBufsize :: !Int- , _downloadPrefix :: !FilePath- , _verboseDebug :: !Verbosity- , _useTLS :: !UseTLS- } deriving Show--clientArgs :: Parser QueryArgs-clientArgs =- QueryArgs- <$> host- <*> port- <*> verb- <*> path- <*> extraHeaders- <*> extraTrailers- <*> postData- <*> milliseconds "inter-ping-delay-ms" 0- <*> milliseconds "ping-timeout-ms" 5000- <*> milliseconds "inter-flow-control-updates-ms" 1000- <*> concurrency- <*> allowPush- <*> frameBytes- <*> headersBytes- <*> initialWindowBytes- <*> initialWindowKick- <*> numConcurrentThreads- <*> numQueriesPerThread- <*> milliseconds "delay-before-quitting-ms" 0- <*> kthxByeMessage- <*> encoderBufSize- <*> decoderBufSize- <*> downloadPrefix- <*> verboseDebug- <*> useTLS- where- bstrOption = fmap ByteString.pack . strOption- milliseconds what base = fmap (*1000) $ option auto (long what <> value base)- keyval kv = let (k,v1) = ByteString.break (== ':') kv in (k, ByteString.drop 1 v1)-- host = strOption (long "host" <> value "127.0.0.1")- path = bstrOption (long "path" <> value "/")- port = option auto (long "port" <> value 443)- verb = bstrOption (long "verb" <> value "GET")- extraHeaders = many (fmap keyval $ bstrOption (short 'H'))- extraTrailers = many (fmap keyval $ bstrOption (short 'T'))- postData = optional (fmap postDataForString (strOption (short 'd')))- concurrency = option auto (long "max-concurrency" <> value 100)- allowPush = flag PushEnabled PushDisabled (long "disable-server-push")- frameBytes = option auto (long "max-frame-size" <> value 1048576)- headersBytes = option auto (long "max-headers-list-size" <> value 1048576)- initialWindowBytes = option auto (long "initial-window-size" <> value 10485760)- initialWindowKick = option auto (long "initial-window-kick" <> value 0)- numConcurrentThreads = option auto (long "num-concurrent-threads" <> value 1)- numQueriesPerThread = option auto (long "num-queries-per-thread" <> value 1)- kthxByeMessage = bstrOption (long "exit-greeting" <> value "kthxbye (>;_;<)")- encoderBufSize = option auto (long "hpack-encoder-buffer-size" <> value 4096)- decoderBufSize = option auto (long "hpack-decoder-buffer-size" <> value 4096)- downloadPrefix = strOption (long "push-files-prefix" <> value ":stdout-pp")- verboseDebug = flag NonVerbose Verbose (long "verbose")- useTLS = flag UseTLS PlainText (long "plain-text")--main :: IO ()-main = execParser opts >>= client- where- opts = info (helper <*> clientArgs) (mconcat [- fullDesc- , header "http2-client-exe: a CLI HTTP2 client written in Haskell"- ])--client :: QueryArgs -> IO ()-client QueryArgs{..} = do- hSetBuffering stdout LineBuffering-- -- If we need to post data then we prepare an upload function.- -- Otherwise the function does nothing but we tell the server that we are- -- finished with the stream after HTTP headers.- --- -- Note that this implementation reads the body from files and into memory- -- (better for testing concurrency, worse for large uploads).- (headersFlags, dataPostFunction) <- case (_extraTrailers, _postData) of- ([], (Just (PostBytestring dataPayload))) ->- return $ (id, upload dataPayload HTTP2.setEndStream)- ([], (Just (PostFileContent filepath))) -> do- dataPayload <- ByteString.readFile filepath- return $ (id, upload dataPayload HTTP2.setEndStream)- ([], Nothing) ->- return $ (HTTP2.setEndStream, \_ _ _ _ -> return ())- (_, (Just (PostBytestring dataPayload))) ->- return $ (id, (\c ofc s sofc -> do- upload dataPayload id c ofc s sofc- trailers s _extraTrailers HTTP2.setEndStream- ))- (_, (Just (PostFileContent filepath))) -> do- dataPayload <- ByteString.readFile filepath- return $ (id, (\c ofc s sofc -> do- upload dataPayload id c ofc s sofc- trailers s _extraTrailers HTTP2.setEndStream- ))- (_, Nothing) ->- return $ (id, (\_ _ s _ -> do- trailers s _extraTrailers HTTP2.setEndStream- ))-- let headersPairs = [ (":method", _verb)- , (":scheme", "https")- , (":path", _path)- , (":authority", ByteString.pack _host)- ] <> _extraHeaders <> [("Trailer", ByteString.unwords $ fmap fst _extraTrailers)]--- let ppHandler n idx _ stream ppHdrs streamFlowControl _ = void $ forkIO $ do- let pushpath = fromMaybe "unspecified-path" (lookup ":path" ppHdrs)- timePrint ("push stream started" :: String, pushpath)- ret <- fromStreamResult <$> waitStream stream streamFlowControl (ppHandler n idx)- either (\e -> timePrint e) (dump PushPromiseFile pushpath n idx _downloadPrefix) ret- timePrint ("push stream ended" :: String)-- let conf = [ (HTTP2.SettingsMaxFrameSize, _settingsMaxFrameSize)- , (HTTP2.SettingsMaxConcurrentStreams, _settingsMaxConcurrency)- , (HTTP2.SettingsMaxHeaderBlockSize, _settingsMaxHeaderBlockSize)- , (HTTP2.SettingsInitialWindowSize, _settingsInitialWindowSize)- , (HTTP2.SettingsEnablePush, case _settingsAllowServerPush of- PushEnabled -> 1 ; PushDisabled -> 0)- ]- timePrint conf-- let tlsSetting = case _useTLS of- UseTLS ->- Just tlsParams- PlainText ->- Nothing- frameConn <- newHttp2FrameConnection _host _port tlsSetting- let wrappedFrameConn = frameConn {- _makeFrameClientStream = \sid ->- let frameClient = (_makeFrameClientStream frameConn) sid- in frameClient {- _sendFrames = \mkFrames -> do- xs <- mkFrames- print $ (">>> "::String, _getStreamId frameClient, map snd xs)- _sendFrames frameClient (pure xs)- }- , _serverStream =- let- currentServerStrean = _serverStream frameConn- in- currentServerStrean {- _nextHeaderAndFrame = do- hdrFrame@(hdr,_) <- _nextHeaderAndFrame currentServerStrean- print ("<<< "::String, HTTP2.streamId hdr, hdrFrame)- return hdrFrame- }- }- parentThread <- myThreadId- let withConn = case _verboseDebug of- Verbose ->- runHttp2Client wrappedFrameConn _encoderBufsize _decoderBufsize conf defaultGoAwayHandler printUnhandledFrame- NonVerbose ->- runHttp2Client frameConn _encoderBufsize _decoderBufsize conf (throwTo parentThread) ignoreFallbackHandler-- withConn $ \conn -> do- _addCredit (_incomingFlowControl conn) _initialWindowKick- _ <- forkIO $ forever $ do- updated <- _updateWindow $ _incomingFlowControl conn- when updated $ timePrint ("sending flow-control update" :: String)- threadDelay _interFlowControlUpdates-- _ <- forkIO $ when (_interPingDelay > 0) $ forever $ do- threadDelay _interPingDelay- (t0, t1, pingReply) <- ping conn _pingTimeout "pingpong"- timePrint $ ("ping-reply:" :: String, pingReply, diffUTCTime t1 t0)-- let go 0 idx = timePrint $ "done worker: " <> show idx- go n idx = do- _ <- (withHttp2Stream conn $ \stream ->- let initStream =- headers stream headersPairs headersFlags- handler streamINFlowControl streamOUTFlowControl = do- timePrint $ "stream started " <> show (idx, n)- _ <- async $ do- dataPostFunction conn- (_outgoingFlowControl conn)- stream- streamOUTFlowControl- ret <- fromStreamResult <$> waitStream stream streamINFlowControl (ppHandler n idx)- either (\e -> timePrint e) (dump MainFile _path n idx _downloadPrefix) ret- timePrint $ "stream ended " <> show (idx, n)- in StreamDefinition initStream handler)- go (n - 1) idx-- _ <- waitAnyCancel =<< traverse (async . go _numberQueries) [1 .. _concurrentQueriesCount]-- when (_finalDelay > 0) (threadDelay _finalDelay)--- _gtfo conn HTTP2.NoError _finalMessage- where- tlsParams = TLS.ClientParams {- TLS.clientWantSessionResume = Nothing- , TLS.clientUseMaxFragmentLength = Nothing- , TLS.clientServerIdentification = (_host, ByteString.pack $ show _port)- , TLS.clientUseServerNameIndication = True- , TLS.clientShared = def- , TLS.clientHooks = def { TLS.onServerCertificate = \_ _ _ _ -> return []- }- , TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_default }- , TLS.clientDebug = def- }--data DumpType = MainFile | PushPromiseFile--dump :: DumpType -> Path -> Int -> Int -> FilePath -> StreamResponse -> IO ()-dump MainFile _ _ _ ":none" (hdrs, _, trls) = do- timePrint hdrs- timePrint trls-dump MainFile _ _ _ ":stdout" (hdrs, body, trls) = do- timePrint hdrs- ByteString.putStrLn body- timePrint trls-dump PushPromiseFile _ _ _ ":stdout" (hdrs, _, trls) = do- timePrint hdrs- timePrint trls-dump MainFile _ _ _ ":stdout-pp" (hdrs, body, trls) = do- timePrint hdrs- ByteString.putStrLn body- timePrint trls-dump PushPromiseFile _ _ _ ":stdout-pp" (hdrs, body, trls) = do- timePrint hdrs- ByteString.putStrLn body- timePrint trls-dump _ querystring nquery nthread prefix (hdrs, body, trls) = do- timePrint hdrs- ByteString.writeFile filepath body- timePrint trls- where- filepath = mconcat [ prefix- , "/" - , show nquery- , "."- , show nthread- , ByteString.unpack $ cleanPath querystring- ]- cleanPath = ByteString.takeWhile (/= '?')- . ByteString.map (\c -> if c == '/' then '_' else c)--timePrint :: Show a => a -> IO ()-timePrint x = do- tst <- getCurrentTime- ByteString.hPutStrLn stderr $ ByteString.pack $ show (tst, x)--printUnhandledFrame :: FallBackFrameHandler-printUnhandledFrame (fh,fp) = timePrint ("UNHANDLED:"::String, fh, fp)
− examples/SimpleGet.lhs
@@ -1,127 +0,0 @@-First you need some imports and pragmas to make our life easier while typing-string literals.--> {-# LANGUAGE OverloadedStrings #-}--Main http2-client imports.--> import Network.HTTP2.Client-> import Network.HTTP2.Client.Helpers--Other imports, that we may eventually hide.--> import Network.HTTP2-> import Data.Default.Class (def)-> import Network.TLS as TLS-> import Network.TLS.Extra.Cipher as TLS-> -> main :: IO ()-> main = do--First, create a connection, you need to specify the host and port you want to-connect to. Unlike HTTP, HTTP2 proposes to use TLS, hence, the port should-almost always be 443 instead of 80.--> frameConn <- newHttp2FrameConnection "www.google.com" 443 (Just tlsParams )--The 'runHttp2Client' function takes control of the current thread so that we-never leave an internal thread leaking, even in presence of asynchronous-exceptions (likely in networked systems). Hence, the body of the logic we want-to perform is passed as a callback to 'runHttp2Client'. This callback is the-last argument for synctactic reason.--We also need to give more arguments to 'runHttp2Client'. The 8k values-corresponds to header sizes for sending/receiving. If you expect to receive or-send queries with more than 8MB of sent/received buffers, then you should tune-these values. The settings at the list gives the server a warrant to send more-bytes as soon as the server is ready to send queries (cf. HTTP2 flow control).--> runHttp2Client frameConn 8192 8192 [(SettingsInitialWindowSize,10000000)] defaultGoAwayHandler ignoreFallbackHandler $ \conn -> do--We need to update the flow-control window for a snappy experience, so add-credit to the whole connection (else the SettingsInitialWindowSize would be-useless). Then, we immediately transfer credit to the server using-_updateWindow. We don't expect to receive more data for this connection so-we're good to go, but in a long-running application we would have to query this-function a number of time.--> let fc = _incomingFlowControl conn-> _addCredit fc 10000000-> _ <- _updateWindow fc--Prepare the HTTP request using, first meta-headers, then normal headers.--> let requestHeaders = [ (":method", "GET")-> , (":scheme", "https")-> , (":path", "?q=http2")-> , (":authority", "www.google.com")-> ]--Start a new stream. A new stream opened by a client takes two steps: first we-initialize the stream then we wait for a reply (i.e., we handle the reply). If-you were sending an HTTP POST you would be sending data from the client to the-server, which requires outbound flow-control too.--> _ <- withHttp2Stream conn $ \stream ->-> let--For a simple HTTP GET, the headers constitute the whole query. We don't plan to-send more HTTP headers in continuation frames, hence we tell the server that-the headers are sent (so that the server can start generating a response). We-also tell the server that we're not gonna send more data with the setEndStream-flag.--> initStream = headers stream requestHeaders (setEndHeader . setEndStream)--The handler uses the high-level waitStream, which blocks and waits for the-whole query result. The fromStreamResult coalesces every single frames from the-response (each of which may fail) as a single tuple with headers and the body-reply, wrapped in a single error.--If you expected the server to send you some data you can use onPushPromise with-a handler with the same type and 'waitStream' mechanism as the-client-originated streams (however with its own flow-control context).--> resetPushPromises _ pps _ _ _ = _rst pps RefusedStream->-> handler sfc _ = do-> waitStream stream sfc resetPushPromises >>= print . fromStreamResult--We've defined an initializer and a handler so far.--> in --We need to package the initializer and the handler in a StreamDefinition-object. Once you're done, the _startStream function will manage your stream-synchronously. If you wanted to run multiple concurrent queries then you should-have one async per stream, and pay attention to the return value of-_startStream.--> StreamDefinition initStream handler--We are done, congratz!--> putStrLn "done"-> _goaway conn NoError "https://github.com/lucasdicioccio/http2-client example"--You need to pass some TLS parameters. Here are a settings that could work with-an SSL verification that accepts any server certificate.--> tlsParams :: ClientParams-> tlsParams = TLS.ClientParams {-> TLS.clientWantSessionResume = Nothing-> , TLS.clientUseMaxFragmentLength = Nothing-> , TLS.clientServerIdentification = ("127.0.0.1", "")-> , TLS.clientUseServerNameIndication = True-> , TLS.clientShared = def-> , TLS.clientHooks = def { TLS.onServerCertificate = \_ _ _ _ -> return [] }-> , TLS.clientSupported = def { TLS.supportedCiphers = TLS.ciphersuite_default }-> , TLS.clientDebug = def-> }---An example output is as below:--$ http2-client-example-Right ([(":status","302"),("location","https://www.google.fr/?q=http2&gws_rd=cr&ei=7mOXWY6IMsSya_f2gsAJ"),("cache-control","private"),("content-type","text/html; charset=UTF-8"),("p3p","CP=\"This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info.\""),("date","Fri, 18 Aug 2017 22:02:22 GMT"),("server","gws"),("content-length","269"),("x-xss-protection","1; mode=block"),("x-frame-options","SAMEORIGIN"),("set-cookie","NID=110=KvJPXSY6bwoZ1nJZe7LkRLu8GfSQIis1BCmh0u6agjDqyu4Ny8iAbmtZJSUXRen47uBjVmumFF8itvEDqQkVWTK3rKS1SAviGtzcoFj4Qel7FH5FpB2hvijgN1JFEbNM; expires=Sat, 17-Feb-2018 22:02:22 GMT; path=/; domain=.google.com; HttpOnly"),("alt-svc","quic=\":443\"; ma=2592000; v=\"39,38,37,35\"")],"<HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">\n<TITLE>302 Moved</TITLE></HEAD><BODY>\n<H1>302 Moved</H1>\nThe document has moved\n<A HREF=\"https://www.google.fr/?q=http2&gws_rd=cr&ei=7mOXWY6IMsSya_f2gsAJ\">here</A>.\r\n</BODY></HTML>\r\n")-done
http2-client.cabal view
@@ -1,5 +1,5 @@ name: http2-client-version: 0.8.0.0+version: 0.8.0.1 synopsis: A native HTTP2 client library. description: Please read the README.md at the homepage. homepage: https://github.com/lucasdicioccio/http2-client@@ -33,35 +33,22 @@ , tls >= 1.4 && < 2 default-language: Haskell2010 -executable http2-client-exe- hs-source-dirs: app- main-is: Main.hs- ghc-options: -threaded -rtsopts -with-rtsopts=-N -O2- build-depends: base- , async- , bytestring- , data-default-class- , http2- , http2-client- , optparse-applicative- , time- , tls- default-language: Haskell2010--executable http2-client-example-simple-get- hs-source-dirs: examples- main-is: SimpleGet.lhs- ghc-options: -threaded -rtsopts -with-rtsopts=-N -O2- build-depends: base- , async- , bytestring- , data-default-class- , http2- , http2-client- , optparse-applicative- , time- , tls- default-language: Haskell2010+-- Commented-out to avoid distribution+--+--executable http2-client-example-simple-get+-- hs-source-dirs: examples+-- main-is: SimpleGet.lhs+-- ghc-options: -threaded -rtsopts -with-rtsopts=-N -O2+-- build-depends: base+-- , async+-- , bytestring+-- , data-default-class+-- , http2+-- , http2-client+-- , optparse-applicative+-- , time+-- , tls+-- default-language: Haskell2010 test-suite http2-client-test type: exitcode-stdio-1.0