diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
 ## Sample lambda function
 
 ```
-stack new my-haskell-lambda https://github.com/theam/aws-lambda-haskell-runtime/raw/master/stack-template.hsfiles --resolver=lts-12.13 --omit-packages
+stack new my-haskell-lambda https://github.com/theam/aws-lambda-haskell-runtime/raw/master/stack-template.hsfiles --resolver=lts-13.0 --omit-packages
 cd my-haskell-lambda
 stack docker pull
 ```
@@ -21,7 +21,7 @@
 - .
 
 extra-deps:
-- aws-lambda-haskell-runtime-1.0.9
+- aws-lambda-haskell-runtime-1.0.10
 ```
 
 to your `stack.yaml`
@@ -40,7 +40,7 @@
 
 The ARN of the runtime layer is:
 ```
-arn:aws:lambda:<YOUR REGION>:785355572843:layer:aws-haskell-runtime:2
+arn:aws:lambda:<YOUR REGION>:785355572843:layer:aws-haskell-runtime:5
 ````
 
 ## Full user guide
diff --git a/app/Main.hs b/app/Main.hs
--- a/app/Main.hs
+++ b/app/Main.hs
@@ -1,13 +1,21 @@
-module Main where
+-- | Main entry point for the layer
+module Main
+  ( main
+  ) where
 
-import Control.Monad
-import Control.Monad.Except
 import Aws.Lambda.Runtime
+import Control.Monad
+import qualified Network.HTTP.Client as Http
 
 
+httpManagerSettings :: Http.ManagerSettings
+httpManagerSettings =
+  -- We set the timeout to none, as AWS Lambda freezes the containers.
+  Http.defaultManagerSettings
+  { Http.managerResponseTimeout = Http.responseTimeoutNone
+  }
+
 main :: IO ()
-main = forever $ do
-  res <- runExceptT lambdaRunner
-  case res of
-    Right _ -> return ()
-    Left err  -> putStrLn $ show err
+main = do
+  manager <- Http.newManager httpManagerSettings
+  forever (runLambda manager)
diff --git a/aws-lambda-haskell-runtime.cabal b/aws-lambda-haskell-runtime.cabal
--- a/aws-lambda-haskell-runtime.cabal
+++ b/aws-lambda-haskell-runtime.cabal
@@ -4,10 +4,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 7cc036543eaa96e63b9689f813bbc9413840dd185700a6f40ef90f3a40341b52
+-- hash: a4a6b9dc1371218a01d228e551520a6e1296955192cb06ca15f14ebe0923badd
 
 name:           aws-lambda-haskell-runtime
-version:        1.0.10
+version:        1.1.0
 synopsis:       Haskell runtime for AWS Lambda
 description:    Please see the README on GitHub at <https://github.com/theam/aws-lambda-haskell-runtime#readme>
 category:       AWS
@@ -29,32 +29,43 @@
 
 library
   exposed-modules:
+      Aws.Lambda
       Aws.Lambda.Runtime
-      Aws.Lambda.Configuration
   other-modules:
-      Aws.Lambda.ThHelpers
+      Aws.Lambda.Configuration
+      Aws.Lambda.Meta.Common
+      Aws.Lambda.Meta.Discover
+      Aws.Lambda.Meta.Dispatch
+      Aws.Lambda.Meta.Main
+      Aws.Lambda.Meta.Run
+      Aws.Lambda.Runtime.API.Endpoints
+      Aws.Lambda.Runtime.API.Version
+      Aws.Lambda.Runtime.ApiInfo
+      Aws.Lambda.Runtime.Context
+      Aws.Lambda.Runtime.Environment
+      Aws.Lambda.Runtime.Error
+      Aws.Lambda.Runtime.IPC
+      Aws.Lambda.Runtime.Publish
+      Aws.Lambda.Runtime.Result
       Paths_aws_lambda_haskell_runtime
   hs-source-dirs:
       src
-  default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables DeriveGeneric TypeApplications
-  ghc-options: -Wall -optP-Wno-nonportable-include-path
+  default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables DeriveGeneric TypeApplications FlexibleContexts DeriveAnyClass QuasiQuotes
+  ghc-options: -Wall -fno-warn-orphans -optP-Wno-nonportable-include-path -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -fhide-source-paths -freverse-errors
   build-depends:
       aeson
     , base >=4.7 && <5
     , bytestring
-    , case-insensitive
-    , conduit
-    , directory
-    , filepath
     , http-client
-    , microlens-platform
-    , mtl
+    , http-types
     , optparse-generic
+    , path
+    , path-io
     , process
+    , safe-exceptions-checked
     , template-haskell
     , text
     , uuid
-    , wreq
   default-language: Haskell2010
 
 executable bootstrap
@@ -63,12 +74,12 @@
       Paths_aws_lambda_haskell_runtime
   hs-source-dirs:
       app
-  default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables DeriveGeneric TypeApplications
-  ghc-options: -Wall -optP-Wno-nonportable-include-path
+  default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables DeriveGeneric TypeApplications FlexibleContexts DeriveAnyClass QuasiQuotes
+  ghc-options: -Wall -fno-warn-orphans -optP-Wno-nonportable-include-path -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -fhide-source-paths -freverse-errors
   build-depends:
       aws-lambda-haskell-runtime
     , base >=4.7 && <5
-    , mtl
+    , http-client
   default-language: Haskell2010
 
 test-suite aws-lambda-haskell-runtime-test
@@ -78,10 +89,9 @@
       Paths_aws_lambda_haskell_runtime
   hs-source-dirs:
       test
-  default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables DeriveGeneric TypeApplications
-  ghc-options: -Wall -optP-Wno-nonportable-include-path -threaded -rtsopts -with-rtsopts=-N
+  default-extensions: TemplateHaskell OverloadedStrings RecordWildCards ScopedTypeVariables DeriveGeneric TypeApplications FlexibleContexts DeriveAnyClass QuasiQuotes
+  ghc-options: -Wall -fno-warn-orphans -optP-Wno-nonportable-include-path -Wincomplete-uni-patterns -Wincomplete-record-updates -Wcompat -Widentities -Wredundant-constraints -Wmissing-export-lists -Wpartial-fields -fhide-source-paths -freverse-errors -threaded -rtsopts -with-rtsopts=-N
   build-depends:
       base >=4.7 && <5
     , hspec
-    , mtl
   default-language: Haskell2010
