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:
webApp
is anHttpHandler
;choose
is anList<HttpHandler> -> HttpHandler
;GET
is anHttpHandler
;route "/"
is anHttpHandler
;indexHandler "world"
is anHttpHandler
;>=>
, also known ascompose
, combines two HttpHandler's into... anHttpHandler
(called a "pipeline");- ... and so on.
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:
next
of typeHttpFunc
is a function that invokes the next handler in the pipeline, if there are any. In other words, if you havehandler1 >=> handler2
somewhere in yourwebApp
, andhandler1
callsnext
, it will invokehandler2
.ctx
is theHttpContext
we mentioned above.
The return value of the handler is of type Task<HttpContext option>
and it follows the conventions:
- If the return value is
Task<Some HttpContext>
, it means the pipeline is finished and no further handlers are called. I call these "terminal" handlers in this article. - If the return value is
Task<None>
, it means thatchoose
should abandon this pipeline and skip to the next one, if any. You should probably not return this outside ofchoose
.
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
}