packages feed

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