Adding Last-Modified response header to Haskell Servant API
Given the following Servant API (boilerplate redacted for brevity):
1 type MyAPI = "some-api" :> Get '[JSON] NoContent 2 3 someApi = return NoContent
How do you add a Last-Modified
header? As a first attempt, we can use the
Header
type with addHeader
and a UTCTime
:
1 import Data.Time.Clock (UTCTime, getCurrentTime) 2 3 type LastModifiedHeader = Header "Last-Modified" UTCTime 4 type MyAPI = "some-api" :> Get '[JSON] (Headers '[LastModifiedHeader] NoContent) 5 6 someApi = do 7 now <- getCurrentTime 8 addHeader now 9 return NoContent
Unfortunately, this returns the time in the wrong format!
> curl -I localhost/some-api | grep Last-Modified
Last-Modified: 2018-09-30T19:56:39Z
It should be RFC
1123. We can
fix this with a newtype
that wraps the
formatting functions available in Data.Time.Format
:
1 {-# LANGUAGE GeneralizedNewtypeDeriving #-} 2 3 import Data.ByteString (pack) 4 import Data.Time.Clock (UTCTime, getCurrentTime) 5 import Data.Time.Format (formatTime, defaultTimeLocale, rfc1123DateFormat) 6 7 newtype RFC1123Time = RFC1123Time UTCTime 8 deriving (Show, FormatTime) 9 10 instance ToHttpApiData RFC1123Time where 11 toUrlPiece = error "Not intended to be used in URLs" 12 toHeader = 13 let rfc1123DateFormat = "%a, %_d %b %Y %H:%M:%S GMT" in 14 pack . formatTime defaultTimeLocale rfc1123DateFormat 15 16 type LastModifiedHeader = Header "Last-Modified" RFC1123Time 17 type MyAPI = "some-api" :> Get '[JSON] (Headers '[LastModifiedHeader] NoContent) 18 19 someApi = do 20 now <- getCurrentTime 21 addHeader $ RFC1123Time now 22 return NoContent
> curl -I localhost/some-api | grep Last-Modified
Last-Modified: Sun, 30 Sep 2018 20:44:16 GMT
If anyone knows a simpler way, please let me know!
Irreverant technical asides
Many implementations reference RFC822 for Last-Modified
format. What gives?
RFC822 was updated by RFC1123, which only adds a few clauses to tighten up the
definition. Most importantly, it updates the year format from 2 digits to 4!
Note that
Date.Time.Format.rfc882DateFormat
is technically incorrect here, specifying a four digit year.
Data.Time.Format.RFC822
gets it right.
rfc822DateFormat
is also technically incorrect in another way: it uses the
%Z
format specifier for timezone, which produces UTC
on a UTCTime
. This
is not an allowed value! However,
RFC 2616 says
“for the purposes of HTTP, GMT is exactly equal to UTC” so GMT can safely be
hardcoded here since we know we always have a UTC time.