Routing¶
The applications we built so far had a single handler. However, most web applications will have many handlers each one for serving different HTTP methods, URL paths etc. Let us see how to achieve this in WebGear.
Alternatives¶
We know that handlers are kleisli arrows. This means that we can combine multiple handlers using the <|> operator
from Alternative type class. Thus, if we have two handlers getTime and setTime, we could create a
combined handler like this.
timeHandler = getTime <|> setTime
So, how do we decide which handler gets invoked for a given request? How do we "route" a request to a specific handler? It turns out we can do this using a middleware.
Router Monad¶
As explained in the traits section, handlers are kleisli arrows on the Router monad. However, it is
useful to look at the MonadRouter type class instead of using this monad directly.
class (Alternative m, MonadPlus m) => MonadRouter m where
-- | Mark the current route as rejected, alternatives can be tried
rejectRoute :: m a
-- | Short-circuit the current handler and return a response
errorResponse :: Response ByteString -> m a
-- | Handle an error response
catchErrorResponse :: m a -> (Response ByteString -> m a) -> m a
The rejectRoute method can be used by middlewares and handlers to flag the current route as "not matching", so
that the next route is tried. This forms the basis of routing. For example, given this code:
-- Reject the route so that alternatives will be tried
method :: MonadRouter m => Middleware' m req (Method t:req) a a
method handler = Kleisli $ \request -> do
res <- probe @(Method t) request
either (const rejectRoute) (runKleisli handler) res
timeHandler = getTime <|> setTime
getTime :: Handler req a
getTime = method @GET getTimeHandler
setTime :: Handler req a
setTime = method @POST setTimeHandler
The MonadRouter will try each route sequentially. If any of them calls rejectRoute, the next one will be
tried and the response from the first matching handler will be returned. If none of the route handlers matched, a 404
Not Found response will be returned.
The errorResponse and catchErrorResponse are used in cases where you want to indicate that the current
handler matches the route but you want to return an exceptional response. This is similar to the exception handling
mechanisms offered by the MonadError type class.
Middlewares for Routing¶
As you can see from the above description, the routing mechanism is very flexible in WebGear. Virtually any middleware
or handler can invoke rejectRoute to skip the current handler and try the next. WebGear does not assume anything
about which request attributes are used in the handler selection. That decision is left to middlewares and handlers.
However, for most use cases, you want to route based on the HTTP method and/or the URL path. WebGear provides a number of middlewares that support this common use case.
methodattempts to match an HTTP method.pathattempts to match a prefix portion of the request URL path.pathVarattempts to parse the next component from the URL path to a value viaFromHttpApiDatatype class.pathEndsucceeds only if all URL path components are already consumed bypathorpathVar
Here is how you would use them:
-- Matches a GET request on URL /v1/widgets/<widgetId>
-- where <widgetId> can be parsed as an Int
getWidget :: Handler req a
getWidget = method @GET
$ path @"/v1/widgets"
$ pathVar @"widgetId" @Int
$ pathEnd
$ getWidgetHandler
If you prefer a less verbose version, you can use template haskell quasiquoter:
getWidget :: Handler req a
getWidget = [route| GET /v1/widgets/widgetId:Int |] getWidgetHandler
This version using the route quasiquoter is equivalent to the previous one.
There is also the match quasiquoter which is similar to route but does not add the pathEnd
middleware. It is useful in cases where only a prefix of the path needs to be matched.