http-exchange (empty) → 0.1.1.0
raw patch · 8 files changed
+644/−0 lines, 8 filesdep +basedep +byteslicedep +bytesmith
Dependencies added: base, byteslice, bytesmith, bytestring, http-exchange, http-interchange, primitive, tasty, tasty-hunit, text
Files
- CHANGELOG.md +5/−0
- LICENSE +30/−0
- http-exchange.cabal +65/−0
- src-testdep/OkChannel.hs +76/−0
- src-types/Http/Exchange/Types.hs +35/−0
- src/Channel.hsig +49/−0
- src/Exchange.hs +253/−0
- test/Main.hs +131/−0
+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for http-exchange++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2023, Andrew Martin++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Andrew Martin nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ http-exchange.cabal view
@@ -0,0 +1,65 @@+cabal-version: 3.0+name: http-exchange+version: 0.1.1.0+synopsis: Perform HTTP Requests+description: Perform HTTP requests. This uses backpack and is agnostic to the backend.+license: BSD-3-Clause+license-file: LICENSE+author: Andrew Martin+maintainer: andrew.thaddeus@gmail.com+copyright: 2023 Andrew Martin+category: Data+build-type: Simple+extra-doc-files: CHANGELOG.md++library types+ ghc-options: -Wall+ exposed-modules: Http.Exchange.Types+ build-depends: base >=4.16.3.0 && <5+ hs-source-dirs: src-types+ default-language: GHC2021++library testdep+ ghc-options: -Wall+ exposed-modules: OkChannel+ build-depends:+ , base >=4.16.3.0 && <5+ , byteslice >=0.2.11+ , types+ hs-source-dirs: src-testdep+ default-language: GHC2021++library+ signatures: Channel+ ghc-options: -Wall+ exposed-modules: Exchange+ build-depends:+ , base >=4.16.3.0 && <5+ , http-interchange >=0.3.1+ , text >= 2.0+ , types+ , primitive >=0.8+ , byteslice >=0.2.11+ , bytesmith >=0.3.9+ hs-source-dirs: src+ default-language: GHC2021++test-suite test+ ghc-options: -Wall+ default-language: GHC2021+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Main.hs+ build-depends:+ , base >=4.16.3.0 && <5+ , http-interchange >=0.3.1+ , http-exchange+ , testdep+ , tasty >=1.4.3+ , tasty-hunit >=0.10.0.3+ , byteslice+ , bytestring >=0.11+ , primitive >=0.8+ mixins:+ http-exchange (Exchange as OkExchange)+ requires (Channel as OkChannel)
+ src-testdep/OkChannel.hs view
@@ -0,0 +1,76 @@+{-# language DeriveFunctor #-}+{-# language DerivingStrategies #-}+{-# language KindSignatures #-}++module OkChannel+ ( M(..)+ , ReceiveException(..)+ , SendException+ , showsPrecReceiveException+ , showsPrecSendException+ , Resource+ , send+ , receive+ ) where++import Data.Bytes (Bytes)+import Data.Bytes.Chunks (Chunks(ChunksNil,ChunksCons))+import Data.Void (Void,absurd)++import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Chunks as Chunks++type Resource = ()++data ReceiveException = ExpectedMoreInput+ deriving (Show)++showsPrecReceiveException :: Int -> ReceiveException -> String -> String+showsPrecReceiveException = showsPrec++type SendException = Void++showsPrecSendException :: Int -> SendException -> String -> String+showsPrecSendException _ x _ = absurd x++-- First arg is input, second arg is output+-- The input is peeled off one byte sequence at a time by receive+-- We use this feature to feed input byte-by-byte to test streaming+-- features.+data M a = M (Chunks -> Bytes -> (Chunks,Bytes,a))+ deriving stock (Functor)++bindM :: M a -> (a -> M b) -> M b+bindM (M f) g = M $ \inbound0 outbound0 ->+ case f inbound0 outbound0 of+ (inbound1,outbound1,a) ->+ case g a of+ M h -> h inbound1 outbound1++pureM :: a -> M a+pureM a = M $ \x y -> (x,y,a)++instance Applicative M where+ pure = pureM+ f <*> a = f `bindM` \f' -> a `bindM` \a' -> pureM (f' a')++instance Monad M where+ (>>=) = bindM++send ::+ ()+ -> Chunks+ -> M (Either SendException ())+send _ b = M $ \inbound outbound ->+ (inbound,outbound <> Chunks.concat b,Right ())++receive ::+ ()+ -> M (Either ReceiveException Bytes)+receive _ = M $ \inbound0 outbound ->+ let go inbound = case inbound of+ ChunksNil -> (inbound,outbound,Left ExpectedMoreInput)+ ChunksCons b ch -> case Bytes.null b of+ True -> go ch+ False -> (ch,outbound,Right b)+ in go inbound0
+ src-types/Http/Exchange/Types.hs view
@@ -0,0 +1,35 @@+{-# language DeriveAnyClass #-}+{-# language DerivingStrategies #-}++module Http.Exchange.Types+ ( HttpException(..)+ ) where++import qualified Control.Exception as E++-- | Exceptions that occur when decoding an HTTP response.+-- If this happens, the only way to proceed is to+-- shut down the connection. Either the server does not+-- speak HTTP correct, or there is a mistake in this libary.+data HttpException+ = ChunkTooLarge+ | ChunkedBodyEndOfInput+ | NonchunkedBodyEndOfInput+ | ContentLengthDuplicated+ | ContentLengthMalformed+ | ContentLengthTooLarge+ | ExpectedCrlfAfterChunk+ | ExpectedCrlfAfterChunkLength+ | ExpectedCrlfBeforeChunkLength+ | HeadersMalformed+ | HeadersEndOfInput+ | HeadersTooLarge+ | ImplementationMistake+ -- ^ If this one happens, there is a mistake in this+ -- library.+ | NonNumericChunkLength+ | PipelinedResponses+ | TransferEncodingUnrecognized+ | TransferEncodingDuplicated+ deriving stock (Show)+ deriving anyclass (E.Exception)
+ src/Channel.hsig view
@@ -0,0 +1,49 @@+{-# language KindSignatures #-}++signature Channel where++import Data.Kind (Type)+import Data.Bytes.Chunks (Chunks)+import Data.Bytes (Bytes)++-- | A resource that, in the monadic context @M@, can be used to send+-- and receive data. Typically instantiated to a network socket type.+data Resource :: Type++-- | Exceptions that can occur while sending data.+data SendException :: Type++showsPrecSendException ::+ Int -> SendException -> String -> String++-- | Exceptions that can occur while receiving data.+data ReceiveException :: Type++showsPrecReceiveException ::+ Int -> ReceiveException -> String -> String++-- | The monadic context in which an HTTP exchange takes place. This+-- is always instantiated with 'IO' when building a real HTTP client,+-- but the test suite uses a different non-IO context that is fully+-- deterministic.+data M :: Type -> Type+instance Functor M+instance Applicative M+instance Monad M++-- | Send bytes.+send ::+ Resource+ -> Chunks+ -> M (Either SendException ())++-- | Receive a chunk of input. The caller does not specify a+-- size hint. This helps make the test suite easier to write, but+-- we should probably figure out a way to support size hints as well.+--+-- Postcondition: If the returned chunked is empty, the source has+-- shut down. The indefinite module @Exchange@ interprets a zero-length+-- chunk to mean that the source has reached the end of its input.+receive ::+ Resource+ -> M (Either ReceiveException Bytes)
+ src/Exchange.hs view
@@ -0,0 +1,253 @@+{-# language DeriveAnyClass #-}+{-# language DerivingStrategies #-}+{-# language DuplicateRecordFields #-}+{-# language LambdaCase #-}+{-# language OverloadedStrings #-}++module Exchange+ ( Exception(..)+ , HttpException(..)+ , exchange+ ) where++import Channel (M,Resource,SendException,ReceiveException,send,receive)+import Control.Monad (when)+import Data.Char (ord)+import Data.Bytes (Bytes)+import Data.Bytes.Chunks (Chunks(ChunksCons,ChunksNil))+import Data.Word (Word64)+import Http.Bodied (Bodied(Bodied))+import Http.Exchange.Types (HttpException)+import Http.Header (Header(Header))+import Http.Types (Request,Response,Headers,LookupException(Duplicate,Missing))+import Text.Read (readMaybe)+import Data.Bytes.Parser (Parser)++import qualified Data.Bytes.Parser as Parser+import qualified Data.Bytes.Parser.Latin as Latin+import qualified Http.Exchange.Types as E+import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Chunks as Chunks+import qualified Data.Text as T+import qualified Http.Header+import qualified Http.Headers as Headers+import qualified Http.Request as Request+import qualified Http.Response as Response+import qualified Http.Bodied+import qualified Control.Exception+import qualified Channel++data Continuation = Continuation+ !Instruction+ !Chunks -- these chunks are in reverse order++-- Not exported+data Instruction+ = More -- we are in the middle of a chunk+ !Int -- how much input was requested (zero is special)+ !Int -- how much more input do we need to consume+ | MorePostCr+ !Int -- how much input was requested for the last chunk+ | ChunkLength+ -- We are in the middle (or at the beginning) of chunk length,+ -- the leading CRLF has already been consumed+ !Word64 -- chunk length accumulator+ | PostCr -- We already got the CR after the chunk length+ !Int -- how much input we need to consume, but we need to consume the LF first+ | Done+ -- ^ We got all the chunks, and we got the zero-length chunk+ -- at the end, and we got the trailing CRLF. We are done.++data TransferEncoding+ = Nonchunked+ | Chunked++-- | An exception that occurs during an HTTP exchange.+data Exception+ = Http -- ^ The response was not a valid HTTP response+ !HttpException+ | Send+ -- ^ Transport exception while sending. When backed by stream sockets,+ -- exceptions like @ECONNRESET@ show up here.+ !SendException+ | Receive+ -- ^ Transport exception while receiving. Depending on the backend,+ -- this may or may not include an end-of-input exception. For stream+ -- sockets, end-of-input is not presented as an exception. It is+ -- presented as a zero-length result.+ !ReceiveException+ deriving anyclass (Control.Exception.Exception)++instance Show Exception where+ showsPrec d (Http e) = showParen (d > 10)+ (showString "Http " . showsPrec 11 e)+ showsPrec d (Send e) = showParen (d > 10)+ (showString "Send " . Channel.showsPrecSendException 11 e)+ showsPrec d (Receive e) = showParen (d > 10)+ (showString "Receive " . Channel.showsPrecReceiveException 11 e)++-- | Send an HTTP request and await a response. This function takes+-- responsibility for encoding the request and decoding the response.+-- It deals with the @Transfer-Encoding@ of the response and supports+-- both chunked and nonchunked responses.+exchange ::+ Resource+ -> Bodied Request -- http request line and headers+ -> M (Either Exception (Bodied Response))+exchange ctx req = do+ let enc = Request.bodiedToChunks req+ send ctx enc >>= \case+ Left err -> pure (Left (Send err))+ Right () -> receiveResponse ctx++receiveResponse ::+ Resource+ -> M (Either Exception (Bodied Response))+receiveResponse !ctx = do+ let go !oldOutput = receive ctx >>= \case+ Left err -> pure (Left (Receive err))+ Right newOutput -> case Bytes.length newOutput of+ 0 -> pure (Left (Http E.HeadersEndOfInput))+ _ -> do+ let output = oldOutput <> newOutput+ case splitEndOfHeaders output of+ Nothing -> if Bytes.length output > 16000+ then pure (Left (Http E.HeadersTooLarge))+ else go output+ Just (pre,post) -> case Response.decode 128 pre of+ Nothing -> pure (Left (Http E.HeadersMalformed))+ Just resp@Response.Response{headers} -> case lookupTransferEncoding headers of+ Left err -> pure (Left (Http err))+ Right enc -> case enc of+ Nonchunked -> handleNonchunkedBody ctx resp post headers+ Chunked -> handleChunkedBody ctx resp post+ go mempty++handleChunkedBody ::+ Resource+ -> Response+ -> Bytes+ -> M (Either Exception (Bodied Response))+handleChunkedBody !ctx resp !input0 = do+ let go contA !inputA = case Parser.parseBytes (parserChunked contA) inputA of+ Parser.Failure e -> pure (Left (Http e))+ Parser.Success (Parser.Slice _ leftoverLen contB) -> case leftoverLen of+ -- We expect that parserChunked consumes all input, so we check+ -- here to be certain that it actually does.+ 0 -> case contB of+ Continuation Done revChunks -> pure $ Right $ Bodied+ { metadata = resp+ , body = Chunks.reverse revChunks+ }+ _ -> receive ctx >>= \case+ Right inputB -> case Bytes.length inputB of+ 0 -> pure (Left (Http E.ChunkedBodyEndOfInput))+ _ -> go contB inputB+ Left err -> pure (Left (Receive err))+ _ -> pure (Left (Http E.ImplementationMistake))+ let cont0 = Continuation (ChunkLength 0) ChunksNil+ go cont0 input0++parserChunked :: Continuation -> Parser HttpException s Continuation+parserChunked (Continuation instr chunks0) = case instr of+ Done -> Parser.fail E.ImplementationMistake+ More total n -> parserChunkedMore total n chunks0+ MorePostCr total -> parserChunkedMorePostCr total chunks0+ ChunkLength acc -> parserChunkedChunkLength acc chunks0+ PostCr n -> parserChunkedChunkLengthPostCr n chunks0++parserChunkedMore :: Int -> Int -> Chunks -> Parser HttpException s Continuation+parserChunkedMore !total !n !chunks0 = case n of+ -- If there are no more bytes left in the chunk, we start+ -- on the next decimal-encoded chunk length.+ 0 -> parserChunkedMorePost total chunks0+ _ -> do+ b <- Parser.takeUpTo n+ case Bytes.length b of+ -- If there was no input left, we return to request more input.+ -- If we didn't check for this, we would go into a loop.+ 0 -> pure (Continuation (More total n) chunks0)+ m -> do+ let chunks1 = ChunksCons b chunks0+ parserChunkedMore total (n - m) chunks1++parserChunkedMorePost :: Int -> Chunks -> Parser HttpException s Continuation+parserChunkedMorePost !total !chunks0 = Latin.opt >>= \case+ Just '\r' -> parserChunkedMorePostCr total chunks0+ Just _ -> Parser.fail E.ExpectedCrlfAfterChunk+ Nothing -> pure (Continuation (More total 0) chunks0)++parserChunkedMorePostCr :: Int -> Chunks -> Parser HttpException s Continuation+parserChunkedMorePostCr !total !chunks0 = Latin.opt >>= \case+ Just '\n' -> case total of+ 0 -> pure (Continuation Done chunks0)+ _ -> parserChunkedChunkLength 0 chunks0+ Just _ -> Parser.fail E.ExpectedCrlfAfterChunk+ Nothing -> pure (Continuation (MorePostCr total) chunks0)++parserChunkedChunkLength :: Word64 -> Chunks -> Parser HttpException s Continuation+parserChunkedChunkLength !acc !chunks0 = if acc > 100_000_000+ then Parser.fail E.ChunkTooLarge+ else Latin.opt >>= \case+ Nothing -> pure (Continuation (ChunkLength acc) chunks0)+ Just c -> case c of+ '\r' -> Latin.opt >>= \case+ Just d -> case d of+ '\n' -> do+ let !acc' = fromIntegral acc :: Int+ parserChunkedMore acc' acc' chunks0+ _ -> Parser.fail E.ExpectedCrlfAfterChunkLength+ Nothing -> pure (Continuation (PostCr (fromIntegral acc)) chunks0)+ _ | c >= '0', c <= '9' -> parserChunkedChunkLength (acc * 16 + fromIntegral (ord c - 0x30)) chunks0+ _ | c >= 'a', c <= 'f' -> parserChunkedChunkLength (acc * 16 + fromIntegral (ord c - (0x61 - 10))) chunks0+ _ | c >= 'A', c <= 'F' -> parserChunkedChunkLength (acc * 16 + fromIntegral (ord c - (0x41 - 10))) chunks0+ _ -> Parser.fail E.NonNumericChunkLength++parserChunkedChunkLengthPostCr :: Int -> Chunks -> Parser HttpException s Continuation+parserChunkedChunkLengthPostCr !n !chunks0 = Latin.opt >>= \case+ Just d -> case d of+ '\n' -> parserChunkedMore n n chunks0+ _ -> Parser.fail E.ExpectedCrlfAfterChunkLength+ Nothing -> pure (Continuation (PostCr n) chunks0)++handleNonchunkedBody :: Resource -> Response -> Bytes -> Headers -> M (Either Exception (Bodied Response))+handleNonchunkedBody ctx resp !post !headers = case lookupContentLength headers of+ Left err -> pure (Left (Http err))+ Right len -> do+ let finish reversedChunks n = case compare n 0 of+ LT -> pure (Left (Http E.PipelinedResponses))+ EQ -> pure $ Right $ Bodied+ { metadata = resp+ , body = Chunks.reverse reversedChunks+ }+ GT -> receive ctx >>= \case+ Right chunk -> case Bytes.length chunk of+ 0 -> pure (Left (Http E.NonchunkedBodyEndOfInput))+ _ -> finish (ChunksCons chunk reversedChunks) (n - Bytes.length chunk)+ Left err -> pure (Left (Receive err))+ finish (ChunksCons post ChunksNil) (len - Bytes.length post)++splitEndOfHeaders :: Bytes -> Maybe (Bytes, Bytes)+splitEndOfHeaders !b = case Bytes.findTetragramIndex 0x0D 0x0A 0x0D 0x0A b of+ Nothing -> Nothing+ Just n -> Just (Bytes.unsafeTake (n + 4) b, Bytes.unsafeDrop (n + 4) b)++lookupTransferEncoding :: Headers -> Either HttpException TransferEncoding+lookupTransferEncoding !hdrs =+ case Headers.lookupTransferEncoding hdrs of+ Right Header{value} -> case value of+ "chunked" -> Right Chunked+ _ -> Left E.TransferEncodingUnrecognized+ Left Missing -> Right Nonchunked+ Left Duplicate -> Left E.TransferEncodingDuplicated++lookupContentLength :: Headers -> Either HttpException Int+lookupContentLength !hdrs =+ case Headers.lookupContentLength hdrs of+ Left Missing -> Right 0+ Left Duplicate -> Left E.ContentLengthDuplicated+ Right Header{value} -> case readMaybe (T.unpack value) of+ Nothing -> Left E.ContentLengthMalformed+ Just i -> do+ when (i > 8_000_000_000) (Left E.ContentLengthTooLarge)+ Right i
+ test/Main.hs view
@@ -0,0 +1,131 @@+{-# language DuplicateRecordFields #-}+{-# language OverloadedStrings #-}++import Test.Tasty (TestTree,testGroup,defaultMain)+import Test.Tasty.HUnit (testCase,(@=?))+import Data.Bytes (Bytes)+import Http.Request (Request(Request),RequestLine(RequestLine))+import Http.Types (Response, Bodied(Bodied), Header(Header))+import OkChannel (M(M))+import Data.Bytes.Chunks (Chunks(ChunksCons,ChunksNil))++import qualified Data.Bytes as Bytes+import qualified Data.Bytes.Chunks as Chunks+import qualified GHC.Exts as Exts+import qualified OkExchange as E+import qualified Http.Header+import qualified Http.Headers as Headers+import qualified Http.Bodied+import qualified Http.Request as Request+import qualified Data.Bytes.Text.Ascii as Ascii++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests = testGroup "tests"+ [ testCase "get-a" $ do+ (input,output,Bodied{body}) <- evaluateM (E.exchange () getReqA) (Chunks.fromBytes getRespA)+ body @=? ChunksNil+ input @=? mempty+ output @=? Chunks.concat (Request.bodiedToChunks getReqA)+ , testCase "get-chunked-a" $ do+ (input,output,Bodied{body}) <- evaluateM (E.exchange () getReqA) (Chunks.fromBytes getRespChunkedA)+ body @=? ChunksNil+ input @=? mempty+ output @=? Chunks.concat (Request.bodiedToChunks getReqA)+ , testCase "get-chunked-byte-by-byte-a" $ do+ (input,output,Bodied{body}) <- evaluateM (E.exchange () getReqA) (bytesToSingleByteChunks getRespChunkedA)+ body @=? ChunksNil+ input @=? mempty+ output @=? Chunks.concat (Request.bodiedToChunks getReqA)+ , testCase "get-body-a" $ do+ (input,output,Bodied{body}) <- evaluateM (E.exchange () getReqA) (Chunks.fromBytes getRespBodyA)+ input @=? mempty+ body @=? ChunksCons (Ascii.fromString "helloworld") ChunksNil+ output @=? Chunks.concat (Request.bodiedToChunks getReqA)+ , testCase "get-chunked-b" $ do+ (input,output,Bodied{body}) <- evaluateM (E.exchange () getReqA) (Chunks.fromBytes getRespChunkedB)+ Ascii.fromString "hello to my friends." @=? Chunks.concat body+ mempty @=? input+ Chunks.concat (Request.bodiedToChunks getReqA) @=? output+ , testCase "get-chunked-byte-by-byte-b" $ do+ (input,output,Bodied{body}) <- evaluateM (E.exchange () getReqA) (bytesToSingleByteChunks getRespChunkedB)+ Ascii.fromString "hello to my friends." @=? Chunks.concat body+ mempty @=? input+ Chunks.concat (Request.bodiedToChunks getReqA) @=? output+ , testCase "get-chunked-two-by-two-b" $ do+ (input,output,Bodied{body}) <- evaluateM (E.exchange () getReqA) (bytesToDoubletonByteChunks getRespChunkedB)+ Ascii.fromString "hello to my friends." @=? Chunks.concat body+ mempty @=? input+ Chunks.concat (Request.bodiedToChunks getReqA) @=? output+ ]++bytesToSingleByteChunks :: Bytes -> Chunks+bytesToSingleByteChunks = Bytes.foldr'+ (\w acc -> ChunksCons (Bytes.singleton w) acc+ ) ChunksNil++bytesToDoubletonByteChunks :: Bytes -> Chunks+bytesToDoubletonByteChunks b0 = go (Exts.toList b0)+ where+ go (x : y : zs) = ChunksCons (Exts.fromList [x,y]) (go zs)+ go [x] = ChunksCons (Bytes.singleton x) ChunksNil+ go [] = ChunksNil++evaluateM ::+ M (Either E.Exception (Bodied Response))+ -> Chunks -- prebuilt response+ -> IO (Chunks,Bytes,Bodied Response)+evaluateM (M f) resp = case f resp mempty of+ (input,output,r) -> case r of+ Left e -> case e of+ E.Http err -> fail ("exchange http protocol failure: " ++ show err)+ E.Receive err -> fail ("exchange http transport receive failure: " ++ show err)+ Right b -> pure (input,output,b)++getReqA :: Bodied Request+getReqA = Bodied+ { metadata = Request+ { requestLine=RequestLine+ { method = "GET"+ , path = "/health"+ }+ , headers = Headers.fromArray $ Exts.fromList+ [ Header{name="Host",value="example.com"}+ ]+ }+ , body = mempty+ }++getRespA :: Bytes+getRespA = Ascii.fromString+ "HTTP/1.1 200 OK\r\n\+ \Server: testsuite/1.2.3\r\n\+ \Content-Type: text/plain\r\n\r\n"++getRespBodyA :: Bytes+getRespBodyA = Ascii.fromString+ "HTTP/1.1 200 OK\r\n\+ \Server: testsuite/1.2.3\r\n\+ \Content-Type: text/plain\r\n\+ \Content-Length: 10\r\n\r\n\+ \helloworld"++getRespChunkedA :: Bytes+getRespChunkedA = Ascii.fromString+ "HTTP/1.1 200 OK\r\n\+ \Server: testsuite/1.2.3\r\n\+ \Transfer-Encoding: chunked\r\n\+ \Content-Type: text/plain\r\n\r\n\+ \0\r\n\r\n"++getRespChunkedB :: Bytes+getRespChunkedB = Ascii.fromString+ "HTTP/1.1 200 OK\r\n\+ \Server: testsuite/1.2.3\r\n\+ \Transfer-Encoding: chunked\r\n\+ \Content-Type: text/plain\r\n\r\n\+ \5\r\nhello\r\n\+ \f\r\n to my friends.\r\n\+ \0\r\n\r\n"