packages feed

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 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"