0

What I'm trying to do is to create a somewhat smart reverse proxy server that should process some requests on its own and forward the others to the backend of choice. To make it challenging I'm trying hard to do it in Haskell, which I am a total newbie in.

Here's the code I've come up so far:

{-# LANGUAGE OverloadedStrings #-}

import Control.Applicative
import Data.ByteString
import Network.HTTP.ReverseProxy
import Network.HTTP.Types
import Network.Wai
import Network.Wai.Handler.Warp
import Network.Wai.Middleware.RequestLogger
import qualified Network.HTTP.Client as HC

helloApp :: Application
helloApp req respond =
  respond $ responseLBS status200 [("Content-Type", "text/plain")] "Hello"

proxyStubApp :: Application
proxyStubApp req respond =
  respond $ responseLBS status200 [("Content-Type", "text/plain")] "You've hit the stub"

proxyApp :: IO Application
proxyApp = do
  manager <- HC.newManager HC.defaultManagerSettings
  return $ waiProxyTo (const $ return $ WPRProxyDest ProxyDest { pdHost = "localhost", pdPort = 9393 }) defaultOnExc manager

app :: Application
app req respond =
  serve req respond
    where serve = lookupServeFunction req

lookupServeFunction :: Request -> Application
lookupServeFunction req
  | isInfixOf "sample_path" (rawPathInfo req) = proxyStubApp
  | otherwise                                 = helloApp

main = run 3011 =<< (logStdoutDev <$> return app)

It works fine, but when I exchange proxyStubApp for actual proxyApp I am forced to add IO all over the place. Particularly it gets added to app, consequently leaving me with the following compilation error message:

Couldn't match expected type ‘Request -> t5 -> t4’
            with actual type ‘IO Application’
The equation(s) for ‘app’ have two arguments,
but its type ‘IO Application’ has none

I feel like I understand why it is happening, but I'm out of ideas of how to cope with it :( Or am I doing something totally wrong?

Thank you!

P.S. Here are the dependencies should you want to compile the thing on your own: wai warp http-types text bytestring wai-extra time http-reverse-proxy http-client

SkyWriter
  • 1,454
  • 10
  • 17

1 Answers1

1

The IO in IO Application is kind-of redundant. Note that

type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived

so, expanding proxyApp's arguments (what you already do in proxyStubApp), you get

proxyApp :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
proxyApp req continuation = do
     manager <- HC.newManager HC.defaultManagerSettings
     waiProxyTo (...) req respond

That works, because in either case

proxyApp :: IO Application
proxyApp = do
   manager <- HC.newManager ...
   ...        

and

proxyApp :: Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
proxyApp req continuation = do
   manager <- HC.newManager ...
   ...

the IO action HC.newManager ... is "run within IO".

You may find it conceptually clearer to construct an Application in IO and hand it to some other place, and I won't argue with you. I want to note though, that you choose the Application based on the Request, so in a way you are in the hypothetical HTTP monad when choosing, so lookupServeFunction's signature Request -> Application makes more sense to me.

If you want to keep that type signature for proxyApp, lookupServeFunction and app will have to be in IO as well and main will have to change accordingly, e.g.

myApp <- app
...

As haoformayor said, It is generally easier to work without the outer IO layer.


You might also like to simplify main.

fmap logStdoutDev (return app)

is the same as

return (logStdoutDev app)

and run 3011 =<< return (logStdoutDev app)

is the same as

run 3011 (logStdoutDev app)

You might want to install hlint, which will help you spot these.

ibotty
  • 707
  • 4
  • 10
  • I can see how `IO Application` can make sense. Imagine, it's an `Application` that is being returned as a result of an IO action, which looks quite like my case: `waiProxyTo` here is a sort of an app generator that depends on a `manager` which in turn is a result of an IO somehow. Can't it be that way? – SkyWriter Dec 28 '15 at 14:30
  • 1
    I think the real point is that `a -> IO b` is isomorphic to `IO (a -> IO b)`. Any value of one of those types can be converted to the other. If you have `wibble :: a -> IO b` and `wobble :: IO (a -> IO b)`, `return wibble` gets you going forward and `(wobble >>=) . flip id` gets you going backward. (And of the two, `a -> IO b` is generally easier to work with, in e.g. situations like this one, although that's a subjective preference. The fewer monadic blankets you have to climb out of to peek your head out the better, I say.) – hao Dec 28 '15 at 15:44
  • @haoformayor, does this mean that `proxyApp :: IO Application` should really be the same thing as `proxyApp :: Application`? Removing `IO` makes compiler upset at the line where I call `HC.newManager` and bind it with the following message `Couldn't match type ‘IO b0’ with ‘Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived’. Expected type: IO HC.Manager -> (HC.Manager -> IO b0) -> Application. Actual type: IO HC.Manager -> (HC.Manager -> IO b0) -> IO b0`... I wonder how long does it take to start making sense of these :-) – SkyWriter Dec 28 '15 at 16:10
  • Maybe my answer is unclear. I'll try to clarify it in a moment. – ibotty Dec 28 '15 at 17:57