A slightly more complex example: routing HTTP requests
Most .NET web frameworks are based on the object-oriented paradigm. This means that you have a class called something like CheesesController or CheesesModule. The methods in that class are then associated with url routes, with the most common routing methods being convention based, attribute-based (e.g. adding attributes like [Route("cheeses/{cheeseId}/taste")]
and [HttpGet]
) or some sort of global routing table.
Suave is designed to fit in with F#'s functional programming paradigm, so routing does not use any of these routing methods. Instead routing takes place using a single function, itself composed from many smaller functions. This function has the signature WebPart
:
type WebPart = HttpContext -> Async<HttpContext option>
This function has a single parameter of type HttpContext
. This is an F# record type that includes the HTTP request, the HTTP response, and a few other things. This should be pretty familiar to anyone who has done any web programming before.
A WebPart function returns an asynchronous workflow which itself ultimately returns an HttpContext option
. Asynchronous workflows are hopefully already somewhat familiar. The HttpContext option
returned by the asynchronous workflow is either Some HttpContext
record or None
, and it is the option that is used to determine routing. Here is a web server with some simple routing logic. You can ignore the >=>
operator and the exact workings of choose
for now (both are explained in more detail later), just focus on the tree-like structure of the code:
open Suave open Suave.Filters open Suave.Operators open Suave.Successful let app = choose [ GET >=> choose [ path "/hello" >=> OK "Hello GET" path "/goodbye" >=> OK "Good bye GET" ] POST >=> choose [ path "/hello" >=> OK "Hello POST" path "/goodbye" >=> OK "Good bye POST" ] ] startWebServer defaultConfig app
By using choose
we execute different logic depending on whether the request was a GET or a POST, and depending on whether the url was /hello or /goodbye. If a request matches a given path in the decision tree, choose
will return Some HttpContext
, if it doesn't, choose
will return None
. The end result is that when someone makes a request the server will walk down this tree looking for the first part that returns Some HttpContext
, and then return it to the client. If no part of the tree returns Some HttpContext
then the result is an exception. You can can also add a default route which returns a 404 page.
The server won't evalute the entire data structure for every request, only the actual decisions, so there is no need to be concerned about performance.
Handling Errors
How does the userState
work when there's an error?
Even though the web part that created the state succeed earlier in the pipeline, there's no way to get at it later. Whenever there's an exception, Suave will terminate the currently running web part and jump into the exception handler. No, your best bet is to avoid throwing exceptions and working with types and values for control flow instead. BestPractice™
module WebPart
from Suave
--------------------
type WebPart = HttpContext -> Async<HttpContext option>
Full name: temp.WebPart
--------------------
type WebPart<'a> = 'a -> Async<'a option>
Full name: Suave.WebPart.WebPart<_>
module HttpContext
from Suave.Http
--------------------
type HttpContext =
{request: HttpRequest;
runtime: HttpRuntime;
connection: Connection;
userState: Map<string,obj>;
response: HttpResult;}
member clientIp : trustProxy:bool -> sources:string list -> IPAddress
member clientPort : trustProxy:bool -> sources:string list -> Port
member clientProto : trustProxy:bool -> sources:string list -> string
member clientIpTrustProxy : IPAddress
member clientPortTrustProxy : Port
member clientProtoTrustProxy : string
member isLocal : bool
static member clientIp_ : Property<HttpContext,IPAddress>
static member clientPort_ : Property<HttpContext,Port>
static member clientProto_ : Property<HttpContext,string>
static member connection_ : Property<HttpContext,Connection>
static member isLocal_ : Property<HttpContext,bool>
static member request_ : Property<HttpContext,HttpRequest>
static member response_ : Property<HttpContext,HttpResult>
static member runtime_ : Property<HttpContext,HttpRuntime>
static member userState_ : Property<HttpContext,Map<string,obj>>
Full name: Suave.Http.HttpContext
module Async
from YoLo
--------------------
type Async
static member AsBeginEnd : computation:('Arg -> Async<'T>) -> ('Arg * AsyncCallback * obj -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit)
static member AwaitEvent : event:IEvent<'Del,'T> * ?cancelAction:(unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate)
static member AwaitIAsyncResult : iar:IAsyncResult * ?millisecondsTimeout:int -> Async<bool>
static member AwaitTask : task:Task -> Async<unit>
static member AwaitTask : task:Task<'T> -> Async<'T>
static member AwaitWaitHandle : waitHandle:WaitHandle * ?millisecondsTimeout:int -> Async<bool>
static member CancelDefaultToken : unit -> unit
static member Catch : computation:Async<'T> -> Async<Choice<'T,exn>>
static member Choice : computations:seq<Async<'T option>> -> Async<'T option>
static member FromBeginEnd : beginAction:(AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg:'Arg1 * beginAction:('Arg1 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * beginAction:('Arg1 * 'Arg2 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromBeginEnd : arg1:'Arg1 * arg2:'Arg2 * arg3:'Arg3 * beginAction:('Arg1 * 'Arg2 * 'Arg3 * AsyncCallback * obj -> IAsyncResult) * endAction:(IAsyncResult -> 'T) * ?cancelAction:(unit -> unit) -> Async<'T>
static member FromContinuations : callback:(('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T>
static member Ignore : computation:Async<'T> -> Async<unit>
static member OnCancel : interruption:(unit -> unit) -> Async<IDisposable>
static member Parallel : computations:seq<Async<'T>> -> Async<'T []>
static member RunSynchronously : computation:Async<'T> * ?timeout:int * ?cancellationToken:CancellationToken -> 'T
static member Sleep : millisecondsDueTime:int -> Async<unit>
static member Start : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions * ?cancellationToken:CancellationToken -> Task<'T>
static member StartChild : computation:Async<'T> * ?millisecondsTimeout:int -> Async<Async<'T>>
static member StartChildAsTask : computation:Async<'T> * ?taskCreationOptions:TaskCreationOptions -> Async<Task<'T>>
static member StartImmediate : computation:Async<unit> * ?cancellationToken:CancellationToken -> unit
static member StartWithContinuations : computation:Async<'T> * continuation:('T -> unit) * exceptionContinuation:(exn -> unit) * cancellationContinuation:(OperationCanceledException -> unit) * ?cancellationToken:CancellationToken -> unit
static member SwitchToContext : syncContext:SynchronizationContext -> Async<unit>
static member SwitchToNewThread : unit -> Async<unit>
static member SwitchToThreadPool : unit -> Async<unit>
static member TryCancelled : computation:Async<'T> * compensation:(OperationCanceledException -> unit) -> Async<'T>
static member CancellationToken : Async<CancellationToken>
static member DefaultCancellationToken : CancellationToken
Full name: Microsoft.FSharp.Control.Async
--------------------
type Async<'T>
Full name: Microsoft.FSharp.Control.Async<_>
Full name: Microsoft.FSharp.Core.option<_>
from Suave
from Suave
from Suave
Full name: temp.app
Full name: Suave.WebPart.choose
Full name: Suave.Filters.GET
Full name: Suave.Filters.path
Full name: Suave.Successful.OK
Full name: Suave.Filters.POST
Full name: Suave.Web.startWebServer
Full name: Suave.Web.defaultConfig