packages feed

http-streams 0.3.1.0 → 0.4.0.0

raw patch · 8 files changed

+249/−134 lines, 8 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

+ Network.Http.Client: makeConnection :: ByteString -> IO () -> OutputStream ByteString -> InputStream ByteString -> IO Connection
- Network.Http.Client: buildRequest :: Connection -> RequestBuilder α -> IO Request
+ Network.Http.Client: buildRequest :: RequestBuilder α -> IO Request
- Network.Http.Client: getHostname :: Request -> ByteString
+ Network.Http.Client: getHostname :: Connection -> Request -> ByteString
- Network.Http.Client: setHostname :: ByteString -> RequestBuilder ()
+ Network.Http.Client: setHostname :: Hostname -> Port -> RequestBuilder ()

Files

http-streams.cabal view
@@ -1,6 +1,6 @@ cabal-version:       >= 1.10 name:                http-streams-version:             0.3.1.0+version:             0.4.0.0 synopsis:            An HTTP client using io-streams description:  /Overview/
src/Network/Http/Client.hs view
@@ -40,7 +40,7 @@ \ main = do \     c <- 'openConnection' \"www.example.com\" 80 -\     q <- 'buildRequest' c $ do+\     q <- 'buildRequest' $ do \         'http' GET \"\/\" \         'setAccept' \"text/html\" @@ -159,7 +159,10 @@     openConnectionSSL,     baselineContextSSL,     modifyContextSSL,-    establishConnection+    establishConnection,++    -- * Testing support+    makeConnection ) where  import Network.Http.Connection
src/Network/Http/Connection.hs view
@@ -23,6 +23,7 @@     openConnection,     openConnectionSSL,     closeConnection,+    getHostname,     sendRequest,     receiveResponse,     emptyBody,@@ -68,34 +69,53 @@             -- ^ will be used as the Host: header in the HTTP request.         cClose :: IO (),             -- ^ called when the connection should be closed.-        cOut   :: OutputStream ByteString,+        cOut   :: OutputStream Builder,         cIn    :: InputStream ByteString     }  instance Show Connection where     show c =    {-# SCC "Connection.show" #-}         concat-           ["Connection {",-            "cHost = \"",+           ["Host: ",              S.unpack $ cHost c,-             "\"}"]+             "\n"]   ----- | Creates a raw Connection object from the given parts.+-- | Create a raw Connection object from the given parts. This is+-- primarily of use when teseting, for example: --+-- > fakeConnection :: IO Connection+-- > fakeConnection = do+-- >     o  <- Streams.nullOutput+-- >     i  <- Streams.nullInput+-- >     c  <- makeConnection "www.example.com" (return()) o i+-- >     return c+--+-- is an idiom we use frequently in testing and benchmarking, usually+-- replacing the InputStream with something like:+--+-- >     x' <- S.readFile "properly-formatted-response.txt"+-- >     i  <- Streams.fromByteString x'+--+-- If you're going to do that, keep in mind that you /must/ have CR-LF+-- pairs after each header line and between the header and body to+-- be compliant with the HTTP protocol; otherwise, parsers will+-- reject your message.+-- makeConnection     :: ByteString-    -- ^ will be used as the Host: header in the HTTP request.+    -- ^ will be used as the @Host:@ header in the HTTP request.     -> IO ()-    -- ^ called when the connection is terminated.+    -- ^ an action to be called when the connection is terminated.     -> OutputStream ByteString-    -- ^ write end of the HTTP client connection.+    -- ^ write end of the HTTP client-server connection.     -> InputStream ByteString-    -- ^ read end of the client connection.+    -- ^ read end of the HTTP client-server connection.     -> IO Connection-makeConnection h c o i =-    return $! Connection h c o i+makeConnection h c o1 i = do+    o2 <- Streams.builderStream o1+    return $! Connection h c o2 i   --@@ -104,7 +124,7 @@ -- @Connection@ afterwards. -- -- >     x <- withConnection (openConnection "s3.example.com" 80) $ (\c -> do--- >         q <- buildRequest c $ do+-- >         q <- buildRequest $ do -- >             http GET "/bucket42/object/149" -- >         sendRequest c q emptyBody -- >         ...@@ -156,11 +176,14 @@     s <- socket (addrFamily addr) Stream defaultProtocol      connect s a-    (i,o) <- Streams.socketToStreams s+    (i,o1) <- Streams.socketToStreams s++    o2 <- Streams.builderStream o1+     return Connection {         cHost  = h',         cClose = close s,-        cOut   = o,+        cOut   = o2,         cIn    = i     }   where@@ -212,11 +235,14 @@     ssl <- SSL.connection ctx s     SSL.connect ssl -    (i,o) <- Streams.sslToStreams ssl+    (i,o1) <- Streams.sslToStreams ssl++    o2 <- Streams.builderStream o1+     return Connection {         cHost  = h',         cClose = closeSSL s ssl,-        cOut   = o,+        cOut   = o2,         cIn    = i     }   where@@ -253,8 +279,6 @@ -} sendRequest :: Connection -> Request -> (OutputStream Builder -> IO α) -> IO α sendRequest c q handler = do-    o2 <- Streams.builderStream o1-     -- write the headers      Streams.write (Just msg) o2@@ -306,15 +330,29 @@     return x    where-    o1 = cOut c+    o2 = cOut c     e = qBody q     t = qExpect q-    msg = composeRequestBytes q+    msg = composeRequestBytes q h'+    h' = cHost c     i = cIn c     rsp p = Builder.toByteString $ composeResponseBytes p   --+-- | Get the virtual hostname that will be used as the @Host:@ header in+-- the HTTP 1.1 request. Per RFC 2616 § 14.23, this will be of the form+-- @hostname:port@ if the port number is other than the default, ie 80+-- for HTTP.+--+getHostname :: Connection -> Request -> ByteString+getHostname c q =+    case qHost q of+        Just h' -> h'+        Nothing -> cHost c+++-- -- | Handle the response coming back from the server. This function -- hands control to a handler function you supply, passing you the -- 'Response' object with the response headers and an 'InputStream'@@ -422,7 +460,7 @@ -- -- >     c <- openConnection "kernel.operationaldynamics.com" 58080 -- >--- >     q <- buildRequest c $ do+-- >     q <- buildRequest $ do -- >         http GET "/time" -- > -- >     sendRequest c q emptyBody@@ -445,7 +483,7 @@ -- debugHandler :: Response -> InputStream ByteString -> IO () debugHandler p i = do-    putStr $ show p+    S.putStr $ S.filter (/= '\r') $ Builder.toByteString $ composeResponseBytes p     Streams.connect i stdout  
src/Network/Http/Inconvenience.hs view
@@ -155,7 +155,7 @@ -- >     let url = "https://www.example.com/photo.jpg" -- > -- >     c <- establishConnection url--- >     q <- buildRequest c $ do+-- >     q <- buildRequest $ do -- >         http GET url -- >     ... --@@ -237,8 +237,17 @@  ------------------------------------------------------------------------------ +{-+    Account for bug where "http://www.example.com" is parsed with no+    path element, resulting in an illegal HTTP request line.+-}+ path :: URI -> ByteString-path u = T.encodeUtf8 $! T.pack+path u = case url of+            ""  -> "/"+            _   -> url+  where+    url = T.encodeUtf8 $! T.pack                       $! concat [uriPath u, uriQuery u, uriFragment u]  @@ -282,7 +291,7 @@     u = parseURL r'      process c = do-        q <- buildRequest c $ do+        q <- buildRequest $ do             http GET (path u)             setAccept "*/*" @@ -348,7 +357,7 @@     u = parseURL r'      process c = do-        q <- buildRequest c $ do+        q <- buildRequest $ do             http POST (path u)             setAccept "*/*"             setContentType t@@ -385,7 +394,7 @@     u = parseURL r'      process c = do-        q <- buildRequest c $ do+        q <- buildRequest $ do             http POST (path u)             setAccept "*/*"             setContentType "application/x-www-form-urlencoded"@@ -455,7 +464,7 @@     u = parseURL r'      process c = do-        q <- buildRequest c $ do+        q <- buildRequest $ do             http PUT (path u)             setAccept "*/*"             setHeader "Content-Type" t
src/Network/Http/RequestBuilder.hs view
@@ -30,7 +30,8 @@ import Blaze.ByteString.Builder (Builder) import qualified Blaze.ByteString.Builder as Builder (fromByteString,                                                       toByteString)-import qualified Blaze.ByteString.Builder.Char8 as Builder (fromShow)+import qualified Blaze.ByteString.Builder.Char8 as Builder (fromShow,+                                                            fromString) import Control.Monad.State import Data.ByteString (ByteString) import qualified Data.ByteString.Base64 as BS64@@ -53,20 +54,27 @@ -- | Run a RequestBuilder, yielding a Request object you can use on the -- given connection. ----- >     q <- buildRequest c $ do+-- >     q <- buildRequest $ do -- >         http POST "/api/v1/messages" -- >         setContentType "application/json"+-- >         setHostname "clue.example.com" 80 -- >         setAccept "text/html" -- >         setHeader "X-WhoDoneIt" "The Butler" -- -- Obviously it's up to you to later actually /send/ JSON data. ---buildRequest :: Connection -> RequestBuilder α -> IO Request-buildRequest c mm = do+-- /Note/+--+-- The API of this function changed from version 0.3.1 to verison 0.4.0;+-- the original requirement to pass a Connection object has been removed,+-- thereby allowing you to build your Request before opening the+-- connection to the web server.+--+buildRequest :: RequestBuilder α -> IO Request+buildRequest mm = do     let (RequestBuilder s) = (mm)-    let h = cHost c     let q = Request {-        qHost = h,+        qHost = Nothing,         qMethod = GET,         qPath = "/",         qBody = Empty,@@ -83,7 +91,7 @@ http m p' = do     q <- get     let h0 = qHeaders q-    let h1 = updateHeader h0 "User-Agent" "http-streams/0.3.1.0"+    let h1 = updateHeader h0 "User-Agent" "http-streams/0.4.0.0"     let h2 = updateHeader h1 "Accept-Encoding" "gzip"      let e  = case m of@@ -109,12 +117,20 @@ -- header in HTTP 1.1 and is set directly from the name of the server -- you connected to when calling 'Network.Http.Connection.openConnection'. ---setHostname :: ByteString -> RequestBuilder ()-setHostname v' = do+setHostname :: Hostname -> Port -> RequestBuilder ()+setHostname h p = do     q <- get     put q {-        qHost = v'+        qHost = Just v'     }+  where+    v' :: ByteString+    v' = if p == 80+        then S.pack h+        else Builder.toByteString $ mconcat+           [Builder.fromString h,+            ":",+            Builder.fromShow p]  -- -- | Set a generic header to be sent in the HTTP request. The other@@ -199,7 +215,7 @@ -- >         setAuthorizationBasic "Aladdin" "open sesame" -- -- will result in an @Authorization:@ header value of--- @Basic: QWxhZGRpbjpvcGVuIHNlc2FtZQ==@.+-- @Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==@. -- -- Basic authentication does /not/ use a message digest function to -- encipher the password; the above string is only base-64 encoded and@@ -210,7 +226,7 @@ -- validate requests. Keep in mind in these cases the secret is still -- sent to the servers on the other side and passes in clear through -- all layers after the SSL termination. Do /not/ use basic--- authentication to protect secure or user-originaed privacy-sensitve+-- authentication to protect secure or user-originated privacy-sensitve -- information. -- {-
src/Network/Http/ResponseParser.hs view
@@ -62,22 +62,44 @@      hs <- many parseHeader -    let hp = buildHeaders hs-     _ <- crlf +    let h  = buildHeaders hs+    let te = case lookupHeader h "Transfer-Encoding" of+            Just x' -> if mk x' == "chunked"+                        then Chunked+                        else None+            Nothing -> None++    let ce = case lookupHeader h "Content-Encoding" of+            Just x' -> if mk x' == "gzip"+                        then Gzip+                        else Identity+            Nothing -> Identity++    let n  = case lookupHeader h "Content-Length" of+            Just x' -> readDecimal x' :: Int+            Nothing -> 0+     return Response {         pStatusCode = sc,         pStatusMsg = sm,-        pHeaders = hp+        pTransferEncoding = te,+        pContentEncoding = ce,+        pContentLength = n,+        pHeaders = h     }+  where  + parseStatusLine :: Parser (Int,ByteString) parseStatusLine = do-    sc <- string "HTTP/1.1 " *> decimal <* char ' '+    sc <- string "HTTP/1." *> satisfy version *> char ' ' *> decimal <* char ' '     sm <- takeTill (== '\r') <* crlf     return (sc,sm)+  where+    version c = c == '1' || c == '0'  {-     Needs to be expanded to accept multi-line headers.@@ -111,36 +133,20 @@ readResponseBody :: Response -> InputStream ByteString -> IO (InputStream ByteString) readResponseBody p i1 = do -    i2 <- case encoding of+    i2 <- case t of         None        -> readFixedLengthBody i1 n         Chunked     -> readChunkedBody i1 -    i3 <- case compression of+    i3 <- case c of         Identity    -> return i2         Gzip        -> readCompressedBody i2-        Deflate     -> throwIO (UnexpectedCompression $ show compression)+        Deflate     -> throwIO (UnexpectedCompression $ show c)      return i3   where--    encoding = case header "Transfer-Encoding" of-        Just x'-> if mk x' == "chunked"-                    then Chunked-                    else None-        Nothing -> None--    compression = case header "Content-Encoding" of-        Just x'-> if mk x' == "gzip"-                    then Gzip-                    else Identity-        Nothing -> Identity--    header = getHeader p--    n = case header "Content-Length" of-        Just x' -> readDecimal x' :: Int-        Nothing -> 0-+    t = pTransferEncoding p+    c = pContentEncoding p+    n = pContentLength p  readDecimal :: (Enum a, Num a, Bits a) => ByteString -> a readDecimal = S.foldl' f 0@@ -152,12 +158,6 @@     digitToInt c | c >= '0' && c <= '9' = toEnum $! ord c - ord '0'                  | otherwise = error $ "'" ++ [c] ++ "' is not an ascii digit" {-# INLINE readDecimal #-}---data TransferEncoding = None | Chunked--data ContentEncoding = Identity | Gzip | Deflate-    deriving (Show)  data UnexpectedCompression = UnexpectedCompression String         deriving (Typeable, Show)
src/Network/Http/Types.hs view
@@ -9,6 +9,7 @@ -- the BSD licence. -- +{-# LANGUAGE BangPatterns      #-} {-# LANGUAGE OverloadedStrings #-} {-# OPTIONS -fno-warn-orphans #-} @@ -16,9 +17,10 @@     Request(..),     EntityBody(..),     ExpectMode(..),-    getHostname,     Response(..),     StatusCode,+    TransferEncoding(..),+    ContentEncoding(..),     getStatusCode,     getStatusMessage,     getHeader,@@ -49,6 +51,7 @@ import Data.CaseInsensitive (CI, mk, original) import Data.HashMap.Strict (HashMap, delete, empty, foldrWithKey, insert,                             lookup)+import Data.List (foldl') import Data.Monoid (mconcat, mempty) import Data.String (IsString, fromString) @@ -100,19 +103,24 @@ -- line and headers (as it will be sent over the wire but with the @\\r@ -- characters stripped) which can be handy for debugging. --+-- Note that the @Host:@ header is not set until+-- 'Network.Http.Connection.sendRequest' is called, so you will not see+-- it in the Show instance (unless you call 'setHostname' to override+-- the value inherited from the @Connection@).+-- data Request     = Request {-        qMethod  :: Method,-        qHost    :: ByteString,-        qPath    :: ByteString,-        qBody    :: EntityBody,-        qExpect  :: ExpectMode,-        qHeaders :: Headers+        qMethod  :: !Method,+        qHost    ::  Maybe ByteString,+        qPath    :: !ByteString,+        qBody    :: !EntityBody,+        qExpect  :: !ExpectMode,+        qHeaders :: !Headers     }  instance Show Request where     show q = {-# SCC "Request.show" #-}-        S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ composeRequestBytes q+        S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ composeRequestBytes q "<default>"   data EntityBody = Empty | Chunking | Static Int@@ -132,8 +140,8 @@     with removing them. -} -composeRequestBytes :: Request -> Builder-composeRequestBytes q =+composeRequestBytes :: Request -> ByteString -> Builder+composeRequestBytes q h' =     mconcat        [requestline,         hostLine,@@ -147,24 +155,29 @@         " ",         version,         "\r\n"]-    method = Builder.fromString $ show $ qMethod q+    method = case qMethod q of+        GET     -> "GET"+        HEAD    -> "HEAD"+        POST    -> "POST"+        PUT     -> "PUT"+        DELETE  -> "DELETE"+        TRACE   -> "TRACE"+        OPTIONS -> "OPTIONS"+        CONNECT -> "CONNECT"+        PATCH   -> "PATCH"+        (Method x) -> Builder.fromByteString x+     uri = Builder.copyByteString $ qPath q     version = "HTTP/1.1"      hostLine = mconcat ["Host: ", hostname, "\r\n"]-    hostname = Builder.copyByteString $ qHost q+    hostname = case qHost q of+        Just x' -> Builder.copyByteString x'+        Nothing -> Builder.copyByteString h'      headerFields = joinHeaders $ unWrap $ qHeaders q  ------ | Get the virtual hostname that will be used as the @Host:@ header in--- the HTTP 1.1 request. Per RFC 2616 § 14.23, this will be of the form--- @hostname:port@ if the port number is other than the default, ie 80--- for HTTP.----getHostname :: Request -> ByteString-getHostname q = qHost q  type StatusCode = Int @@ -180,15 +193,25 @@ -- data Response     = Response {-        pStatusCode :: StatusCode,-        pStatusMsg  :: ByteString,-        pHeaders    :: Headers+        pStatusCode       :: !StatusCode,+        pStatusMsg        :: !ByteString,+        pTransferEncoding :: !TransferEncoding,+        pContentEncoding  :: !ContentEncoding,+        pContentLength    :: !Int,+        pHeaders          :: !Headers     }  instance Show Response where     show p =     {-# SCC "Response.show" #-}         S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ composeResponseBytes p ++data TransferEncoding = None | Chunked++data ContentEncoding = Identity | Gzip | Deflate+    deriving (Show)++ -- -- | Get the HTTP response status code. --@@ -250,6 +273,7 @@  instance IsString Builder where     fromString x = Builder.fromString x+    {-# INLINE fromString #-}  -- -- | The map of headers in a 'Request' or 'Response'. Note that HTTP@@ -298,15 +322,15 @@ updateHeader x k v =     Wrap result   where-    result = insert (mk k) v m-    m = unWrap x+    !result = insert (mk k) v m+    !m = unWrap x  removeHeader :: Headers -> ByteString -> Headers removeHeader x k =     Wrap result   where-    result = delete (mk k) m-    m = unWrap x+    !result = delete (mk k) m+    !m = unWrap x   {-@@ -319,18 +343,18 @@ buildHeaders hs =     Wrap result   where-    result = foldr addHeader empty hs+    result = foldl' addHeader empty hs  addHeader-    :: (ByteString,ByteString)-    -> HashMap (CI ByteString) ByteString+    :: HashMap (CI ByteString) ByteString+    -> (ByteString,ByteString)     -> HashMap (CI ByteString) ByteString-addHeader (k,v) m =+addHeader m (k,v) =     insert (mk k) v m  lookupHeader :: Headers -> ByteString -> Maybe ByteString lookupHeader x k =     lookup (mk k) m   where-    m = unWrap x+    !m = unWrap x 
tests/Check.hs view
@@ -24,7 +24,7 @@ import Network.URI (parseURI) import OpenSSL (withOpenSSL) import Test.Hspec (Spec, describe, hspec, it)-import Test.Hspec.Expectations (shouldThrow, Selector, anyException)+import Test.Hspec.Expectations (Selector, anyException, shouldThrow) import Test.HUnit  --@@ -71,6 +71,7 @@      describe "Parsing responses" $ do         testResponseParser1+        testResponseParserMismatch         testChunkedEncoding         testContentLength         testCompressedResponse@@ -83,6 +84,7 @@         testPostChunks         testPostWithForm         testGetRedirects+        testGetFormatsRequest         testExcessiveRedirects         testGeneralHandler         testEstablishConnection@@ -91,11 +93,11 @@ testRequestTermination =     it "terminates with a blank line" $ do         c <- openConnection "127.0.0.1" localPort-        q <- buildRequest c $ do+        q <- buildRequest $ do             http GET "/time"             setAccept "text/plain" -        let e' = Builder.toByteString $ composeRequestBytes q+        let e' = Builder.toByteString $ composeRequestBytes q "booga"         let n = S.length e' - 4         let (a',b') = S.splitAt n e' @@ -110,10 +112,10 @@         (fakeConnection)         (return)         (\c -> do-            q <- buildRequest c $ do+            q <- buildRequest $ do                 http GET "/time" -            let e' = Builder.toByteString $ composeRequestBytes q+            let e' = Builder.toByteString $ composeRequestBytes q (cHost c)             let l' = S.takeWhile (/= '\r') e'              assertEqual "Invalid HTTP request line" "GET /time HTTP/1.1" l')@@ -123,18 +125,13 @@ fakeConnection = do     i <- Streams.nullInput     o <- Streams.nullOutput-    return $ Connection {-        cHost  = "www.example.com",-        cClose = return (),-        cIn    = i,-        cOut   = o-    }+    c <- makeConnection "www.example.com" (return ()) o i+    return c   testAcceptHeaderFormat =     it "properly formats Accept header" $ do-        c <- fakeConnection-        q <- buildRequest c $ do+        q <- buildRequest $ do             setAccept' [("text/html", 1),("*/*", 0.0)]          let h = qHeaders q@@ -143,8 +140,7 @@  testBasicAuthorizatonHeader =     it "properly formats Authorization header" $ do-        c <- fakeConnection-        q <- buildRequest c $ do+        q <- buildRequest $ do             setAuthorizationBasic "Aladdin" "open sesame"          let h = qHeaders q@@ -176,33 +172,56 @@ -} testEnsureHostField =     it "has a properly formatted Host header" $ do-        c <- fakeConnection-        q1 <- buildRequest c $ do+        q1 <- buildRequest $ do             http GET "/hello.txt"          let h1 = qHost q1-        assertEqual "Incorrect Host header" "www.example.com" h1+        assertEqual "Incorrect Host header" Nothing h1 -        q2 <- buildRequest c $ do+        q2 <- buildRequest $ do             http GET "/hello.txt"-            setHostname "other.example.com"+            setHostname "other.example.com" 80          let h2 = qHost q2-        assertEqual "Incorrect Host header" "other.example.com" h2+        assertEqual "Incorrect Host header" (Just "other.example.com") h2 +        q3 <- buildRequest $ do+            http GET "/hello.txt"+            setHostname "other.example.com" 54321 +        let h3 = qHost q3+        assertEqual "Incorrect Host header" (Just "other.example.com:54321") h3++ testResponseParser1 =     it "parses a simple 200 response" $ do         b' <- S.readFile "tests/example1.txt"-        parseTest parseResponse b'+        let re = parseOnly parseResponse b'+        let p = case re of+                    Left str    -> error str+                    Right x     -> x++        assertEqual "Incorrect parse of response" 200 (getStatusCode p)         return () +testResponseParserMismatch =+    it "parses response when HTTP version doesn't match" $ do+        b' <- S.readFile "tests/example3.txt"+        let re = parseOnly parseResponse b'+        let p = case re of+                    Left str    -> error str+                    Right x     -> x +        assertEqual "Incorrect parse of response" 200 (getStatusCode p)+        return ()+++ testChunkedEncoding =     it "recognizes chunked transfer encoding and decodes" $ do         c <- openConnection "127.0.0.1" localPort -        q <- buildRequest c $ do+        q <- buildRequest $ do             http GET "/time"          sendRequest c q emptyBody@@ -221,7 +240,7 @@     it "recognzies fixed length message" $ do         c <- openConnection "127.0.0.1" localPort -        q <- buildRequest c $ do+        q <- buildRequest $ do             http GET "/static/statler.jpg"          sendRequest c q emptyBody@@ -252,7 +271,7 @@     it "recognizes gzip content encoding and decompresses" $ do         c <- openConnection "127.0.0.1" localPort -        q <- buildRequest c $ do+        q <- buildRequest $ do             http GET "/static/hello.html"             setHeader "Accept-Encoding" "gzip" @@ -284,7 +303,7 @@     it "sends expectation and handles 100 response" $ do         c <- openConnection "127.0.0.1" localPort -        q <- buildRequest c $ do+        q <- buildRequest $ do             http PUT "/resource/x149"             setExpectContinue @@ -388,7 +407,13 @@             len <- getCount             assertEqual "Incorrect number of bytes read" 29 len +testGetFormatsRequest =+    it "GET includes a properly formatted request path" $ do+        let url = S.concat ["http://", localhost ]+        x' <- get "http://localhost" concatHandler' +        assertBool "Incorrect context path" (S.length x' > 0)+ testExcessiveRedirects =     it "too many redirects result in an exception" $ do         let url = S.concat ["http://", localhost, "/loop"]@@ -435,7 +460,7 @@         let url = S.concat ["http://", localhost, "/static/statler.jpg"]          x' <- withConnection (establishConnection url) $ (\c -> do-            q <- buildRequest c $ do+            q <- buildRequest $ do                 http GET "/static/statler.jpg"                     -- TODO be nice if we could replace that with 'url';                     -- fix the routeRequests function in TestServer maybe?