Anatomy of a Giraffe HttpHandler

2022-01-10

Introduction

HttpHandler is undoubtedly the most important type in Giraffe. This article aims to be a bottom-up, step-by-step introduction for beginners, with plenty of examples. Please consider it a supplement to the "Fundamentals" section of the official Giraffe documentation.

This is second in a series where I will build a fully-functional project in Giraffe. Please also check out my first article, which covers the ASP.NET Core background necessary for Giraffe programming.

Getting to Know HttpHandler

Somewhere in Program.fs generated by the Giraffe template, you'll find the heart of your application:

let webApp =
    choose [
        GET >=>
            choose [
                route "/" >=> indexHandler "world"
                routef "/hello/%s" indexHandler
            ]
        setStatusCode 404 >=> text "Not Found" ]

Hopefully, you can get a rough idea of what this thing does. For example, the inner choose somehow selects the correct "route" based on the request URL, and invokes indexHandler. If a GET request doesn't match either of the routes, or if it's not a GET request at all, it sets status code 404 and returns the content "Not Found".

After admiring how succinct and readable this syntax is, let's look at some of types involved:

Are you starting to see a pattern? Yes, everything revolves around the HttpHandler type, so let's examine it in detail. There are three main types involved:

type HttpFuncResult = Task<HttpContext option>
type HttpFunc       = HttpContext -> HttpFuncResult
type HttpHandler    = HttpFunc -> HttpContext -> HttpFuncResult
                      // which is the same as:
                    = HttpFunc -> HttpContext -> Task<HttpContext option>

Note that HttpContext is Microsoft.AspNetCore.Http, the same one that C# applications would use. It has all the information you need regarding the incoming HTTP request, the HTTP response that is being built, as well as the application environment. Giraffe extends this class with many useful functions to make it more functional; take a look at the source code more. Note that the default Giraffe template does not have an open for this namespace, which you should add, so you can be more explicit in your function signatures.

This function signature makes much more sense with an example. Below is the basic template for writing your own handler:

let handler : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        task {
            // Write your code here!

            // return some value of type Task<HttpContext option>
            return! ... 
        }

Actually, the task {} computation expression is not strictly necessary if you aren't doing anything asynchronous. I only include it above since, in my opinion, most interesting HTTP handlers would do something asynchronous. (I'll include both versions where relevant). It could also be written as:

let handler : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        // Write your code here!

        // return some value of type Task<HttpContext option>

Of course, you could leave out some of the type specifications... you don't need to specify the types of next and ctx because F# will be smart enough to figure it out. I'll leave the types there for the sake of clarity for now, though.

The function takes two parameters:

The return value of the handler is of type Task<HttpContext option> and it follows the conventions:

Pipelines

A "pipeline" is an informal term used to describe an HttpHandler built out of other handlers. The two primary ways of combining handlers are sequential composition, using the compose function or >=> operator, and choose. All of the following expressions are of type HttpHandler:

handler1 >=> handler2
handler1 >=> handler2 >=> handler3
choose [ GET ; HEAD ] 
choose [ GET ; HEAD ] >=> route "/" >=> handler

In a pipeline, any given handler can choose to continue with the pipeline, terminate it, or skip the pipeline entirely to move onto the next choice in choose. choose tries each of its pipelines, one by one in order, until one them terminates successfully. The mechanism for these behaviors are described in a later section.

It does not always make sense to combine two handlers, however. For example, the response header cannot be modified after starting the body, so trying to setHttpHeader after writing any content would throw an error. Further, since handlers sewn together with composition are processed in order, authentication, authorization, and similar handlers must come before any content is served.

Similarly, since choose processes its choices sequentially, you have to specify the most-specific handlers first. For example, in the following examples, the latter choices would never be executed:

route "/" >=> 
    choose [
        choose [ GET ; HEAD ] => indexHandler
        GET => neverExecuted ]

GET >=> 
    choose [
        routef "/hello/%s" => indexHandler
        route "/hello/world" => neverExecuted ]

Writing Terminal Handlers

Before digging deeper, let's write something useful: a handler that sets the status code to 404, sets a header, and stops the pipeline:

// synchronous version
let stopRightThere (rejectionReason : string) : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        ctx.SetHeader("X-Rejection-Reason", rejectionReason)
        ctx.SetStatusCode 400
        Task.FromResult (Some ctx)

// asynchronous version
let stopRightThereAsync (rejectionReason : string) : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        task { 
            ctx.SetHeader("X-Rejection-Reason", rejectionReason)
            ctx.SetStatusCode 400
            // do some asynchronous stuff here, like logging this incident to a database.
            return! Task.FromResult (Some ctx)
        }

When we compose a pipeline involving this function, it will not proceed any further:

someHandler1 >=> stopRightThere >=> handlerThatNeverRuns

