Build a HTTP Proxy in Haskell on AWS Lambda

This is just a quick one to put some Haskell code on AWS Lambda, and what’s better than a good-old HTTP Proxy?

The Setup

npm install -g serverless

And we’re going to use serverless-haskell as our Haskell-Lambda library and stack as our build tool. So let’s install stack if we haven’t already:

curl -sSL https://get.haskellstack.org/ | sh

and let’s create a project with serverless-haskell:

stack new http-proxy-lambda https://raw.githubusercontent.com/seek-oss/serverless-haskell/master/serverless-haskell.hsfiles

We’ve called our project http-proxy-lambda and we’ve used the serverless-haskell template to create the project. Now cd http-proxy-lambda.

The Config

functions:  
http-proxy-lambda:
runtime: haskell
handler: http-proxy-lambda.http-proxy-lambda-exe
events:
- http:
path: endpoint/{url}
method: get
cors: true

There’s a couple things to note here. First is that we’ve moved runtime from the top-level to inside our specific function. This is because this provides us with the possibility of having multiple run-times for different functions at a later date — maybe we want a JS frontend?

The handler key is what tells serverless where to find the handler code. Specifically, http-proxy-lambda.http-proxy-lambda-exe says this is the http-proxy-lambda project (which we just created) and http-proxy-lambda-exe is the name of the executable. We can change the executable name by finding it in package.yaml and changing it there.

The events key is where we configure our API Gateway, setting up a HTTP GET request-response cycle when you hit endpoint/{url} — and we’ve turned CORS on, because why not?

The Code

{-# LANGUAGE OverloadedStrings #-} module Main where import           AWSLambda.Events.APIGateway
import Control.Lens
import qualified Data.ByteString.Lazy.Internal as BSL
import qualified Data.HashMap.Strict as HMS
import Data.Text
import qualified Data.Text.Lazy as LazyText
import qualified Data.Text.Lazy.Encoding as LazyText
import qualified Network.Wreq as Http
main :: IO ()
main = apiGatewayMain handler
htmlRes :: Int -> Text -> IO (APIGatewayProxyResponse Text)
htmlRes status proxyBody
= pure $ htmlResWithNoBody status & responseBody ?~ proxyBody
where
htmlResWithNoBody :: Int -> APIGatewayProxyResponse Text
htmlResWithNoBody statusCode
= APIGatewayProxyResponse statusCode [("Content-Type", "text/html")] Nothing
getProxyBody :: Http.Response BSL.ByteString -> IO Text
getProxyBody resFromGivenUrl
= return
. LazyText.toStrict
. LazyText.decodeUtf8
$ resFromGivenUrl ^. Http.responseBody
handler :: APIGatewayProxyRequest Text -> IO (APIGatewayProxyResponse Text)
handler request = do
let urlPath = HMS.lookup "url" $ request ^. agprqPathParameters
case urlPath of
Just path ->
(Http.get $ "https://" <> unpack path)
>>= getProxyBody
>>= htmlRes 200
Nothing -> htmlRes 500 "No path found"

Because this will be compiled to an executable, we need to define a main function. And in this main function we set up what kind of Lambda handler we will be using — in this case, an apiGatewayMain handler.

The most important function after main is the handler function. And what it does is it takes the request and pulls off the path parameters. So if you visit endpoint/proxymeplease the request will contain a path hashmap of url: proxymeplease. This url key comes directly from the serverless.yml config we wrote earlier, when we wrote path: endpoint/{url}.

With this hashmap we lookup for the url key and if we find it we make a HTTP GET request to that endpoint. Once we’ve done that we pull out the response body with getProxyBody and then we form some HTML which we will return with htmlRes. If we don’t find a url key then we just return a 500 page.

What’s nice about this is that to return HTML, instead of the default JSON response Lambda would return, all we have to do is construct a APIGatewayProxyResponse and give it a Content-Type of text/html, as is done in the htmlRes function.

Note that the core sequence logic of our code is expressed in 3 lines here:

(Http.get $ "https://" <> unpack path)         
>>= getProxyBody
>>= htmlRes 200

How clean and elegant.

With that we can deploy our Lambda + API Gateway with:

sls deploy

Providing we have an AWS account, of course. This will then give us a magical https://{bla}.execute-api-region.amazonaws.com/dev/endpoint/{url} endpoint.

The Result

And since our AWS Lambda is sitting in the US (us-east-1), we can verify that we are proxying through US servers.

Appendum

Also note I’ve turned the Lambda off, which incidentally is done by simply running the command sls remove. Neat!

Software Engineer at the BBC. All views my own.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store