Web Application in Haskell
This publication will introduce you into development process of web application with usage of Haskell programming language. I would like you to have a clue about “How does the internet work?” and “What is HTTP?” before we will proceed into intial chapter.
Iterative server loop
When you build a web application, something has to sit on a machine and wait for requests to arrive. At the lowest level that “something” is just a loop that repeatedly accepts a connection, handles it, and goes back to waiting. I’ve seen it being called an “iterative server loop” and it’s the simplest shape in which server could appear.
create socket
bind(address, port)
listen()
while true:
conn, client_addr = accept()
handle(conn) # then close
In everyday web development you almost always reach for a higher-level library that hides the socket machinery, and for good reason: virtually nothing above the TCP layer benefits from thinking in terms of individual connections. You don’t need a detailed understanding of how servers work with TCP sockets, nor of how the operating system manages them - knowing which abstraction you’re working at, and what you can safely ignore, is enough. The computer takes care of everything else - whether or not you understand how, and whether that turns out well or badly.
Implementing an HTTP server in Haskell means working with the abstractions the language and its ecosystem provide for talking to the lower layers of the system. In this case we need three well-established packages from Hackage: Network.Socket (to create, bind and accept TCP sockets), Network.HTTP (to parse incoming HTTP requests and send responses) and Network.URI (to extract the path from URI).
Let’s start out with “Hello World” application which would use putStrLn to display a string to you once you will run a compiled binary. I’ve assumed you know what Cabal is and compilation of application binary along execution isn’t a problem for you. We might start with .cabal package which would include dependencies.
cabal-version: 3.0
name: web
version: 0.1.0.0
license: BSD-3-Clause
license-file: LICENSE
author: keinsell
maintainer: [email protected]
build-type: Simple
extra-doc-files: CHANGELOG.md
common warnings
ghc-options: -Wall
executable haskell
import: warnings
main-is: Main.hs
build-depends:
base ^>=4.21.0.0,
network ^>=3.2.8.0,
HTTP ^>=4000.4.1,
network-uri ^>=2.6.4.2
hs-source-dirs: app
default-language: Haskell2010
module Main (main) where
main :: IO ()
main = putStrLn "Hello World"
Hello World
We would like to open a TCP socket, bind it to port 8080 on all interfaces, and start listening for connections. We will not accept any connections yet.
module Main (main) where
import Network.Socket
( Family(AF_INET)
, SocketType(Stream)
, SockAddr(SockAddrInet)
, bind
, defaultProtocol
, listen
, socket
, tupleToHostAddress
)
main :: IO ()
main = do
lsock <- socket AF_INET Stream defaultProtocol
bind lsock (SockAddrInet 8080 (tupleToHostAddress (0, 0, 0, 0)))
listen lsock 1
putStrLn "Listening on port 8080…"
getLine >> pure () -- keep it alive until we press Enter
We have created a stream (TCP) socket for IPv4 and bound it to 0.0.0.0:8080, when we would reach our application by curl http://localhost:8080 the connection will hang because we never accepted it.
To accept connections we use forever from Control.Monad to loop indefinitely, and accept from Network.Socket to block until a client connects. When a client connects, accept returns a new socket for that connection, which we can then use to communicate with the client.
module Main (main) where
import Control.Monad (forever)
import Network.HTTP
( Request(rqURI)
, Response(..)
, close
, receiveHTTP
, respondHTTP
, socketConnection
)
import Network.Socket
( Family(AF_INET)
, SocketType(Stream)
, SockAddr(SockAddrInet)
, accept
, bind
, defaultProtocol
, listen
, socket
, tupleToHostAddress
)
import Network.URI (uriPath)
main :: IO ()
main = do
lsock <- socket AF_INET Stream defaultProtocol
bind lsock (SockAddrInet 8080 (tupleToHostAddress (0, 0, 0, 0)))
listen lsock 1
putStrLn "Listening on port 8080…"
forever $ do
(csock, _) <- accept lsock -- wait for a client
hs <- socketConnection "" 8080 csock
req <- receiveHTTP hs
case req of
Left _ -> close hs
Right r -> do
let path = uriPath (rqURI r)
resp
| path == "/" = Response (2,0,0) "OK" [] "Hello, World!\n"
| otherwise = Response (4,0,4) "Not Found" [] "Nothing here\n"
respondHTTP hs resp
close hs
Application begun accepting connections, and we can now use its functionality to return a simple static greeting string when the root URL is requested, and a 404 when a request can’t be routed to the appropriate handler.
curl http://localhost:8080/ # → Hello, World!
curl http://localhost:8080/anything # → Nothing here
There’s a long way ahead.
There’s a lot of what we didn’t touched and what have been showed up there is a drop of a water in the sea of what is left unsaid there. Before we will touch a high-level library or framework we might consider walking down at the bottom until what coming up from high-level will come to us naturally.