servant 0.5 released

Table of contents

Introduction

We’re happy to announce the release of servant-0.5. Quite a lot of exciting changes have happened since the last major release. A lot of things happened under the hood, but there’s also a number of changes on the surface. This will force servant users that want to upgrade to perform some changes to their code. Therefore the following section contains an upgrade guide. We hope, upgrading will go smoothly. At least when you didn’t write your own combinators it should be fairly straightforward.

The second section of this blog post contains a description of the major new features in servant-0.5.

Upgrading

servant-server

  • Instead of EitherT (from either) we now use ExceptT (from mtl), which behaves the same, but doesn’t add a dependency. Your handler’s types have to change from e.g. EitherT ServantErr IO MyType to ExceptT ServantErr IO MyType. Throwing an error used to work with e.g. left myError and that now has to be switched to throwE myError. Both ExceptT and throwE reside in Control.Monad.Trans.Except.

  • Post now returns 200. Replace Post with PostCreated if you want to stick to the old behaviour, which was returning a 201.

  • Methods returning () do not automatically return 204 anymore. Use GetNoContent (and PostNoContent, etc.) in conjunction with the NoContent type instead:

    -- Old code
    Get '[] ()
    -- New code
    GetNoContent '[JSON] NoContent
  • MatrixParam has been removed.

  • The content-type list now is invariably used to determine whether to respond – that is, even when you’re returning NoContent you need to specify matching content-types.

servant-client

  • client now takes a Manager parameter. (See Manager.)

  • MatrixParam has been removed.

servant-docs

  • ToSample now only takes one type parameter.

  • MatrixParam has been removed.

If you have your own combinators

Then the story is more complicated. The HasServer class changed quite drastically:

-- Previously
class HasServer layout config where
   type ServerT layout (m :: * -> *) :: *
   route :: Proxy layout -> Server layout -> RoutingApplication

-- Now
class HasServer layout config where
   type ServerT layout (m :: * -> *) :: *
   route :: Proxy layout -> Config config -> Delayed (Server layout) -> Router

Read below for more information.

Major Changes

Trie-based routing

Previously routing was O(n) in the number of routes, as we matched them sequentially (and indeed, bodies would sometimes be deserialized more than once). Andres Löh improved this by transforming the routing into a trie.

Proper error handling

One of the biggest embarassements for servant was the fact that in certain circumstances, the HTTP error returned was not what one would expect. For example, if a request had both the wrong method and the wrong body, the error returned would be 400 (Bad Request) rather 405 (Method Not Allowed). This was because we matched against parts of your API in the order they appeared in your API type. Andres Löh came up with an elegant solution to this problem, creating a datatype that allows checks (and their side-effects) to be delayed.

This allows for a fine-grained control over when side-effects and general HTTP logic is run. Currently there are five ‘slots’ for effects:

data Delayed c where
  Delayed :: { capturesD :: IO (RouteResult captures)
             , methodD   :: IO (RouteResult ())
             , authD     :: IO (RouteResult auth)
             , bodyD     :: IO (RouteResult body)
             , serverD   :: (captures -> auth -> body -> RouteResult c)
             } -> Delayed c

Which are run in the order in which they appear in the code. Helper functions can be used to add more effects. In practice, this means that if you are writing a combinator for authentication, for example, you can simply use addAuthCheck to schedule your action. It will only be run if the captures and methods match, but before the body is looked at.

These five slots do not exhaust the steps of handling an HTTP request. But rather than straight away implement a very large datatype for the entire HTTP decision diagram, we decided to implement what was needed for the official servant combinators. We now want to gather some experience to see, if something’s missing. So if you feel like another slot is needed, open an issue!

Context machinery

We had an issue that kept annoying us. Consider a HasServer instance for a combinator that provides a User by looking up the cookie in a database. You have to be able to somehow pass the lookup function – or at least a connection to a database – into that instance. But they must be passed in at run-time, and for fairly involved reasons there was no good way of doing so.

We added a new parameter to route that is an HList with configuration data not unlike Spock’s Context. If you are not writing HasServer instances, Context requires no changes to your code for upgrading. If you are, note that the HasServer type class has an extra type parameter.

This change should enable a variety of new combinators that were difficult or impossible previously. Note that Context is (for now at least) meant only for the configuration of combinator instances, not for the configuration of handlers (use enter for the latter).

http-api-data

The type classes FromText and ToText have been renamed, revamped, and relocated. They are now called FromHttpApiData and ToHttpApiData respectively, and exist in the new package http-api-data. This was work by Nickolay Kudasov, and includes a variety of other improvements. Most noticeably, rather than having a single [de/]serialization method, there are now ones for each of the cases for which we need the [de/]serialization. (This was based on an idea from Greg Weber.):