Similarly, here's a handler that writes a string to the body. It is a reimplementation of the built-in text handler:

// synchronous version
let myText (msg: string) : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        ctx.WriteStringAsync(msg)

// asynchronous version
let myTextAsync (msg: string) : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        task {
            // do some asynchronous stuff here, like retrieving stuff from a database
            return! ctx.WriteStringAsync(msg)
        }

We do not need to explicitly return Task.FromResult (Some ctx) from this function because that's already what it returns. json is implementation in a similar fashion.

Continuing with next

We've written two terminal handlers, meant to be used at the end of pipelines. However, in order to compose together multiple handlers, we'll need the rest of the handlers to be 'intermediary' handlers.

That's where the next function comes in. Recall that next is:

type HttpFunc = HttpContext -> Task<HttpContext option>

It's a function that accepts a context, and returns the result of the next HttpHandler in the pipeline. Here's a function that set's a header message and proceeds to the next handler (I'm only showing either the synchronous or asynchronous version from now):

let SetHeaderAndContinue : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        ctx.SetHeader("X-Special-Message", "You are the next contestant on the Price is Right!")
        // calls next handler and returns its return value
        next ctx

And here's a function that checks whether the method is a PATCH or PUT request:

let PatchOrPut : HttpHandler = 
    fun next ctx ->
        if HttpMethods.IsPatch ctx.Request.Method || HttpMethods.IsPut ctx.Request.Method 
        then
            next ctx // continue
        else
            Task.FromResult None

HttpMethods is, again, in the Microsoft.AspNetCore.Http namespace. Notice I omitted the types for next and ctx, since those are inferred from the HttpHandler-ness of the entire expression value.

PatchOrPut returns None when the request does not have the correct method type, and indicates to choose to move on. So, if you had a handler named customerUpdateHandler, and you wanted it to handle both method types, you could do something like:

route "/customers" >=>
    choose [
        PatchOrPut >=> customerUpdateHandler
        GET >=> customerQueryHandler
        /* ... other handlers ... */
    ]

By the way, this is exactly the way that the built-in GET, HEAD, POST, etc are written.

The route family of function is implemented in a similar fashion: grab the request path from the context, next ctx if it matches it against a certain pattern, or Task.FromResult None if not. See the source, it's not scary at all. (Note that in the source, there is a helper variable for Task.FromResult None called skipPipeline).

Calling another handler

So far we know how to write handlers that finish pipelines, those that continue the pipeline, and those that skip the pipeline all together. In this section, we'll look at how to use other handlers directly from another handler (as opposed to using composition and calling next).

One built-in handler you'll see often is setStatusCode of type int -> HttpHandler. Generally, you'd use this function as part of a pipeline like:

POST >=> route "/customer" >=> setStatusCode 201 >=> handleNewCustomer

We can use this handler within another handler as well (note that we've been using ctx.SetStatusCode until now, which is an alternative way of doing the same thing):

let setStatusCode2 HttpHandler = 
    fun next ctx ->
        setStatusCode code next ctx

In the return statements above, we are passing next and ctx to the handler, instead of invoking next ctx directly. First, notice that this makes sense judging from the type... setStatusCode is obj -> HttpFunc -> HttpContext -> Task<HttpContext option>. Second, by passing next and ctx, we're telling setStatusCode to call it for us when it's done with its business.

Of course, we can make the handler we use within another handler as complicated as we like:

let setStatusCode3 : HttpHandler = 
    fun next ctx ->
        (setHttpHeader "X-Foo" "Bar" >=> setStatusCode code) next ctx

One trick illustrated in the official documentation is:

let earlyReturn : HttpFunc = Some >> Task.FromResult

let checkUserIsLoggedIn : HttpHandler =
    fun (next : HttpFunc) (ctx : HttpContext) ->
        if isNotNull ctx.User && ctx.User.Identity.IsAuthenticated
        then next ctx
        else setStatusCode 401 earlyReturn ctx

setStatusCode will call the HttpFunc it was given, in this case earlyReturn instead of next. This will be called with ctx, which evaluates to Task<Some ctx>, which means the pipeline will end.

Another built-in handler that you'll use often (esp. if you're making an API server) is json, which has type obj -> HttpHandler. It will accept any object, try to serialize it to json, set the Content-Type to application/json and write the data. You can use the handler directly in a pipeline like:

route "/" >=> json {| Greeting = "hello world |}

From within handler, you'd do something like:

// synchronous
let handleLookup : HttpHandler =
    fun next ctx ->
        let result = /* some synchronous function based on input data from forms, route params, json body, etc */
        json result next ctx

// asynchronous
let handleLookup : HttpHandler =
    fun next ctx ->
        task {
            let result = /* some asynchronous db query function */
            return! json result next ctx
        }