Request Handling

Falco exposes a uniform API to obtain typed values from IFormCollection, IQueryCollection, RouteValueDictionary, IHeaderCollection, and IRequestCookieCollection. This is achieved by means of the RequestData type and it's derivative FormData. These abstractions are intended to make it easier to work with the url-encoded key/value collections.

Take note of the similarities when interacting with the different sources of request data.

A brief aside on the key/value semantics

RequestData is supported by a recursive discriminated union called RequestValue which represents a parsed key/value collection.

The RequestValue parsing process provides some simple, yet powerful, syntax to submit objects and collections over-the-wire, to facilitate complex form and query submissions.

Key Syntax: Object Notation

Keys using dot notation are interpreted as complex (i.e., nested values) objects.

Consider the following POST request:

POST /my-form HTTP/1.1
Host: foo.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 46

user.name=john%20doe&user.email=abc@def123.com

This will be intepreted as the following RequestValue:

RObject [
    "user", RObject [
        "name", RString "john doe"
        "email", RString "abc@def123.com"
    ]
]

See form binding for details on interacting with form data.

Key Syntax: List Notation

Keys using square bracket notation are interpreted as lists, which can include both primitives and complex objects. Both indexed and non-indexed variants are supported.

Consider the following request:

GET /my-search?name=john&season[0]=summer&season[1]=winter&hobbies[]=hiking HTTP/1.1
Host: foo.example
Content-Type: application/x-www-form-urlencoded
Content-Length: 68

This will be interpreted as the following RequestValue:

RObject [
    "name", RString "john"
    "season", RList [ RString "summer"; RString "winter" ]
    "hobbies", RList [ RString "hking" ]
]

See query binding for details on interacting with form data.

Request Data Access

RequestData provides the ability to safely read primitive types from flat and nested key/value collections.

let requestData : RequestData = // From: Route | Query | Form

// Retrieve primitive options
let str : string option = requestData.TryGetString "name"
let flt : float option = requestData.TryGetFloat "temperature"

// Retrieve primitive, or default
let str : string = requestData.GetString "name"
let strOrDefault : string = requestData.GetString ("name", "John Doe")
let flt : float = requestData.GetFloat "temperature"

// Retrieve primitive list
let strList : string list = requestData.GetStringList "hobbies"
let grades : int list = requestData.GetInt32List "grades"

// Dynamic access, useful for nested/complex collections
// Equivalent to:
// requestData.Get("user").Get("email_address").AsString()
let userEmail = requestData?user?email_address.AsString()

Route Binding

Provides access to the values found in the RouteValueDictionary.

open Falco

// Assuming a route pattern of /{Name}
let manualRouteHandler : HttpHandler = fun ctx ->
    let r = Request.getRoute ctx
    let name = r.GetString "Name"
    // Or, let name = r?Name.AsString()
    // Or, let name = r.TryGetString "Name" |> Option.defaultValue ""
    Response.ofPlainText name ctx

let mapRouteHandler : HttpHandler =
    Request.mapRoute (fun r ->
        r.GetString "Name")
        Response.ofPlainText

Query Binding

Provides access to the values found in the IQueryCollection, as well as the RouteValueDictionary. In the case of matching keys, the values in the IQueryCollection take precedence.

open Falco

type Person =
    { FirstName : string
      LastName : string }

let form : HttpHandler =
    Response.ofHtmlCsrf view

let manualQueryHandler : HttpHandler = fun ctx ->
    let q = Request.getQuery ctx

    let person =
        { FirstName = q.GetString ("FirstName", "John") // Get value or return default value
          LastName  = q.GetString ("LastName", "Doe") }

    Response.ofJson person ctx

let mapQueryHandler : HttpHandler =
    Request.mapQuery (fun q ->
        let first = q.GetString ("FirstName", "John") // Get value or return default value
        let last = q.GetString ("LastName", "Doe")
        { FirstName = first; LastName = last })
        Response.ofJson

Form Binding

Provides access to the values found in he IFormCollection, as well as the RouteValueDictionary. In the case of matching keys, the values in the IFormCollection take precedence.

The FormData inherits from RequestData type also exposes the IFormFilesCollection via the _.Files member and _.TryGetFile(name : string) method.

type Person =
    { FirstName : string
      LastName : string }

let manualFormHandler : HttpHandler = fun ctx ->
    task {
        let! f : FormData = Request.getForm ctx

        let person =
            { FirstName = f.GetString ("FirstName", "John") // Get value or return default value
              LastName = f.GetString ("LastName", "Doe") }

        return! Response.ofJson person ctx
    }

let mapFormHandler : HttpHandler =
    Request.mapForm (fun f ->
        let first = f.GetString ("FirstName", "John") // Get value or return default value
        let last = f.GetString ("LastName", "Doe")
        { FirstName = first; LastName = last })
        Response.ofJson

let mapFormSecureHandler : HttpHandler =
    Request.mapFormSecure (fun f -> // `Request.mapFormSecure` will automatically validate CSRF token for you.
        let first = f.GetString ("FirstName", "John") // Get value or return default value
        let last = f.GetString ("LastName", "Doe")
        { FirstName = first; LastName = last })
        Response.ofJson
        (Response.withStatusCode 400 >> Response.ofEmpty)

multipart/form-data Binding

Microsoft defines large upload as anything > 64KB, which well... is most uploads. Anything beyond this size and they recommend streaming the multipart data to avoid excess memory consumption.

To make this process a lot easier Falco's form handlers will attempt to stream multipart form-data, or return an error message indicating the likely problem.

let imageUploadHandler : HttpHandler =
    let formBinder (f : FormData) : IFormFile option =
        f.TryGetFormFile "profile_image"

    let uploadImage (profileImage : IFormFile option) : HttpHandler =
        // Process the uploaded file ...

    // Safely buffer the multipart form submission
    Request.mapForm formBinder uploadImage

let secureImageUploadHandler : HttpHandler =
    let formBinder (f : FormData) : IFormFile option =
        f.TryGetFormFile "profile_image"

    let uploadImage (profileImage : IFormFile option) : HttpHandler =
        // Process the uploaded file ...

    let handleInvalidCsrf : HttpHandler =
        Response.withStatusCode 400 >> Response.ofEmpty

    // Safely buffer the multipart form submission
    Request.mapFormSecure formBinder uploadImage handleInvalidCsrf

JSON

These handlers use the .NET built-in System.Text.Json.JsonSerializer.

type Person =
    { FirstName : string
      LastName : string }

let jsonHandler : HttpHandler =
    Response.ofJson {
        FirstName = "John"
        LastName = "Doe" }

let mapJsonHandler : HttpHandler =
    let handleOk person : HttpHandler =
        let message = sprintf "hello %s %s" person.First person.Last
        Response.ofPlainText message

    Request.mapJson handleOk

let mapJsonOptionsHandler : HttpHandler =
    let options = JsonSerializerOptions()
    options.DefaultIgnoreCondition <- JsonIgnoreCondition.WhenWritingNull

    let handleOk person : HttpHandler =
        let message = sprintf "hello %s %s" person.First person.Last
        Response.ofPlainText message

    Request.mapJsonOption options handleOk

Next: View engine