hack2-handler-mongrel2-http (empty) → 2011.6.22
raw patch · 11 files changed
+764/−0 lines, 11 filesdep +aesondep +airdep +attoparsecsetup-changed
Dependencies added: aeson, air, attoparsec, base, blaze-builder, blaze-textual, bytestring, case-insensitive, containers, data-default, directory, enumerator, hack2, mtl, network, safe, text, unix, zeromq-haskell
Files
- LICENSE +31/−0
- Nemesis +57/−0
- Setup.lhs +4/−0
- changelog.md +0/−0
- hack2-handler-mongrel2-http.cabal +46/−0
- readme.md +11/−0
- src/Hack2/Handler/Mongrel2.hs +115/−0
- src/Hack2/Handler/Mongrel2/Parser.hs +91/−0
- src/Hack2/Handler/Mongrel2/Response.hs +50/−0
- src/Hack2/Handler/Mongrel2/Types.hs +53/−0
- src/Hack2/Handler/Mongrel2HTTP.hs +306/−0
+ LICENSE view
@@ -0,0 +1,31 @@+Copyright (c) 2011, Jinjing Wang++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 Jinjing Wang 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.
+ Nemesis view
@@ -0,0 +1,57 @@+nemesis = do+ + clean+ [ "**/*.hi"+ , "**/*.o"+ , "manifest"+ , "main"+ , "nemesis-tmp.*"+ , "Test"+ ]+ ++ desc "prepare cabal dist"+ task "dist" - do+ sh "cabal clean"+ sh "cabal configure"+ sh "cabal sdist"+++ desc "put all .hs files in manifest"+ task "manifest" - do+ sh "find . | grep 'hs-' > manifest"+++ desc "start console"+ task "i" (sh "ghci -isrc test/hello.hs")+ + desc "run main"+ task "run" (sh "runghc -isrc src/Hack2/Handler/Mongrel2HTTP.hs")+ ++ desc "test hello"+ task "hello" - do+ sh "runghc test/hello.hs"++ desc "test ab"+ task "ab" - do+ sh "runghc -isrc test/ab.hs"++ + desc "show sloc"+ task "stat" - do+ sh "cloc -match-f=hs- --quiet src"+ + desc "load mongrel config from config/test.conf"+ task "load" - do+ sh "m2sh load -config config/test.conf"+ + desc "list mongrel2 servers"+ task "list" - do+ sh "m2sh servers"+ + desc "start mongrel2 server"+ task "start" - do+ sh "m2sh start -name test"+ +
+ Setup.lhs view
@@ -0,0 +1,4 @@+#! /usr/bin/env runhaskell++> import Distribution.Simple+> main = defaultMain
+ changelog.md view
+ hack2-handler-mongrel2-http.cabal view
@@ -0,0 +1,46 @@+Name: hack2-handler-mongrel2-http+Version: 2011.6.22+Build-type: Simple+Synopsis: Hack2 Mongrel2 HTTP handler+Description: Hack2 Mongrel2 HTTP handler+License: BSD3+License-file: LICENSE+Author: Jinjing Wang+Maintainer: Jinjing Wang <nfjinjing@gmail.com>+Build-Depends: base+Cabal-version: >= 1.2+category: Web+homepage: https://github.com/nfjinjing/hack2-handler-mongrel2-http+data-files: readme.md, changelog.md, Nemesis++library++ build-depends: + base >= 4.0 && < 5+ , network+ , bytestring+ , data-default >= 0.2+ , hack2 >= 2011.6.20+ , containers+ , mtl+ , enumerator < 5+ , blaze-builder < 0.4+ , case-insensitive < 0.3+ , air+ , zeromq-haskell+ , directory+ , safe+ , text+ , aeson+ , blaze-textual+ , unix+ , attoparsec+ hs-source-dirs: src/+ exposed-modules: + Hack2.Handler.Mongrel2HTTP+ other-modules:+ Hack2.Handler.Mongrel2+ Hack2.Handler.Mongrel2.Parser+ Hack2.Handler.Mongrel2.Response+ Hack2.Handler.Mongrel2.Types+
+ readme.md view
@@ -0,0 +1,11 @@+Serving Hack2 application with the great Mongrel2 server+---------------------------------------------------------++* enumerator enabled, constant memory consumption when enumerator all the way down++Know Issue+----------++* Server run loop is naive ><+* default start 10 instances, which might not be the right way to run mongrel2 apps+* Very early stage in development
+ src/Hack2/Handler/Mongrel2.hs view
@@ -0,0 +1,115 @@+-- adopted from Bardur Arantsson's MIT mongrel2-handler+++{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE OverloadedStrings #-}++++module Hack2.Handler.Mongrel2+ ( + mkHandler+ , withConnectedHandler+ , receiveRequest+ , sendResponse+ -- Re-exported+ , module Hack2.Handler.Mongrel2.Types+ ) where++import Blaze.ByteString.Builder (Builder,fromByteString)+import Data.Attoparsec+import Hack2.Handler.Mongrel2.Parser (messageParser)+import Hack2.Handler.Mongrel2.Response+import Hack2.Handler.Mongrel2.Types+import qualified System.ZMQ as ZMQ+import Data.ByteString.Char8 (ByteString, unpack)+import Control.Monad (forM_)+import qualified Data.ByteString.Char8 as B++import Data.Enumerator+import Air.Env+import Prelude ()+++-- | Create a new handler. @mkHandler pullFromAddress publishToAddress id@+-- creates a handler which pulls requests from @fromAddress@ and publishes+-- replies to @publishAddress@.+mkHandler :: String -> String -> Maybe String -> Handler+mkHandler = Handler++-- | Run an IO action with a connected handler.+withConnectedHandler :: Handler -> (ConnectedHandler -> IO a) -> IO a+withConnectedHandler h io =+ ZMQ.withContext 1 - \c ->+ ZMQ.withSocket c ZMQ.Pull - \s -> do+ ZMQ.withSocket c ZMQ.Pub - \ps -> do+ -- Connect to the poll socket.+ ZMQ.connect s - handlerPullFrom h+ -- Bind to publish socket.+ ZMQ.connect ps - handlerPublishTo h+ case handlerId h of+ Nothing -> return ()+ Just uuid -> ZMQ.setOption ps - ZMQ.Identity uuid+ -- Run the action with the connected handler.+ io - ConnectedHandler s ps++-- | Receive a parsed request from the Mongrel2 server. Blocks until+-- a message is received.+receiveRequest :: ConnectedHandler -> IO Request+receiveRequest handler = do+ msg <- ZMQ.receive (chPullSocket handler) []+ + -- putStrLn - "handler received: " + unpack msg+ + -- Parse into a structured message.+ case parseOnly messageParser msg of+ Left errMsg ->+ -- Invalid message. This can only happen if there's+ -- an error in our parsing code or Mongrel2 itself.+ fail - "Couldn't parse message: " + (show errMsg)+ Right m -> do+ -- Return the parsed message.+ return m++sendResponse :: ConnectedHandler -> Response -> IO ()+sendResponse handler response = + run_ - _responseEnum $$ _send_iteratee+ + where+ uuid = responseUuid response+ clientId = responseClientId response+ _responseEnum = responseBody response+ _mk_response msg = mkResponse uuid clientId msg+ + _socket = chPublishSocket handler++ _send msg = ZMQ.send _socket (_mk_response msg) [] -- [ZMQ.SndMore]+ _done = ZMQ.send _socket (_mk_response "") []+ + -- head :: Monad m => Iteratee B.ByteString m (Maybe Word8)+ -- head = continue loop where+ -- loop (Chunks xs) = case BL.uncons (BL.fromChunks xs) of+ -- Just (char, extra) -> yield (Just char) (toChunks extra)+ -- Nothing -> head+ -- loop EOF = yield Nothing EOF+ -- + + _send_iteratee :: Iteratee ByteString IO ()+ _send_iteratee = continue loop where+ loop (Chunks xs) = do+ io - xs.mapM_ (\msg -> do+ -- puts - "send msg: " + msg.unpack+ _send msg+ )+ _send_iteratee+ + loop EOF = do+ -- io - puts - "done"+ io - _done+ yield () EOF+ + -- let combined = B.append _responseHeader _responseBody+ -- putStrLn - "message: " + unpack combined+ + -- ZMQ.send _socket combined []+
+ src/Hack2/Handler/Mongrel2/Parser.hs view
@@ -0,0 +1,91 @@+module Hack2.Handler.Mongrel2.Parser+ ( + messageParser+ ) where++import Data.Aeson (Value(..), json)+import Data.Attoparsec+import Data.Attoparsec.Char8 (decimal)+import Data.ByteString (ByteString)+import Data.Text (Text)+import Data.Text.Encoding (encodeUtf8)+import Data.Word (Word8)+import Prelude hiding (take)+import qualified Data.Map as M+import Hack2.Handler.Mongrel2.Types++++-- Predicates for parsing.+isSpace :: Word8 -> Bool+isSpace 0x20 = True+isSpace _ = False++isColon :: Word8 -> Bool+isColon 0x3a = True+isColon _ = False++isComma :: Word8 -> Bool+isComma 0x2c = True+isComma _ = False++-- Skip spaces.+skipSpace :: Parser ()+skipSpace = skipWhile isSpace++-- Parse a UUID.+uuidParser :: Parser ByteString+uuidParser = takeWhile1 $ \w -> ((w >= 65) && (w <= 90 )) -- A-Z+ || ((w >= 97) && (w <= 122)) -- a-z+ || ((w >= 48) && (w <= 57 )) -- 0-9+ || (w == 45) -- Dash+ -- || (w == 95) -- _+ + ++netstringParser :: (Int -> Parser a) -> Parser a+netstringParser contentParser = do+ len <- decimal+ skip isColon+ str <- contentParser len+ return str++-- mongrel_send 2 / 238:{"PATH":"/","x-forwarded-for":"127.0.0.1","accept":"*/*","user-agent":"curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3","host":"127.0.0.1:6767","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/","PATTERN":"/"},0:,++-- def parse(msg):+-- sender, conn_id, path, rest = msg.split(' ', 3)+-- headers, rest = tnetstrings.parse(rest)+-- body, _ = tnetstrings.parse(rest)+-- +-- if type(headers) is str:+-- headers = json.loads(headers)+-- +-- return Request(sender, conn_id, path, headers, body)+ +messageParser :: Parser Request+messageParser = do+ uuid <- uuidParser+ skipSpace+ clientId <- decimal+ skipSpace+ path <- takeTill isSpace+ skipSpace+ reqHdr <- netstringParser $ \_ -> json+ skip isComma+ reqBody <- netstringParser take+ + return $ Request + {+ requestUuid = uuid+ , requestClientId = clientId+ , requestPath = path+ , requestHeaders = reqHdr+ , requestBody = reqBody+ }++-- extractQuery :: Value -> ([Text], Query)+-- extractQuery (Object hdrs) =+-- case M.lookup "URI" hdrs of+-- Just (String uriText) -> decodePath $ encodeUtf8 uriText+-- _ -> fail "Missing/invalid 'URI' in headers received from Mongrel2"+-- extractQuery _ = fail "Invalid headers received from Mongrel2"
+ src/Hack2/Handler/Mongrel2/Response.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE OverloadedStrings #-}++module Hack2.Handler.Mongrel2.Response+ ( Response+ , mkResponse+ ) where++import Blaze.ByteString.Builder (Builder, toByteString, fromByteString)+import Blaze.Text.Int (integral)+import Data.ByteString (ByteString)+import Data.ByteString.Char8 ()+import Data.List (intersperse)+import Data.Monoid+import Hack2.Handler.Mongrel2.Types+import qualified Data.ByteString as S+++-- | Build a single space.+buildSpace :: Builder+buildSpace = fromByteString " "++-- | Build a UUID.+buildUUID :: UUID -> Builder+buildUUID = fromByteString+++ -- def send(self, uuid, conn_id, msg):+ -- """+ -- Raw send to the given connection ID at the given uuid, mostly used + -- internally.+ -- """+ -- header = "%s %d:%s," % (uuid, len(str(conn_id)), str(conn_id))+ -- self.resp.send(header + ' ' + msg)+ + -- bcc1453e-9cc0-11e0-af11-6cf049b16ec3 1:0, HTTP/1.1 200 OK+ +-- | Build a response from a server UUID, a list of+-- client IDs and a response body builder.+mkResponse :: UUID -> ClientID -> ByteString -> ByteString+mkResponse uuid clientID body =+ toByteString $ mconcat [ buildUUID uuid+ , buildSpace+ , integral $ S.length $ toByteString $ integral clientID+ , fromByteString ":"+ , integral clientID+ , fromByteString ","+ , buildSpace+ , fromByteString body+ ]+
+ src/Hack2/Handler/Mongrel2/Types.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE Rank2Types #-}++module Hack2.Handler.Mongrel2.Types where++import Data.ByteString (ByteString)+import Data.Int (Int64)+import Data.Aeson (Value(..))+import Data.Enumerator (Enumerator, Iteratee (..), ($$), joinI, run_, Enumeratee, Step, (=$), ($=))++import qualified System.ZMQ as ZMQ+import Data.Default++-- | Client identifier from Mongrel2. This identifies the currently+-- connected client uniquely.+type ClientID = Int64++-- | UUID for communicating with Mongrel2.+type UUID = ByteString++data Request = Request+ {+ -- (UUID, ClientID, Path, RequestHeaders, RequestBody)+ requestUuid :: UUID+ , requestClientId :: ClientID+ , requestPath :: ByteString+ , requestHeaders :: Value+ , requestBody :: ByteString+ }+ deriving (Show)++data Response = Response+ {+ responseUuid :: UUID+ , responseClientId :: ClientID+ , responseBody :: (forall a. Enumerator ByteString IO a)+ }+++data Handler = Handler + { handlerPullFrom :: String+ , handlerPublishTo :: String+ , handlerId :: Maybe String+ }++instance Default Handler where+ def = Handler def def def+++data ConnectedHandler = ConnectedHandler + { chPullSocket :: ZMQ.Socket ZMQ.Pull+ , chPublishSocket :: ZMQ.Socket ZMQ.Pub+ }
+ src/Hack2/Handler/Mongrel2HTTP.hs view
@@ -0,0 +1,306 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE Rank2Types #-}+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE ScopedTypeVariables #-}++module Hack2.Handler.Mongrel2HTTP +(++ runWithConfig+, ServerConfig(..)+, Types.Handler(..)+) where++import Prelude ()+import Air.Env hiding (def, Default, log)++import Hack2+import Data.Default (def, Default)+import qualified Data.CaseInsensitive as CaseInsensitive+import Data.ByteString.Char8 (ByteString, pack)+import qualified Data.ByteString.Char8 as B+import Data.Enumerator (Enumerator, Iteratee (..), ($$), joinI, run_, Enumeratee, Step, (=$), ($=), enumList, concatEnums)+import qualified Data.Enumerator.List as EL+import Blaze.ByteString.Builder (Builder, fromByteString, fromLazyByteString, toByteString)++import Data.Maybe (listToMaybe, fromMaybe, isJust, fromJust)+import Data.Map (toAscList, fromAscList)++import Data.IORef (readIORef)++import System.Directory (createDirectory, doesDirectoryExist)+import Control.Monad (when, forever)++import qualified Hack2.Handler.Mongrel2 as Mongrel2++import qualified Hack2.Handler.Mongrel2.Types as Types++import qualified Control.Exception as Exception+import System.IO (hPutStr, stderr)++import System.Posix.Signals+import Control.Concurrent+import Control.Concurrent.MVar+import System.Exit (exitSuccess)++import qualified Data.Map as Map+import Blaze.Text.Int (integral)+import Data.Monoid+import qualified Data.Aeson as Aeson+import Data.Text.Encoding (encodeUtf8)+import Safe (readMay, readDef)++import Control.Concurrent+import System.IO.Unsafe (unsafePerformIO)++ +buildSpace :: Builder+buildSpace = fromByteString " "++buildNewLine :: Builder+buildNewLine = fromByteString "\r\n"++make_response_enumerator :: Response -> Enumerator ByteString IO a+make_response_enumerator response = + concatEnums + [+ header_enum+ , response.body.unHackEnumerator+ ]+ where+ header_enum = enumList 1 - return - make_response_header - response++make_response_header :: Response -> ByteString+make_response_header response = + [+ fromByteString "HTTP/1.1"+ , buildSpace+ , integral - response.status+ , buildSpace+ , fromByteString - (statusReasonMap.Map.lookup (response.status) .fromMaybe "Unknown Status Code")+ + , buildNewLine+ , mconcat - response.headers.map build_header+ , buildNewLine+ ]+ + .mconcat+ .toByteString+ + where+ build_header :: (ByteString, ByteString) -> Builder+ build_header (header, value) = + [+ fromByteString header+ , fromByteString ": "+ , fromByteString value+ , buildNewLine+ ]+ .mconcat++-- example json request string+-- {"PATH":"/","x-forwarded-for":"127.0.0.1","accept":"*/*","user-agent":"curl/7.19.7 (universal-apple-darwin10.0) libcurl/7.19.7 OpenSSL/0.9.8l zlib/1.2.3","host":"127.0.0.1:6767","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/","PATTERN":"/"}++-- { requestMethod :: RequestMethod+-- , scriptName :: ByteString+-- , pathInfo :: ByteString+-- , queryString :: ByteString+-- , serverName :: ByteString+-- , serverPort :: Int+-- , httpHeaders :: [(ByteString, ByteString)]+-- , hackVersion :: (Int, Int, Int)+-- , hackUrlScheme :: HackUrlScheme+-- , hackInput :: HackEnumerator+-- , hackErrors :: HackErrors+-- , hackHeaders :: [(ByteString, ByteString)]+++requestToEnv :: Mongrel2.Request -> Env+requestToEnv request = + let+ maybe_string (Aeson.String x) = Just - x+ maybe_string _ = Nothing+ + _headers = + case request.Mongrel2.requestHeaders of+ Aeson.Object headerMap -> + headerMap+ .Map.toAscList+ .map_snd maybe_string+ .select (snd > isJust)+ .map (\(x,y) -> (x.encodeUtf8, fromJust y.encodeUtf8))+ _ -> []+ + _host = _headers.lookup "host" .fromMaybe ""+ _query = _headers.lookup "QUERY" .fromMaybe ""+ + _mongrel2_header_names = + + [+ "PATH"+ , "host"+ , "METHOD"+ , "VERSION"+ ]+ + _http_headers = _headers+ + _input_enum :: (forall a . Enumerator ByteString IO a)+ _input_enum = enumList 1 [request.Mongrel2.requestBody]+ + in+ + def+ {+ requestMethod = ( _headers.lookup "METHOD" >>= B.unpack > readMay ).fromMaybe GET+ , pathInfo = request.Mongrel2.requestPath+ , queryString = _query+ , serverName = _host.B.takeWhile (is_not ':')+ , serverPort = _host.B.dropWhile (is_not ':') .B.dropWhile (is ':') .B.unpack.readDef 80+ , httpHeaders = _http_headers+ , hackInput = HackEnumerator _input_enum+ }+ ++++-- copied from snap-core+statusReasonMap :: Map.Map Int ByteString+statusReasonMap = Map.fromAscList [+ (100, "Continue"),+ (101, "Switching Protocols"),+ (200, "OK"),+ (201, "Created"),+ (202, "Accepted"),+ (203, "Non-Authoritative Information"),+ (204, "No Content"),+ (205, "Reset Content"),+ (206, "Partial Content"),+ (300, "Multiple Choices"),+ (301, "Moved Permanently"),+ (302, "Found"),+ (303, "See Other"),+ (304, "Not Modified"),+ (305, "Use Proxy"),+ (307, "Temporary Redirect"),+ (400, "Bad Request"),+ (401, "Unauthorized"),+ (402, "Payment Required"),+ (403, "Forbidden"),+ (404, "Not Found"),+ (405, "Method Not Allowed"),+ (406, "Not Acceptable"),+ (407, "Proxy Authentication Required"),+ (408, "Request Time-out"),+ (409, "Conflict"),+ (410, "Gone"),+ (411, "Length Required"),+ (412, "Precondition Failed"),+ (413, "Request Entity Too Large"),+ (414, "Request-URI Too Large"),+ (415, "Unsupported Media Type"),+ (416, "Requested range not satisfiable"),+ (417, "Expectation Failed"),+ (500, "Internal Server Error"),+ (501, "Not Implemented"),+ (502, "Bad Gateway"),+ (503, "Service Unavailable"),+ (504, "Gateway Time-out"),+ (505, "HTTP Version not supported")+ ]+ + ++-- ++data ServerConfig = ServerConfig+ {+ handler :: Mongrel2.Handler+ , number_of_application_instances :: Int+ }++instance Default ServerConfig where+ def = ServerConfig+ {+ handler = def+ , number_of_application_instances = 10+ }+++exit_handler :: MVar () -> IO ()+exit_handler exit_indicator = do+ log "Exiting..."+ putMVar exit_indicator ()++sync_lock :: MVar ()+sync_lock = unsafePerformIO - newMVar ()++jailed :: IO a -> IO a+jailed io = do+ withMVar sync_lock (const io)++-- log :: String -> IO ()+-- log x = jailed (putStrLn x)++log :: String -> IO ()+log = const (return ())+ ++runWithConfig :: ServerConfig -> Application -> IO ()+runWithConfig config app = do+ exit_indicator <- newEmptyMVar++ installHandler sigINT (Catch (exit_handler exit_indicator)) Nothing++ threads <- [1..config.number_of_application_instances] .mapM (\instance_id -> + forkIO - server_safe_loop instance_id+ )+ + log "taking exit_indicator"+ takeMVar exit_indicator+ log "done taking exit_indicator"++ + -- log "killing threads ..."+ -- threads.mapM_ killThread+ -- + -- log "waiting threads termination ..."+ -- sleep 2+ -- + -- log "all threads killed"+ -- log "Mongrel2 main will exist"+ + where+ server_safe_loop instance_id = do+ forever - do+ Exception.catch (loop instance_id (config.handler) app) - \e ->+ log - "Mongrel2 server error: " + (e :: Exception.SomeException).show + "\n"++++loop :: Int -> Mongrel2.Handler -> Application -> IO ()+loop instance_id _handler app = do++ Mongrel2.withConnectedHandler _handler - \connected_handler -> do+ log - "instance " + show instance_id + " waiting ..."+ request <- Mongrel2.receiveRequest connected_handler+ log - "instance " + show instance_id + " handling ..."+ -- putStrLn - "instance " + show instance_id + " handling ..."+ ++ let uuid = request.Mongrel2.requestUuid+ client_id = request.Mongrel2.requestClientId++ app_response <- app - requestToEnv request+ + let { resposne = + Mongrel2.Response+ {+ Mongrel2.responseUuid = uuid+ , Mongrel2.responseClientId = client_id+ , Mongrel2.responseBody = make_response_enumerator app_response+ }+ }++ Mongrel2.sendResponse connected_handler resposne+