class FromHttpApiData a where
    {-# MINIMAL parseUrlPiece | parseQueryParam #-}
    parseUrlPiece :: Text -> Either Text a
    parseUrlPiece = parseQueryParam

    parseHeader :: ByteString -> Either Text a
    parseHeader = parseUrlPiece . decodeUtf8

    parseQueryParam :: Text -> Either Text a
    parseQueryParam = parseUrlPiece

As an added bonus, the Template Haskell for persistent generates these automatically, making using persistent with servant a lot easier.

The haddocks for the package have more information.

Simplify ToSample

ToSample now takes only one parameter. It was an annoying mistake that it ever took two, which Nickolay Kudasov fixed. Additionally, instead of having two methods (toSample and toSamples) defined in terms of one another, we now have a single one (toSamples).

data MyData = MyData Int

-- Old code
instance ToSample MyData MyData where
    toSample _ = singleSample (MyData 42)

instance ToSample MyData where
    toSamples _ = singleSample (MyData 42)

Nickolay also made a Generic default method for this class, so that now you could simply have:

data MyData = MyData Int deriving (Generic)

instance ToSample MyData

Unifying method combinators

Previously each of ‘Get’, ‘Put’, ‘Post’, etc. were entirely separate datatypes. This also meant each interpretation needed to included instances for all of them. The 0.5 release makes them instead type-synonyms, e.g.:

type Get contentTypes a = Verb 'GET 200 contentTypes a

This makes it easy to change the status code of a success response. To further facilitate that, we now provide a variety of other type synonyms for responses other than 200, for example PostCreated for 201.

Non-memoized request body

Prior to Andres Löh’s trie-based routing improvements, we had to keep the request body in memory for more-or-less the duration of the request-response cycle. This was of course far from ideal, but necessary given that we could not tell whether the request might be re-routed to a different endpoint. After the changes, we can now remove it. Note however that certain idioms, such as

type API = ReqBody '[JSON] Int :> Get '[JSON] Int
      :<|> ReqBody '[JSON] String :> Get '[JSON] String

no longer make sense – a request body that cannot be decoded as an Int will be rejected with a 400, and the second endpoint will not be tried. This accords with the behaviour of more traditional frameworks and, as Edsko de Vries showed in a blog post, no loss of expressivity is entailed by the change.

We expect this to enable streaming combinators.

Remove matrix params

Matrix params were interacting poorly with the rest of servant, because they necessitate special behaviour. We decided to remove them. If you were relying on them and would like to see them back, please let us know.

Switch from EitherT to ExceptT

Along with the rest of the world, we’ve moved to ExceptT, which is in mtl. Updating should consist of just replacing all occurrences of EitherT with ExceptT and all occurrences of left with throwE.

Manager and BaseUrl as an argument

Previously the http-client Manager – which is used to issue http requests – was created (with unsafePerformIO) by the servant-client library itself. This meant that it was impossible to configure the Manager. Now the Manager is passed in as an argument to client:

-- Old code
endpoint1 :<|> endpoint2 = client api
-- endpoint1 :: BaseUrl -> X
-- New code
endpoint1 :<|> endpoint2 = client api baseUrl manager
-- endpoint1 :: X

We’re not entirely happy with this solution, since sometimes you want the endpoint1 functions to take these two arguments instead (for example, if you would like to distribute client functions as a library). But it is certainly an improvement.

NoContent and status codes

Previously we were much too smart about HTTP status codes. In particular, when the type of the response was (), we would return 204 (No Content). This was sometimes convenient, but generally more trouble than it was worth – some people wanted 200 codes anyhow, some people pointed out that 205 (Reset Content) was also often a sensible response, and moreover, () was too generally-used a type to convey the appropriate semantics. We now have a dedicated NoContent type for that, and the status code is up to you to decide (by default it will continue to be 200).

NOTE: additionally, and for many of the same reasons, Post now returns an HTTP 200 on success (rather than 201). Use PostCreated if you would like the old behaviour.

servant-js

The javascript codegen story has been considerably improved. Not only is there jquery-based codegen, but also Angular, Axios and vanilla (xhr-based) codegen. freezeboy wrote a separate post going into more details about how all of it works – stay tuned for it.

Furthermore, we have extracted common functionality for code-generation into servant-foreign. During this change, we have switched from String to the text-package. So if you were using servant-jquery before and are now switching to servant-js, take into account that we do not use the String datatype anymore.

servant-foreign

There has been a proliferation of code-generation libraries for servant. With this in mind, Denis Redozubov wrote servant-foreign is a library purposed to address the common code-generation needs. It derives the term-level endpoints descriptions from the servant typelevel API DSL. Once you have this, it’s easy to implement code-generation for any target language. In the nutshell it allows you to generate http api clients for servant servers and incorporate that into your build/CI process. servant-js and some other libraries use servant-foreign under the hood to achieve this.

Auth

One of the most drawn out discussions and PRs in servant has been Auth. Aaron Levin was patient enough to work through several ideas as all of us explored the design space. We currently have two auth combinators: a BasicAuth combinator, and a still somewhat experimental but more general AuthProtect combinator.

Basic Auth

(NOTE: Basic Auth sends text in the clear, so only use this over HTTPS!)

An API protected by Basic Auth in servant looks like this:

data User = ...
type API = BasicAuth "my-realm" User :> PrivateAPI

Where User is an application-specific datatype that your handlers will receive as an argument in case authentication succeeds.

servant needs to know how to generate a User from a username and password string. For that, we use the new Context mechanism.

serverContext :: Context (BasicAuthCheck User ': '[])
serverContext = authCheck :. EmptyContext

authCheck :: BasicAuthCheck User
authCheck =
  let check (BasicAuthData username password) =
        if username == "servant" && password == "server"
        then return (Authorized (User "servant"))
        else return Unauthorized
  in BasicAuthCheck check

Documentation

Sönke Hahn has done a lot of work improving our tutorial and documentation. We have moved to Read the Docs, which should make it easy to provide documentation for multiple versions of servant. We have also moved towards literate Haskell, which has already improved the quality of the documentation.

Release Management

servant-0.5 was a long time in the making. We initially hoped to cut a release last summer. In the future we hope that we can move to a much more aggressive release managament. So we hope servant-0.6 is coming soon, with a lot less changes.

Conclusion

We hope you enjoy the new release. It was certainly lots of fun to work on servant-0.5. We’d like to say thanks to all the people that helped, big or small. Happy hacking.

Posted on March 19, 2016 by The servant team