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 +1/−1
- src/Network/Http/Client.hs +5/−2
- src/Network/Http/Connection.hs +60/−22
- src/Network/Http/Inconvenience.hs +15/−6
- src/Network/Http/RequestBuilder.hs +28/−12
- src/Network/Http/ResponseParser.hs +32/−32
- src/Network/Http/Types.hs +56/−32
- tests/Check.hs +52/−27
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?