diff --git a/src/Aws/Lambda.hs b/src/Aws/Lambda.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda.hs
@@ -0,0 +1,6 @@
+module Aws.Lambda
+  ( module Reexported
+  ) where
+
+import Aws.Lambda.Configuration as Reexported
+import Aws.Lambda.Runtime.Context as Reexported
diff --git a/src/Aws/Lambda/Configuration.hs b/src/Aws/Lambda/Configuration.hs
--- a/src/Aws/Lambda/Configuration.hs
+++ b/src/Aws/Lambda/Configuration.hs
@@ -1,184 +1,27 @@
 {-# OPTIONS_GHC -fno-warn-unused-pattern-binds #-}
 module Aws.Lambda.Configuration
-  ( LambdaOptions (..)
+  ( Main.LambdaOptions(..)
+  , Main.generate
+  , Main.getRecord
   , configureLambda
-  , returnAndFail
-  , returnAndSucceed
-  , decodeObj
-  , Options.getRecord
+  , IPC.returnAndFail
+  , IPC.returnAndSucceed
+  , Dispatch.decodeObj
   )
 where
 
-import Data.Aeson
-
-import qualified Data.Text as Text
-import Data.Text (Text)
-import GHC.Generics
-import Data.Function ((&))
-import Language.Haskell.TH
-import qualified Options.Generic as Options
-import qualified Data.Conduit as Conduit
-import qualified System.Directory as Directory
-import System.FilePath ((</>))
-import System.IO.Error
-import System.IO (hFlush, stdout, stderr)
-import System.Exit (exitSuccess, exitFailure)
-import Control.Monad.Trans
-import Control.Monad
-import qualified Data.Text.Encoding    as Encoding
-import qualified Data.ByteString.Lazy as LazyByteString
-import Data.Void
-import Data.Monoid
-
-
-
-import Aws.Lambda.ThHelpers
-
-putTextLn :: Text -> IO ()
-putTextLn = putStrLn . Text.unpack
-
-data LambdaOptions = LambdaOptions
-  { eventObject     :: Text
-  , contextObject   :: Text
-  , functionHandler :: Text
-  , executionUuid   :: Text
-  } deriving (Generic)
-instance Options.ParseRecord LambdaOptions
-
-
--- This function is the reason why we disable the warning on top of the module
-mkMain :: Q [Dec]
-mkMain = [d|
-  $(pName "main") = getRecord "" >>= run
-  |]
-
-mkRun :: Q Dec
-mkRun = do
-  handlers <- runIO getHandlers
-  clause' <- recordQ "LambdaOptions" ["functionHandler", "contextObject", "eventObject", "executionUuid"]
-  body <- dispatcherCaseQ handlers
-  pure $ FunD (mkName "run") [Clause [clause'] (NormalB body) []]
-
-
-dispatcherCaseQ :: [Text] -> Q Exp
-dispatcherCaseQ fileNames = do
-  caseExp <- eName "functionHandler"
-  matches <- traverse handlerCaseQ fileNames
-  unmatched <- unmatchedCaseQ
-  pure $ CaseE caseExp (matches <> [unmatched])
-
-
-handlerCaseQ :: Text -> Q Match
-handlerCaseQ lambdaHandler = do
-  let pattern = LitP (StringL $ Text.unpack lambdaHandler)
-  body <- [e|do
-    result <- $(eName qualifiedName) (decodeObj $(eName "eventObject")) (decodeObj $(eName "contextObject"))
-    either (returnAndFail $(eName "executionUuid")) (returnAndSucceed $(eName "executionUuid")) result
-    |]
-  pure $ Match pattern (NormalB body) []
- where
-  qualifiedName =
-    lambdaHandler
-    & Text.dropWhile (/= '/')
-    & Text.drop 1
-    & Text.replace "/" "."
-
+import qualified Language.Haskell.TH as Meta
 
-unmatchedCaseQ :: Q Match
-unmatchedCaseQ = do
-  let pattern = WildP
-  body <- [e|
-    returnAndFail $(eName "executionUuid") ("Handler " <> $(eName "functionHandler") <> " does not exist on project")
-    |]
-  pure $ Match pattern (NormalB body) []
+import qualified Aws.Lambda.Meta.Dispatch as Dispatch
+import qualified Aws.Lambda.Meta.Main as Main
+import qualified Aws.Lambda.Meta.Run as Run
+import qualified Aws.Lambda.Runtime.IPC as IPC
 
-configureLambda :: Q [Dec]
+{-| Generates a @main@ function to be used with the
+AWS Lambda layer.
+-}
+configureLambda :: Meta.DecsQ
 configureLambda = do
-  main <- mkMain
-  run <- mkRun
-  return $ main <> [run]
-
-
-returnAndFail :: ToJSON a => Text -> a -> IO ()
-returnAndFail uuid v = do
-  hFlush stdout
-  putTextLn uuid
-  hFlush stdout
-  putTextLn (Encoding.decodeUtf8 $ LazyByteString.toStrict $ encode v)
-  hFlush stdout
-  hFlush stderr
-  exitFailure
-
-returnAndSucceed :: ToJSON a => Text -> a -> IO ()
-returnAndSucceed uuid v = do
-  hFlush stdout
-  putTextLn uuid
-  hFlush stdout
-  putTextLn (Encoding.decodeUtf8 $ LazyByteString.toStrict $ encode v)
-  hFlush stdout
-  exitSuccess
-
-decodeObj :: FromJSON a => Text -> a
-decodeObj x =
-  case (eitherDecode $ LazyByteString.fromStrict $ Encoding.encodeUtf8 x) of
-    Left e -> error e
-    Right v -> v
-
-
-data DirContent = DirList [FilePath] [FilePath]
-                | DirError IOError
-data DirData = DirData FilePath DirContent
-
-
--- Produces directory data
-walk :: FilePath -> Conduit.ConduitM () DirData IO ()
-walk path = do
-  result <- lift $ tryIOError listdir
-  case result of
-    Right dl@(DirList subdirs _) -> do
-      Conduit.yield (DirData path dl)
-      forM_ subdirs (walk . (path </>))
-    Right e -> Conduit.yield (DirData path e)
-    Left e -> Conduit.yield (DirData path (DirError e))
- where
-  listdir = do
-    entries <- filterHidden <$> Directory.getDirectoryContents path
-    subdirs <- filterM isDir entries
-    files   <- filterM isFile entries
-    return $ DirList subdirs files
-   where
-    isFile entry = Directory.doesFileExist (path </> entry)
-    isDir entry = Directory.doesDirectoryExist (path </> entry)
-    filterHidden paths = filter (\p -> head p /= '.' && p /= "node_modules") paths
-
-
--- Consume directories
-myVisitor :: Conduit.ConduitM DirData Void IO [FilePath]
-myVisitor = loop []
- where
-  loop n = do
-    r <- Conduit.await
-    case r of
-      Nothing -> return n
-      Just result  -> loop (process result <> n)
-  process (DirData _ (DirError _)) = []
-  process (DirData dir (DirList _ files)) = map (\f -> dir <> "/" <> f) files
-
-
-getHandlers :: IO [Text]
-getHandlers = do
-  files <- Conduit.runConduit $ walk "." Conduit..| myVisitor
-  handlerFiles <- files
-                   & fmap Text.pack
-                   & filter (Text.isSuffixOf ".hs")
-                   & filterM containsHandler
-                   & fmap (fmap $ Text.dropEnd 3)
-                   & fmap (fmap $ Text.drop 2)
-                   & fmap (fmap (<> ".handler"))
-  return handlerFiles
-
-
-containsHandler :: Text -> IO Bool
-containsHandler file = do
-  fileContents <- readFile $ Text.unpack file
-  return $ "handler :: " `Text.isInfixOf` Text.pack fileContents
+  main <- Main.generate
+  run <- Run.generate
+  return (main <> [run])
diff --git a/src/Aws/Lambda/Meta/Common.hs b/src/Aws/Lambda/Meta/Common.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Meta/Common.hs
@@ -0,0 +1,36 @@
+{-| Helper functions to make code generation easier -}
+module Aws.Lambda.Meta.Common
+  ( declarationName
+  , expressionName
+  , getFieldsFrom
+  ) where
+
+import Data.Text (Text)
+import qualified Data.Text as Text
+import Language.Haskell.TH
+
+-- | Helper for defining names in declarations
+-- think of @myValue@ in @myValue = 2@
+declarationName :: Text -> Q Pat
+declarationName = pure . VarP . mkName . Text.unpack
+
+-- | Helper for defining names in expressions
+-- think of @myFunction@ in @quux = myFunction 3@
+expressionName :: Text -> Q Exp
+expressionName = pure . VarE . mkName . Text.unpack
+
+
+-- | Helper for extracting fields of a specified record
+-- it expects the constructor name as the first parameter,
+-- and the list of fields to bring into scope as second
+-- think of @Person@, and @personAge@, @personName@ in
+-- @myFunction Person { personAge, personName } = ...@
+getFieldsFrom :: Text -> [Text] -> Q Pat
+getFieldsFrom name fields = do
+  extractedFields <- traverse extractField fields
+  pure $ RecP (mkName $ Text.unpack name) extractedFields
+ where
+  -- | Helper for extracting fields of records
+  -- think of @personAge@ in @myFunction Person { personAge = personAge } = ...@
+  extractField :: Text -> Q FieldPat
+  extractField n = pure (mkName $ Text.unpack n, VarP $ mkName $ Text.unpack n)
diff --git a/src/Aws/Lambda/Meta/Discover.hs b/src/Aws/Lambda/Meta/Discover.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Meta/Discover.hs
@@ -0,0 +1,51 @@
+{-| Discovery of AWS Lambda handlers
+A handler is basically a function that has a type definition that
+starts with "handler :: ".
+ -}
+module Aws.Lambda.Meta.Discover
+  ( handlers
+  ) where
+
+import qualified Control.Monad as Monad
+import Data.Function ((&))
+import qualified Data.Maybe as Maybe
+import Data.Text (Text)
+import qualified Data.Text as Text
+
+import Path
+import qualified Path.IO as PathIO
+
+{-| Returns a list of handler paths that look like
+
+@src/Foo/Bar/Quux.handler@
+
+It is the path to the source file, but changing the
+extension for ".handler"
+-}
+handlers :: IO [Text]
+handlers = do
+  (_, files) <- PathIO.listDirRecurRel [reldir|.|]
+  handlerFiles <- modulesWithHandler files
+  pure (handlerNames handlerFiles)
+
+modulesWithHandler :: [Path Rel File] -> IO [Path Rel File]
+modulesWithHandler files =
+  filter isHaskellModule files
+  & Monad.filterM containsHandler
+ where
+  isHaskellModule file =
+    fileExtension file == ".hs"
+
+handlerNames :: [Path Rel File] -> [Text]
+handlerNames modules =
+  fmap changeExtensionToHandler modules
+  & fmap (Text.pack . toFilePath)
+ where
+  changeExtensionToHandler file =
+    setFileExtension ".handler" file
+    & Maybe.fromJust  -- The path will be always parsable, as we just replace the extension
+
+containsHandler :: Path Rel File -> IO Bool
+containsHandler file = do
+  fileContents <- readFile $ toFilePath file
+  pure $ "handler :: " `Text.isInfixOf` Text.pack fileContents
diff --git a/src/Aws/Lambda/Meta/Dispatch.hs b/src/Aws/Lambda/Meta/Dispatch.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Meta/Dispatch.hs
@@ -0,0 +1,61 @@
+{-| Dispatcher generation -}
+module Aws.Lambda.Meta.Dispatch
+  ( generate
+  , decodeObj
+  ) where
+
+import Data.Function ((&))
+import Data.Text (Text)
+import qualified Data.Text as Text
+
+import Data.Aeson
+import qualified Data.ByteString.Lazy.Char8 as LazyByteString
+import qualified Language.Haskell.TH as Meta
+
+import Aws.Lambda.Meta.Common
+
+{-| Helper function that the dispatcher will use to
+decode the JSON that comes as an AWS Lambda event into the
+appropriate type expected by the handler.
+-}
+decodeObj :: FromJSON a => String -> a
+decodeObj x =
+  case (eitherDecode $ LazyByteString.pack x) of
+    Left e  -> error e
+    Right v -> v
+
+{-| Generates the dispatcher out of a list of
+handler names in the form @src/Foo/Bar.handler@
+
+This dispatcher has a case for each of the handlers that calls
+the appropriate qualified function. In the case of the example above,
+the dispatcher will call @Foo.Bar.handler@.
+-}
+generate :: [Text] -> Meta.ExpQ
+generate handlerNames = do
+  caseExp <- expressionName "functionHandler"
+  matches <- traverse handlerCase handlerNames
+  unmatched <- unmatchedCase
+  pure $ Meta.CaseE caseExp (matches <> [unmatched])
+
+handlerCase :: Text -> Meta.MatchQ
+handlerCase lambdaHandler = do
+  let pat = Meta.LitP (Meta.StringL $ Text.unpack lambdaHandler)
+  body <- [e|do
+    result <- $(expressionName qualifiedName) (decodeObj $(expressionName "eventObject")) (decodeObj $(expressionName "contextObject"))
+    either (returnAndFail $(expressionName "executionUuid")) (returnAndSucceed $(expressionName "executionUuid")) result |]
+  pure $ Meta.Match pat (Meta.NormalB body) []
+ where
+  qualifiedName =
+    lambdaHandler
+    & Text.dropWhile (/= '/')
+    & Text.drop 1
+    & Text.replace "/" "."
+
+unmatchedCase :: Meta.MatchQ
+unmatchedCase = do
+  let pattern = Meta.WildP
+  body <- [e|
+    returnAndFail $(expressionName "executionUuid") ("Handler " <> $(expressionName "functionHandler") <> " does not exist on project")
+    |]
+  pure $ Meta.Match pattern (Meta.NormalB body) []
diff --git a/src/Aws/Lambda/Meta/Main.hs b/src/Aws/Lambda/Meta/Main.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Meta/Main.hs
@@ -0,0 +1,27 @@
+{-| main function generation for interoperation with the layer -}
+module Aws.Lambda.Meta.Main
+  ( LambdaOptions(..)
+  , generate
+  , Options.getRecord
+  ) where
+
+import GHC.Generics (Generic)
+
+import qualified Language.Haskell.TH as Meta
+import qualified Options.Generic as Options
+
+import Aws.Lambda.Meta.Common
+
+-- | Options that the generated main expects
+data LambdaOptions = LambdaOptions
+  { eventObject     :: !String
+  , contextObject   :: !String
+  , functionHandler :: !String
+  , executionUuid   :: !String
+  } deriving (Generic, Options.ParseRecord)
+
+-- | Generate the main function that the layer will call
+generate :: Meta.DecsQ
+generate = [d|
+  $(declarationName "main") = getRecord "" >>= run
+  |]
diff --git a/src/Aws/Lambda/Meta/Run.hs b/src/Aws/Lambda/Meta/Run.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Meta/Run.hs
@@ -0,0 +1,22 @@
+module Aws.Lambda.Meta.Run
+  ( generate
+  ) where
+
+import qualified Language.Haskell.TH as Meta
+
+import Aws.Lambda.Meta.Common
+import qualified Aws.Lambda.Meta.Discover as Discover
+import qualified Aws.Lambda.Meta.Dispatch as Dispatch
+
+{-| Generate the run function
+
+It will create a dispatcher that is a huge @case@ expression that
+expects the name of the handler provided by AWS Lambda, and will
+execute the appropriate user function
+ -}
+generate :: Meta.DecQ
+generate = do
+  handlers <- Meta.runIO Discover.handlers
+  clause' <- getFieldsFrom "LambdaOptions" ["functionHandler", "contextObject", "eventObject", "executionUuid"]
+  body <- Dispatch.generate handlers
+  pure $ Meta.FunD (Meta.mkName "run") [Meta.Clause [clause'] (Meta.NormalB body) []]
diff --git a/src/Aws/Lambda/Runtime.hs b/src/Aws/Lambda/Runtime.hs
--- a/src/Aws/Lambda/Runtime.hs
+++ b/src/Aws/Lambda/Runtime.hs
@@ -1,272 +1,49 @@
-module Aws.Lambda.Runtime where
-
-import Control.Exception (Exception, IOException, try)
-import Control.Monad.Except (ExceptT, catchError, throwError)
-import Data.Aeson
-import System.Exit (ExitCode (..))
-import qualified Data.Text as Text
-import Data.Text (Text)
-import GHC.Generics
-import qualified Data.ByteString.Lazy as LazyByteString
-import qualified Data.ByteString as ByteString
-import Control.Monad.Trans
-import Text.Read (readMaybe)
-import qualified Data.Text.Encoding    as Encoding
-import Control.Monad
-import Data.Function ((&))
-import Data.Maybe (listToMaybe)
-import Data.Monoid ((<>))
+module Aws.Lambda.Runtime
+  ( runLambda
+  ) where
 
-import qualified Data.CaseInsensitive as CI
-import Lens.Micro.Platform hiding ((.=))
-import qualified Network.Wreq as Wreq
+import Control.Exception.Safe.Checked
 import qualified Network.HTTP.Client as Http
-import qualified System.Environment as Environment
-import qualified System.Process as Process
-import qualified Data.UUID as UUID
-import qualified Data.UUID.V4 as UUID
-import System.IO (hFlush, stdout)
 
-
-type LByteString = LazyByteString.ByteString
-type ByteString = ByteString.ByteString
-
-type App a =
-  ExceptT RuntimeError IO a
-
-
-data RuntimeError
-  = EnvironmentVariableNotSet Text
-  | ApiConnectionError
-  | ApiHeaderNotSet Text
-  | ParseError Text Text
-  | InvocationError Text
-  deriving (Show)
-instance Exception RuntimeError
-
-instance ToJSON RuntimeError where
-  toJSON (EnvironmentVariableNotSet msg) = object
-    [ "errorType" .= ("EnvironmentVariableNotSet" :: Text)
-    , "errorMessage" .= msg
-    ]
-
-  toJSON ApiConnectionError = object
-    [ "errorType" .= ("ApiConnectionError" :: Text)
-    , "errorMessage" .= ("Could not connect to API to retrieve AWS Lambda parameters" :: Text)
-    ]
-
-  toJSON (ApiHeaderNotSet headerName) = object
-    [ "errorType" .= ("ApiHeaderNotSet" :: Text)
-    , "errorMessage" .= headerName
-    ]
-
-  toJSON (ParseError objectBeingParsed value) = object
-    [ "errorType" .= ("ParseError" :: Text)
-    , "errorMessage" .= ("Parse error for " <> objectBeingParsed <> ", could not parse value '" <> value <> "'")
-    ]
-
-  -- We return the user error as it is
-  toJSON (InvocationError err) = toJSON err
-
-
-
-data Context = Context
-  { memoryLimitInMb    :: !Int
-  , functionName       :: !Text
-  , functionVersion    :: !Text
-  , invokedFunctionArn :: !Text
-  , awsRequestId       :: !Text
-  , xrayTraceId        :: !Text
-  , logStreamName      :: !Text
-  , logGroupName       :: !Text
-  , deadline           :: !Int
-  } deriving (Generic)
-instance FromJSON Context
-instance ToJSON Context
-
-
-newtype LambdaResult =
-  LambdaResult Text
-
-
-awsLambdaVersion :: String
-awsLambdaVersion = "2018-06-01"
-
-
-nextInvocationEndpoint :: Text -> String
-nextInvocationEndpoint endpoint =
-  "http://" <> Text.unpack endpoint <> "/"<> awsLambdaVersion <>"/runtime/invocation/next"
-
-
-responseEndpoint :: Text -> Text -> String
-responseEndpoint lambdaApi requestId =
-  "http://"<> Text.unpack lambdaApi <> "/" <> awsLambdaVersion <> "/runtime/invocation/"<> Text.unpack requestId <> "/response"
-
-
-invocationErrorEndpoint :: Text -> Text -> String
-invocationErrorEndpoint lambdaApi requestId =
-  "http://"<> Text.unpack lambdaApi <> "/" <> awsLambdaVersion <> "/runtime/invocation/"<> Text.unpack requestId <> "/error"
-
-
-runtimeInitErrorEndpoint :: Text -> String
-runtimeInitErrorEndpoint lambdaApi =
-  "http://"<> Text.unpack lambdaApi <> "/" <> awsLambdaVersion <> "/runtime/init/error"
-
-
-readEnvironmentVariable :: Text -> App Text
-readEnvironmentVariable envVar = do
-  v <- lift (Environment.lookupEnv $ Text.unpack envVar)
-  case v of
-    Nothing    -> throwError (EnvironmentVariableNotSet envVar)
-    Just value -> pure (Text.pack value)
-
-
-readFunctionMemory :: App Int
-readFunctionMemory = do
-  let envVar = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"
-  let parseMemory txt = readMaybe (Text.unpack txt)
-  memoryValue <- readEnvironmentVariable envVar
-  case parseMemory memoryValue of
-    Just value -> pure value
-    Nothing    -> throwError (ParseError envVar memoryValue)
-
-
-getApiData :: Text -> App (Wreq.Response LByteString)
-getApiData endpoint =
-  keepRetrying (Wreq.getWith opts $ nextInvocationEndpoint endpoint)
- where
-  opts = Wreq.defaults
-         & Wreq.manager .~ Left (Http.defaultManagerSettings { Http.managerResponseTimeout = Http.responseTimeoutNone })
-  keepRetrying :: IO (Wreq.Response LByteString) -> App (Wreq.Response LByteString)
-  keepRetrying f = do
-    result <- (liftIO $ try f) :: App (Either IOException (Wreq.Response LByteString))
-    case result of
-      Right x -> return x
-      _ -> keepRetrying f
-
-
-extractHeader :: Wreq.Response LByteString -> Text -> Text
-extractHeader apiData header =
-  Encoding.decodeUtf8 (apiData ^. Wreq.responseHeader (CI.mk $ Encoding.encodeUtf8 header))
-
-
-extractIntHeader :: Wreq.Response LByteString -> Text -> App Int
-extractIntHeader apiData headerName = do
-  let header = extractHeader apiData headerName
-  case readMaybe $ Text.unpack header of
-    Nothing    -> throwError (ParseError "deadline" header)
-    Just value -> pure value
-
-
-extractBody :: Wreq.Response LByteString -> Text
-extractBody apiData =
-  Encoding.decodeUtf8 $ LazyByteString.toStrict (apiData ^. Wreq.responseBody)
-
-
-propagateXRayTrace :: Text -> App ()
-propagateXRayTrace xrayTraceId =
-  liftIO $ Environment.setEnv "_X_AMZN_TRACE_ID" $ Text.unpack xrayTraceId
-
-
-initializeContext :: Wreq.Response LByteString -> App Context
-initializeContext apiData = do
-  functionName          <- readEnvironmentVariable "AWS_LAMBDA_FUNCTION_NAME"
-  version               <- readEnvironmentVariable "AWS_LAMBDA_FUNCTION_VERSION"
-  logStream             <- readEnvironmentVariable "AWS_LAMBDA_LOG_STREAM_NAME"
-  logGroup              <- readEnvironmentVariable "AWS_LAMBDA_LOG_GROUP_NAME"
-  memoryLimitInMb       <- readFunctionMemory
-  deadline              <- extractIntHeader apiData "Lambda-Runtime-Deadline-Ms"
-  let xrayTraceId        = extractHeader apiData "Lambda-Runtime-Trace-Id"
-  let awsRequestId       = extractHeader apiData "Lambda-Runtime-Aws-Request-Id"
-  let invokedFunctionArn = extractHeader apiData "Lambda-Runtime-Invoked-Function-Arn"
-  propagateXRayTrace xrayTraceId
-  pure $ Context
-    { functionName       = functionName
-    , functionVersion    = version
-    , logStreamName      = logStream
-    , logGroupName       = logGroup
-    , memoryLimitInMb    = memoryLimitInMb
-    , invokedFunctionArn = invokedFunctionArn
-    , xrayTraceId        = xrayTraceId
-    , awsRequestId       = awsRequestId
-    , deadline           = deadline
-    }
-
-
-getFunctionResult :: UUID.UUID -> Text -> App (Maybe Text)
-getFunctionResult u stdOut = do
-  let out = Text.lines stdOut
-
-  out
-   & takeWhile (/= uuid)
-   & mapM_ ( \t -> do
-    liftIO $ putStrLn $ Text.unpack t
-    liftIO $ hFlush stdout)
-
-  out
-   & dropWhile (/= uuid)
-   & dropWhile (== uuid)
-   & listToMaybe
-   & return
- where
-  uuid = Text.pack $ UUID.toString u
-
-
-invoke :: Text -> Context -> App LambdaResult
-invoke event context = do
-  handlerName <- readEnvironmentVariable "_HANDLER"
-  runningDirectory <- readEnvironmentVariable "LAMBDA_TASK_ROOT"
-  let contextJSON = Encoding.decodeUtf8 $ LazyByteString.toStrict $ encode context
-  uuid <- liftIO UUID.nextRandom
-  out <- liftIO $ Process.readProcessWithExitCode (Text.unpack runningDirectory <> "/haskell_lambda")
-                [ "--eventObject", Text.unpack event
-                , "--contextObject", Text.unpack contextJSON
-                , "--functionHandler", Text.unpack handlerName
-                , "--executionUuid", UUID.toString uuid
-                ]
-                ""
-  case out of
-    (ExitSuccess, stdOut, _) -> do
-      res <- getFunctionResult uuid (Text.pack stdOut)
-      case res of
-        Nothing -> throwError (ParseError "parsing result" $ Text.pack stdOut)
-        Just value -> pure (LambdaResult value)
-    (_, stdOut, stdErr)           ->
-      if stdErr /= ""
-        then throwError (InvocationError $ Text.pack stdErr)
-        else do
-          res <- getFunctionResult uuid (Text.pack stdOut)
-          case res of
-            Nothing -> throwError (ParseError "parsing error" $ Text.pack stdOut)
-            Just value -> throwError (InvocationError value)
-
-
-publishResult :: Context -> Text -> LambdaResult -> App ()
-publishResult Context {..} lambdaApi (LambdaResult result) =
-  void $ liftIO $ Wreq.post (responseEndpoint lambdaApi awsRequestId) (Encoding.encodeUtf8 result)
-
-
-invokeAndPublish :: Context -> Text -> Text -> App ()
-invokeAndPublish ctx event lambdaApiEndpoint = do
-  res <- invoke event ctx
-  publishResult ctx lambdaApiEndpoint res
-
-
-publishError :: Context -> Text -> RuntimeError -> App ()
-publishError Context {..} lambdaApiEndpoint (InvocationError err) =
-  void (liftIO $ Wreq.post (invocationErrorEndpoint lambdaApiEndpoint awsRequestId) (Encoding.encodeUtf8 err))
+import qualified Aws.Lambda.Runtime.ApiInfo as ApiInfo
+import qualified Aws.Lambda.Runtime.Context as Context
+import qualified Aws.Lambda.Runtime.Environment as Environment
+import qualified Aws.Lambda.Runtime.Error as Error
+import qualified Aws.Lambda.Runtime.IPC as IPC
+import qualified Aws.Lambda.Runtime.Publish as Publish
 
-publishError Context {..} lambdaApiEndpoint (ParseError t t2) =
-  void (liftIO $ Wreq.post (invocationErrorEndpoint lambdaApiEndpoint awsRequestId) (toJSON $ ParseError t t2))
+-- | Runs the user @haskell_lambda@ executable and posts back the
+-- results
+runLambda
+  :: Http.Manager
+  -> IO ()
+runLambda manager = do
+  lambdaApi <- Environment.apiEndpoint `catch` variableNotSet
+  event     <- ApiInfo.fetchEvent manager lambdaApi `catch` errorParsing
+  context   <- Context.initialize event `catch` errorParsing `catch` variableNotSet
+  ((invokeAndRun manager lambdaApi event context
+    `catch` \err -> Publish.parsingError err lambdaApi context manager)
+    `catch` \err -> Publish.invocationError err lambdaApi context manager)
+    `catch` \(err :: Error.EnvironmentVariableNotSet) -> Publish.runtimeInitError err lambdaApi context manager
 
-publishError Context {..} lambdaApiEndpoint err =
-  void (liftIO $ Wreq.post (runtimeInitErrorEndpoint lambdaApiEndpoint) (toJSON err))
+invokeAndRun
+  :: Throws Error.Parsing
+  => Throws Error.Invocation
+  => Throws Error.EnvironmentVariableNotSet
+  => Http.Manager
+  -> String
+  -> ApiInfo.Event
+  -> Context.Context
+  -> IO ()
+invokeAndRun manager lambdaApi event context = do
+  result    <- IPC.invoke (ApiInfo.event event) context
+  Publish.result result lambdaApi context manager
+    `catch` \err -> Publish.invocationError err lambdaApi context manager
 
+variableNotSet :: Error.EnvironmentVariableNotSet -> IO a
+variableNotSet (Error.EnvironmentVariableNotSet env) =
+  error ("Error initializing, variable not set: " <> env)
 
-lambdaRunner :: App ()
-lambdaRunner = do
-  lambdaApiEndpoint     <- readEnvironmentVariable "AWS_LAMBDA_RUNTIME_API"
-  apiData               <- getApiData lambdaApiEndpoint
-  let event = extractBody apiData
-  ctx <- initializeContext apiData
-  invokeAndPublish ctx event lambdaApiEndpoint `catchError` publishError ctx lambdaApiEndpoint
+errorParsing :: Error.Parsing -> IO a
+errorParsing Error.Parsing{..} =
+  error ("Failed parsing " <> errorMessage <> ", got" <> actualValue)
diff --git a/src/Aws/Lambda/Runtime/API/Endpoints.hs b/src/Aws/Lambda/Runtime/API/Endpoints.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/API/Endpoints.hs
@@ -0,0 +1,61 @@
+module Aws.Lambda.Runtime.API.Endpoints
+  ( response
+  , invocationError
+  , runtimeInitError
+  , nextInvocation
+  , Endpoint(..)
+  ) where
+
+import qualified Aws.Lambda.Runtime.API.Version as Version
+
+newtype Endpoint =
+  Endpoint String
+  deriving (Show)
+
+-- | Endpoint that provides the ID of the next invocation
+nextInvocation :: String -> Endpoint
+nextInvocation lambdaApi =
+  Endpoint $ concat
+    [ "http://"
+    , lambdaApi
+    , "/"
+    , Version.value
+    , "/runtime/invocation/next"
+    ]
+
+-- | Where the response of the Lambda gets published
+response :: String -> String -> Endpoint
+response lambdaApi requestId =
+  Endpoint $ concat
+    [ "http://"
+    , lambdaApi
+    , "/"
+    , Version.value
+    , "/runtime/invocation/"
+    , requestId
+    , "/response"
+    ]
+
+-- | Invocation (runtime) errors should be published here
+invocationError :: String -> String -> Endpoint
+invocationError lambdaApi requestId =
+  Endpoint $ concat
+    [ "http://"
+    , lambdaApi
+    , "/"
+    , Version.value
+    , "/runtime/invocation/"
+    , requestId
+    , "/error"
+    ]
+
+-- | Runtime initialization errors should go here
+runtimeInitError :: String -> Endpoint
+runtimeInitError lambdaApi =
+  Endpoint $ concat
+    [ "http://"
+    , lambdaApi
+    , "/"
+    , Version.value
+    , "/runtime/init/error"
+    ]
diff --git a/src/Aws/Lambda/Runtime/API/Version.hs b/src/Aws/Lambda/Runtime/API/Version.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/API/Version.hs
@@ -0,0 +1,7 @@
+module Aws.Lambda.Runtime.API.Version
+  ( value
+  ) where
+
+-- | Version of the AWS Lambda runtime REST API
+value :: String
+value = "2018-06-01"
diff --git a/src/Aws/Lambda/Runtime/ApiInfo.hs b/src/Aws/Lambda/Runtime/ApiInfo.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/ApiInfo.hs
@@ -0,0 +1,77 @@
+module Aws.Lambda.Runtime.ApiInfo
+  ( Event(..)
+  , fetchEvent
+  ) where
+
+import qualified Control.Monad as Monad
+import qualified Text.Read as Read
+
+import Control.Exception (IOException)
+import Control.Exception.Safe.Checked
+import Data.ByteString.Char8 (ByteString)
+import qualified Data.ByteString.Char8 as ByteString
+import qualified Data.ByteString.Lazy.Char8 as Lazy
+import qualified Network.HTTP.Client as Http
+import qualified Network.HTTP.Types.Header as Http
+
+import qualified Aws.Lambda.Runtime.API.Endpoints as Endpoints
+import qualified Aws.Lambda.Runtime.Error as Error
+
+-- | Event that is fetched out of the AWS Lambda API
+data Event = Event
+  { deadlineMs         :: !Int
+  , traceId            :: !String
+  , awsRequestId       :: !String
+  , invokedFunctionArn :: !String
+  , event              :: !Lazy.ByteString
+  }
+
+-- | Performs a GET to the endpoint that provides the next event
+fetchEvent :: Throws Error.Parsing => Http.Manager -> String -> IO Event
+fetchEvent manager lambdaApi = do
+  response <- fetchApiData manager lambdaApi
+  let body = Http.responseBody response
+  let headers = Http.responseHeaders response
+  Monad.foldM reduceEvent (initialEvent body) headers
+
+fetchApiData :: Http.Manager -> String -> IO (Http.Response Lazy.ByteString)
+fetchApiData manager lambdaApi = do
+  let Endpoints.Endpoint endpoint = Endpoints.nextInvocation lambdaApi
+  request <- Http.parseRequest endpoint
+  keepRetrying $ Http.httpLbs request manager
+
+reduceEvent :: Throws Error.Parsing => Event -> (Http.HeaderName, ByteString) -> IO Event
+reduceEvent event header =
+  case header of
+    ("Lambda-Runtime-Deadline-Ms", value) ->
+      case Read.readMaybe $ ByteString.unpack value of
+        Just ms -> pure event { deadlineMs = ms }
+        Nothing -> throw (Error.Parsing "deadlineMs" $ ByteString.unpack value)
+
+    ("Lambda-Runtime-Trace-Id", value) ->
+      pure event { traceId = ByteString.unpack value }
+
+    ("Lambda-Runtime-Aws-Request-Id", value) ->
+      pure event { awsRequestId = ByteString.unpack value }
+
+    ("Lambda-Runtime-Invoked-Function-Arn", value) ->
+      pure event { invokedFunctionArn = ByteString.unpack value }
+
+    _ ->
+      pure event
+
+initialEvent :: Lazy.ByteString -> Event
+initialEvent body = Event
+  { deadlineMs = 0
+  , traceId = ""
+  , awsRequestId = ""
+  , invokedFunctionArn = ""
+  , event = body
+  }
+
+keepRetrying :: IO (Http.Response Lazy.ByteString) -> IO (Http.Response Lazy.ByteString)
+keepRetrying action = do
+  result <- try action :: IO (Either IOException (Http.Response Lazy.ByteString))
+  case result of
+    Right success -> pure success
+    _             -> keepRetrying action
diff --git a/src/Aws/Lambda/Runtime/Context.hs b/src/Aws/Lambda/Runtime/Context.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/Context.hs
@@ -0,0 +1,46 @@
+module Aws.Lambda.Runtime.Context
+  ( Context(..)
+  , initialize
+  ) where
+
+import Control.Exception.Safe.Checked
+import Data.Aeson (FromJSON (..), ToJSON (..))
+import GHC.Generics (Generic)
+
+import qualified Aws.Lambda.Runtime.ApiInfo as ApiInfo
+import qualified Aws.Lambda.Runtime.Environment as Environment
+import qualified Aws.Lambda.Runtime.Error as Error
+
+-- | Context that is passed to all the handlers
+data Context = Context
+  { memoryLimitInMb    :: !Int
+  , functionName       :: !String
+  , functionVersion    :: !String
+  , invokedFunctionArn :: !String
+  , awsRequestId       :: !String
+  , xrayTraceId        :: !String
+  , logStreamName      :: !String
+  , logGroupName       :: !String
+  , deadline           :: !Int
+  } deriving (Generic, FromJSON, ToJSON)
+
+-- | Initializes the context out of the environment
+initialize :: Throws Error.Parsing => Throws Error.EnvironmentVariableNotSet => ApiInfo.Event -> IO Context
+initialize ApiInfo.Event{..} = do
+  functionName          <- Environment.functionName
+  version               <- Environment.functionVersion
+  logStream             <- Environment.logStreamName
+  logGroup              <- Environment.logGroupName
+  memoryLimitInMb       <- Environment.functionMemory
+  Environment.setXRayTrace traceId
+  pure Context
+    { functionName       = functionName
+    , functionVersion    = version
+    , logStreamName      = logStream
+    , logGroupName       = logGroup
+    , memoryLimitInMb    = memoryLimitInMb
+    , invokedFunctionArn = invokedFunctionArn
+    , xrayTraceId        = traceId
+    , awsRequestId       = awsRequestId
+    , deadline           = deadlineMs
+    }
diff --git a/src/Aws/Lambda/Runtime/Environment.hs b/src/Aws/Lambda/Runtime/Environment.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/Environment.hs
@@ -0,0 +1,65 @@
+{-| Provides all the values out of
+the environment variables of the system
+-}
+module Aws.Lambda.Runtime.Environment
+  ( functionMemory
+  , apiEndpoint
+  , handlerName
+  , taskRoot
+  , functionName
+  , functionVersion
+  , logStreamName
+  , logGroupName
+  , setXRayTrace
+  ) where
+
+import qualified Aws.Lambda.Runtime.Error as Error
+import Control.Exception.Safe.Checked
+import qualified System.Environment as Environment
+import qualified Text.Read as Read
+
+logGroupName :: Throws Error.EnvironmentVariableNotSet => IO String
+logGroupName =
+  readEnvironmentVariable "AWS_LAMBDA_LOG_GROUP_NAME"
+
+logStreamName :: Throws Error.EnvironmentVariableNotSet => IO String
+logStreamName =
+  readEnvironmentVariable "AWS_LAMBDA_LOG_STREAM_NAME"
+
+functionVersion :: Throws Error.EnvironmentVariableNotSet => IO String
+functionVersion =
+  readEnvironmentVariable "AWS_LAMBDA_FUNCTION_VERSION"
+
+functionName :: Throws Error.EnvironmentVariableNotSet => IO String
+functionName =
+  readEnvironmentVariable "AWS_LAMBDA_FUNCTION_NAME"
+
+setXRayTrace :: String -> IO ()
+setXRayTrace = Environment.setEnv "_X_AMZN_TRACE_ID"
+
+taskRoot :: Throws Error.EnvironmentVariableNotSet => IO String
+taskRoot =
+  readEnvironmentVariable "LAMBDA_TASK_ROOT"
+
+handlerName :: Throws Error.EnvironmentVariableNotSet => IO String
+handlerName =
+  readEnvironmentVariable "_HANDLER"
+
+apiEndpoint :: Throws Error.EnvironmentVariableNotSet => IO String
+apiEndpoint =
+  readEnvironmentVariable "AWS_LAMBDA_RUNTIME_API"
+
+functionMemory :: Throws Error.Parsing => Throws Error.EnvironmentVariableNotSet => IO Int
+functionMemory = do
+  let envVar = "AWS_LAMBDA_FUNCTION_MEMORY_SIZE"
+  memoryValue <- readEnvironmentVariable envVar
+  case Read.readMaybe memoryValue of
+    Just value -> pure value
+    Nothing    -> throw (Error.Parsing envVar memoryValue)
+
+readEnvironmentVariable :: Throws Error.EnvironmentVariableNotSet => String -> IO String
+readEnvironmentVariable envVar = do
+  v <- Environment.lookupEnv envVar
+  case v of
+    Just value -> pure value
+    Nothing    -> throw (Error.EnvironmentVariableNotSet envVar)
diff --git a/src/Aws/Lambda/Runtime/Error.hs b/src/Aws/Lambda/Runtime/Error.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/Error.hs
@@ -0,0 +1,39 @@
+{-| All the errors that the runtime can throw
+-}
+module Aws.Lambda.Runtime.Error
+  ( EnvironmentVariableNotSet(..)
+  , Parsing(..)
+  , Invocation(..)
+  ) where
+
+import Control.Exception.Safe.Checked
+import Data.Aeson (ToJSON (..), object, (.=))
+
+newtype EnvironmentVariableNotSet =
+  EnvironmentVariableNotSet String
+  deriving (Show, Exception)
+
+instance ToJSON EnvironmentVariableNotSet where
+  toJSON (EnvironmentVariableNotSet msg) = object
+    [ "errorType" .= ("EnvironmentVariableNotSet" :: String)
+    , "errorMessage" .= msg
+    ]
+
+data Parsing = Parsing
+  { errorMessage :: String
+  , actualValue  :: String
+  } deriving (Show, Exception)
+
+instance ToJSON Parsing where
+  toJSON (Parsing objectBeingParsed value) = object
+    [ "errorType" .= ("Parsing" :: String)
+    , "errorMessage" .= ("Parse error for " <> objectBeingParsed <> ", could not parse value '" <> value <> "'")
+    ]
+
+newtype Invocation =
+  Invocation String
+  deriving (Show, Exception)
+
+instance ToJSON Invocation where
+  -- We return the user error as it is
+  toJSON (Invocation err) = toJSON err
diff --git a/src/Aws/Lambda/Runtime/IPC.hs b/src/Aws/Lambda/Runtime/IPC.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/IPC.hs
@@ -0,0 +1,116 @@
+{-| Inter-Process Communication
+
+Used for when the user project is called from a layer.
+
+This is used to call the @haskell_lambda@ executable, which is
+provided by the user, when they want to use the layer.
+
+This IPC protocol is based on printing an UUID that is
+created by the layer, and then the result. So everything that
+is printed before the UUID, is considered STDOUT printed by
+the lambda, while what comes after the UUID is considered.
+
+In the case that the lambda execution fails, the exit code
+won't be 0 (exit-success), so it will use the STDERR.
+-}
+module Aws.Lambda.Runtime.IPC
+  ( invoke
+  , returnAndFail
+  , returnAndSucceed
+  ) where
+
+
+import Data.Function ((&))
+import qualified Data.Maybe as Maybe
+import qualified Data.String as String
+import qualified System.Exit as Exit
+import qualified System.IO as IO
+import qualified System.Process as Process
+
+import Control.Exception.Safe.Checked
+import Data.Aeson
+import qualified Data.ByteString.Lazy.Char8 as ByteString
+import qualified Data.UUID as UUID
+import qualified Data.UUID.V4 as UUID
+
+import Aws.Lambda.Runtime.Context (Context (..))
+import qualified Aws.Lambda.Runtime.Environment as Environment
+import qualified Aws.Lambda.Runtime.Error as Error
+import Aws.Lambda.Runtime.Result (LambdaResult (..))
+
+-- | Returns the JSON value failing, according to the protocol
+returnAndFail :: ToJSON a => String -> a -> IO ()
+returnAndFail uuid v = do
+  IO.hFlush IO.stdout
+  putStrLn uuid
+  IO.hFlush IO.stdout
+  putStrLn (ByteString.unpack $ encode v)
+  IO.hFlush IO.stdout
+  IO.hFlush IO.stderr
+  Exit.exitFailure
+
+-- | Returns the JSON value succeeding, according to the protocol
+returnAndSucceed :: ToJSON a => String -> a -> IO ()
+returnAndSucceed uuid v = do
+  IO.hFlush IO.stdout
+  putStrLn uuid
+  IO.hFlush IO.stdout
+  putStrLn (ByteString.unpack $ encode v)
+  IO.hFlush IO.stdout
+  Exit.exitSuccess
+
+-- | Invokes a function defined by the user as the @haskell_lambda@ executable
+invoke
+  :: Throws Error.Invocation
+  => Throws Error.Parsing
+  => Throws Error.EnvironmentVariableNotSet
+  => ByteString.ByteString
+  -> Context
+  -> IO LambdaResult
+invoke event context = do
+  handlerName <- Environment.handlerName
+  runningDirectory <- Environment.taskRoot
+  let contextJSON = ByteString.unpack $ encode context
+  uuid <- UUID.nextRandom
+  out <- Process.readProcessWithExitCode (runningDirectory <> "/haskell_lambda")
+                [ "--eventObject", ByteString.unpack event
+                , "--contextObject", contextJSON
+                , "--functionHandler", handlerName
+                , "--executionUuid", UUID.toString uuid
+                ]
+                ""
+  case out of
+    (Exit.ExitSuccess, stdOut, _) -> do
+      res <- getFunctionResult uuid stdOut
+      case res of
+        Nothing    -> throw (Error.Parsing "parsing result" stdOut)
+        Just value -> pure (LambdaResult value)
+    (_, stdOut, stdErr)           ->
+      if stdErr /= ""
+        then throw (Error.Invocation stdErr)
+        else do
+          res <- getFunctionResult uuid stdOut
+          case res of
+            Nothing    -> throw (Error.Parsing "parsing error" stdOut)
+            Just value -> throw (Error.Invocation value)
+
+getFunctionResult :: UUID.UUID -> String -> IO (Maybe String)
+getFunctionResult u stdOut = do
+  let out = String.lines stdOut
+  let uuid = UUID.toString u
+  printAfterUuid uuid out
+  returnAfterUuid uuid out
+ where
+  printAfterUuid uuid out =
+    out
+    & takeWhile (/= uuid)
+    & mapM_ ( \t -> do
+      putStrLn t
+      IO.hFlush IO.stdout )
+
+  returnAfterUuid uuid out =
+    out
+    & dropWhile (/= uuid)
+    & dropWhile (== uuid)
+    & Maybe.listToMaybe
+    & pure
diff --git a/src/Aws/Lambda/Runtime/Publish.hs b/src/Aws/Lambda/Runtime/Publish.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/Publish.hs
@@ -0,0 +1,55 @@
+{-| Publishing of results/errors back to the
+AWS Lambda runtime API -}
+module Aws.Lambda.Runtime.Publish
+  ( result
+  , invocationError
+  , parsingError
+  , runtimeInitError
+  ) where
+
+import Control.Monad (void)
+import Data.Aeson
+import qualified Data.ByteString.Char8 as ByteString
+import qualified Network.HTTP.Client as Http
+
+import qualified Aws.Lambda.Runtime.API.Endpoints as Endpoints
+import Aws.Lambda.Runtime.Context (Context (..))
+import qualified Aws.Lambda.Runtime.Error as Error
+import Aws.Lambda.Runtime.Result (LambdaResult (..))
+
+-- | Publishes the result back to AWS Lambda
+result :: LambdaResult -> String -> Context -> Http.Manager -> IO ()
+result (LambdaResult res) lambdaApi context manager = do
+  let Endpoints.Endpoint endpoint = Endpoints.response lambdaApi (awsRequestId context)
+  rawRequest <- Http.parseRequest endpoint
+  let request = rawRequest
+                { Http.method = "POST"
+                , Http.requestBody = Http.RequestBodyBS (ByteString.pack res)
+                }
+  void $ Http.httpNoBody request manager
+
+-- | Publishes an invocation error back to AWS Lambda
+invocationError :: Error.Invocation -> String -> Context -> Http.Manager -> IO ()
+invocationError err lambdaApi context =
+  publish err (Endpoints.invocationError lambdaApi $ awsRequestId context)
+    context
+
+-- | Publishes a parsing error back to AWS Lambda
+parsingError :: Error.Parsing -> String -> Context -> Http.Manager -> IO ()
+parsingError err lambdaApi context =
+  publish err (Endpoints.invocationError lambdaApi $ awsRequestId context)
+    context
+
+-- | Publishes a runtime initialization error back to AWS Lambda
+runtimeInitError :: ToJSON err => err -> String -> Context -> Http.Manager -> IO ()
+runtimeInitError err lambdaApi =
+  publish err (Endpoints.runtimeInitError lambdaApi)
+
+publish :: ToJSON err => err -> Endpoints.Endpoint -> Context -> Http.Manager -> IO ()
+publish err (Endpoints.Endpoint endpoint) Context{..} manager = do
+  rawRequest <- Http.parseRequest endpoint
+  let request = rawRequest
+                { Http.method = "POST"
+                , Http.requestBody = Http.RequestBodyLBS (encode err)
+                }
+  void $ Http.httpNoBody request manager
diff --git a/src/Aws/Lambda/Runtime/Result.hs b/src/Aws/Lambda/Runtime/Result.hs
new file mode 100644
--- /dev/null
+++ b/src/Aws/Lambda/Runtime/Result.hs
@@ -0,0 +1,7 @@
+module Aws.Lambda.Runtime.Result
+  ( LambdaResult(..)
+  ) where
+
+-- | Wrapper type to handle the result of the user
+newtype LambdaResult =
+  LambdaResult String
diff --git a/src/Aws/Lambda/ThHelpers.hs b/src/Aws/Lambda/ThHelpers.hs
deleted file mode 100644
--- a/src/Aws/Lambda/ThHelpers.hs
+++ /dev/null
@@ -1,35 +0,0 @@
-module Aws.Lambda.ThHelpers
-  ( pName
-  , eName
-  , recordQ
-  ) where
-
-import Language.Haskell.TH
-import qualified Data.Text as Text
-import Data.Text (Text)
-
--- | Helper for defining names in declarations
--- think of @myValue@ in @myValue = 2@
-pName :: Text -> Q Pat
-pName = pure . VarP . mkName . Text.unpack
-
--- | Helper for defining names in expressions
--- think of @myFunction@ in @quux = myFunction 3@
-eName :: Text -> Q Exp
-eName = pure . VarE . mkName . Text.unpack
-
-
--- | Helper for extracting fields of a specified record
--- it expects the constructor name as the first parameter,
--- and the list of fields to bring into scope as second
--- think of @Person@, and @personAge@, @personName@ in
--- @myFunction Person { personAge, personName } = ...@
-recordQ :: Text -> [Text] -> Q Pat
-recordQ name fields = do
-  extractedFields <- traverse fName fields
-  pure $ RecP (mkName $ Text.unpack name) extractedFields
- where
-  -- | Helper for extracting fields of records
-  -- think of @personAge@ in @myFunction Person { personAge = personAge } = ...@
-  fName :: Text -> Q FieldPat
-  fName n = pure (mkName $ Text.unpack n, VarP $ mkName $ Text.unpack n)
