(This blog post is generated from a literate Haskell file. The source code alone can be seen here.)

Before we get to the code, let’s introduce the cast of characters:

module Servant.Deprecated where import Data.List

import Data.Proxy

import Data.String.Conv

import Data.Time

import GHC.TypeLits

import Network.HTTP.Date

import qualified Network.HTTP.Types as HTTP

import Network.Wai

import Servant

Automatic sunset header injection

At the core of servant lies a handful of types that describe different facets of an API and its HTTP endpoints. These include things like the path components, query params, bodies, etc. These different facets can be combined together with the help of combinators to form larger and more complicated endpoints.

Our first step is to introduce a new type that can be used to annotate API endpoints, marking them as deprecated.

In our annotation we include a date (specified at the type level using Nat s) on which we would like to drop support for our deprecated endpoint.

data Deprecated (year :: Nat) (month :: Nat) (day :: Nat)

Notice that we don’t have to include a constructor for this type because it is only going to be used at the type level.

It’s important for us to not drop support for endpoints that are still actively in use. Setting a date in the future allows us to monitor how many people are still using deprecated endpoints. More on that later.

Now that we have our annotation, we want automatically inject information in our endpoints’ response headers in order to let clients know they are hitting outdated endpoints.

servant uses a typeclass to build up routers for big APIs from small pieces. The typeclass constraint here says that if we have an API we know how to handle, then we also know how to handle a deprecated version of that api. KnownNat helps us turn type level numbers into value level number.

instance (KnownNat year

,KnownNat month

,KnownNat day

,HasServer api ctx

) => HasServer (Deprecated year month day :> api) ctx where

The ServerT type family transforms the type of an api into the type of its handler. For example, if an endpoint is described by the type Get ‘[JSON] String , then its handler might look like return “hello world" . If the type indicates a capturing segment like Capture "UserId" Int :> Get '[JSON] String , then its handler would look more like \userId -> return $ “hello user “ ++ show userId .

Deprecating an endpoint doesn’t change the type of its handler, so we return the same handler as the non-deprecated api type.

type ServerT (Deprecated year month day :> api) m = ServerT api m

We do, however, want to change the responses of the endpoint. The route function describes how requests are routed through an api, matching paths and methods and testing capture groups until a matching endpoint is found.

The result of this function is a Router env , and fortunately for us, servant provides a method tweakResponse which will allow us to inject our sunset header.

route _ ctx dlyd =

let sub = route (Proxy :: Proxy api) ctx dlyd

in tweakResponse (fmap addSunsetHeader) sub



where

Headers in servant are represented as a list, so we just add in our new header:

addSunsetHeader :: Response -> Response

addSunsetHeader = mapResponseHeaders (sunsetHeader:)

The sunset spec says that the dates should be in a particular format, so we do a little wrangling to get things in the right format:

sunsetHeader :: HTTP.Header

sunsetHeader = (“Sunset”

, formatHTTPDate $ utcToHTTPDate sunsetDate

) sunsetDate :: UTCTime

sunsetDate = UTCTime sunsetDay 0

In order to turn our type level deprecation date (of kind Nat ) into a concrete header value (of type Integer ) we use type level literal reflection utilities provided by GHC:

sunsetDay :: Day

sunsetDay = fromGregorian

(natVal $ Proxy @year)

(fromIntegral . natVal $ Proxy @month)

(fromIntegral . natVal $ Proxy @day)

Since we’re not changing the handler type, we don’t have to change the way we hoist that handler.

hoistServerWithContext _ = hoistServerWithContext (Proxy @api)

That’s all we have to change in order to start getting deprecation headers in our responses.

Testing the sunset header

We can create a little API to test this out:

type TestAPI =

Deprecated 2019 5 1 :> Get '[JSON] String

:<|> "real" :> Get '[JSON] Bool testAPI :: Server TestAPI

testAPI =

return "I'm deprecated!"

:<|> return True

In GHCI, we can then use warp to server our API:

> import qualified Network.Wai.Handler.Warp as W

> W.run 5150 $ serve @TestAPI Proxy testAPI

We can now try curling our little server to hit our deprecated endpoint

> curl localhost:5150 -i

HTTP/1.1 200 OK

Transfer-Encoding: chunked

Date: Fri, 26 Apr 2019 20:21:42 GMT

Server: Warp/3.2.22

Sunset: Tue, 01 May 2019 00:00:00 GMT

Content-Type: application/json;charset=utf-8 "I'm deprecated!"

And our non-deprecated endpoint

> curl localhost:5150/real -i

HTTP/1.1 200 OK

Transfer-Encoding: chunked

Date: Fri, 26 Apr 2019 20:22:41 GMT

Server: Warp/3.2.22

Content-Type: application/json;charset=utf-8 true

Using sunset headers in the clients

Now that clients have this info, they can help facilitate the migration to non-deprecated versions of the API.

Clients could log a warning (or an error if they are past the deprecation date). Aggregating this information could help lend confidence to a decision to remove support for an old endpoint.

For example, if 10% of clients are still on an outdated version, we’re not ready to drop support, even if we’re past the support date. It would be better to focus on getting old clients to upgrade and revisit in a month.

Another possibility would be to prompt users to upgrade to a new version if their clients are being notified that they are hitting deprecated endpoints.