Introduction.
Suave is a simple web development F# library providing a lightweight web server and a set of combinators to manipulate route flow and task composition. Suave is inspired in the simplicity of Happstack and born out of the necessity of embedding web server capabilities in my own applications.
Still in its early stages Suave supports HTTPS, multiple TCP/IP bindings, Basic Access Authentication, Keep-Alive. Suave also takes advantage of F# asynchronous workflows to perform non-blocking IO. In fact, Suave is written in a completely non-blocking fashion throughout.
What follows is a tutorial on how to create applications. Scroll past the tutorial to see detailed function documentation.
Tutorial: Hello World!
The simplest Suave application is a simple HTTP server that greets all visitors with the string "Hello World!"
web_server default_config (OK "Hello World!")
The above statement will start a web server on default port 8083. web_server
takes a configuration record and the webpart (OK "Hello World")
Webparts are functions with the following type: HttpRequest -> Option<Async<unit>>
. For every request web_server will evaluate the webpart, if the evaluation succeeds it will execute the resulting asynchronous computation. OK
is a combinator that always succeed and writes its argument to the underlying response stream.
Tutorial: Composing bigger programs.
Logic is expressed with the help of different combinators built around the option<HttpRequest>
type. We build webparts out of functions of type HttpRequest -> HttpRequest option
and the operator >>=
in the following way.
let simple_app _ = url "/hello" >>= OK "Hello World" ;
To select between different routes or options we use the function choose; for example:
let complex_app _ = choose [ Console.OpenStandardOutput() |> log >>= never ; url "/hello" >>= never >>= OK "Never executes" ; url "/hello" >>= OK "Hello World" ]
The function choose
accepts a list of webparts and execute each webpart in the list until one returns success. Since choose itself returns a webpart we can nest them for more complex logic.
let nested_logic _ = choose [ GET >>= choose [ url "/hello" >>= OK "Hello GET" ; url "/goodbye" >>= OK "Good bye GET" ] ; POST >>= choose [ url "/hello" >>= OK "Hello POST" ; url "/goodbye" >>= OK "Good bye POST" ] ]
To gain access to the underlying HttpRequest
and read query and http form data we can use the warbler* combinator.
let http_form _ = choose [ GET >>= url "/query" >>= warbler(fun x -> OK ("Hello " + (x.Query) ? name)) ; POST >>= url "/query" >>= warbler(fun x -> OK ("Hello " + (x.Form) ? name)) ; notfound "Found no handlers" ]
or alternatively with >>== = warbler
let http_form _ = choose [ GET >>= url "/query" >>== (fun x -> OK ("Hello " + (x.Query) ? name)) ; POST >>= url "/query" >>== (fun x -> OK ("Hello " + (x.Form) ? name)) ; notfound "Found no handlers" ]
Here is how http authentication would work:
let requires_authentication _ = choose [ GET >>= url "/public" >>= OK "Hello anonymous" //access to handlers bellow this one will require authentication ; authenticate_basic (fun x -> x.Username.Equals("foo") && x.Password.Equals("bar")) ; GET >>= url "/protected" >>== (fun x -> OK ("Hello " + x.Username)) ]
*warbler gets its name from the famous book “To Mock a Mockingbird” by Raymond Smullyan.
Typed routes
let testapp : WebPart = choose [ urlscan "/add/%d/%d" (fun (a,b) -> OK((a + b).ToString())) ; notfound "Found no handlers" ]
SSL support
Multiple bindings andSuave supports binding the application to multiple TCP/IP addresses and ports combinations. It also supports HTTPS.
let sslCert = new X509Certificate("suave.pfx","easy"); let cfg = { default_config with bindings = [ { scheme = HTTP ; ip = IPAddress.Parse "127.0.0.1" ; port = 80us } ; { scheme = HTTPS sslCert ; ip = IPAddress.Parse "192.168.13.138" ; port = 443us } ] ; timeout = TimeSpan.FromMilliseconds 3000. } choose [ Console.OpenStandardOutput() |> log >>= never // log to standard output ; url "/hello" >>= OK "Hello World" ; notfound "Found no handlers" ] |> web_server cfg
API
HTTP Verbs
Default-supportedSee RFC 2616.
/// Match on GET requests let GET (x : HttpRequest) = meth0d "GET" x /// Match on POST requests let POST (x : HttpRequest) = meth0d "POST" x /// Match on DELETE requests let DELETE (x : HttpRequest) = meth0d "DELETE" x /// Match on PUT requests let PUT (x : HttpRequest) = meth0d "PUT" x /// Match on HEAD requests let HEAD (x : HttpRequest) = meth0d "HEAD" x /// Match on CONNECT requests let CONNECT (x : HttpRequest) = meth0d "CONNECT" x /// Match on PATCH requests let PATCH (x : HttpRequest) = meth0d "PATCH" x /// Match on TRACE requests let TRACE (x : HttpRequest) = meth0d "TRACE" x /// Match on OPTIONS requests let OPTIONS (x : HttpRequest) = meth0d "OPTIONS" x
Server configuration
The first argument to web_server
is a configuration record with the following signature.
/// Gets the supported protocols, HTTP and HTTPS with a certificate type Protocol = /// The HTTP protocol is the core protocol | HTTP /// The HTTP protocol tunneled in a TLS tunnel | HTTPS of X509Certificate
/// A HTTP binding is a protocol is the product of HTTP or HTTP, a DNS or IP binding and a port number type HttpBinding = /// The scheme in use { scheme : Protocol /// The host or IP address to bind to. This will be interpreted by the operating system ; ip : IPAddress /// The port for the binding ; port : uint16 }
/// The core configuration of suave type Config = /// The bindings for the web server to launch with { bindings : HttpBinding list /// An error handler to use for handling exceptions that are /// are thrown from the web parts ; error_handler : ErrorHandler /// Timeout for responses to be generated ; timeout : TimeSpan /// A cancellation token for the web server. Signalling this token /// means that the web server shuts down ; ct : CancellationToken }
bindings array of bindings of the form protocol, ip address, port
error_handler a handler to deal with runtime errors
timeout maximun number of milliseconds before killing a request
Coding Guidelines
Style Guide
Two space indentation.
match x with // '|' characters at base of 'match' | A -> () | Bcdef -> "aligned arrows" // space after '|' character
Parameters
Let type annotations be specified with spaces after the argument symbol and before
the type.
static member FromString(scheme : string, ?cert) =
Method formatting with no spaces after/before normal parenthesis
let my_method_name first_arg (second : WithType) = async { // and monad builder return! f first_arg second } // at base of 'let' + 2 spaces
You need to document your methods with ‘///’ to create XML-doc. A XML
documentation file is generated together with the compilation and is distributed
with the NuGet so that others can read your code’s intentions easily.
Don’t put unnecessary parenthesis unless it makes the code more clear.
Testing
Run Tests as a console app. Return status code = 0 means success.