-
Notifications
You must be signed in to change notification settings - Fork 3
Description
First of all, thanks for writing this great library! π
It works like clockwork π
While using servant-rate-limit
, I stumbled on issue about not being able to specify custom error body when the exception is thrown. It's empty by default and this results in some JSON parsing errors. And this is hardcoded here:
wai-rate-limit/servant-rate-limit/src/Servant/RateLimit/Server.hs
Lines 59 to 64 in 4fca1af
unless allowRequest $ delayedFailFatal $ ServerError{ | |
errHTTPCode = 429, | |
errReasonPhrase = "Rate limit exceeded", | |
errBody = "", | |
errHeaders = [] | |
} |
It would be great to be able to specify custom error body (and headers). Here is the design I came up with.
The main idea is to introduce a typeclass that allows to return a custom error body and list of headers based on request.
-- | A typeclass for types that tell how to produce error body and relevant headers from 'Request' in
-- errors with the 429 code (Rate Limit Exceeded).
class HasRateLimitErrBody err where
getErrBody :: Request -> IO err
mkErrBody :: err -> Request -> (ByteString, [Header])
A few possible instances of the HasRateLimitErrBody
typeclass:
-- | Empty error body with no text and headers
data EmptyErrorBody = EmptyErrorBody
instance HasRateLimitErrBody EmptyErrorBody where
getErrBodySetter _ = pure EmptyErrorBody
mkErrBody _ _ = ("", []) -- to emulate the existing behaviour of 'RateLimit'
-- | Simple hardcoded error body as JSON
data SimpleJsonErrBody = SimpleJsonErrBody
instance HasRateLimitErrBody SimpleJsonErrBody where
getErrBodySetter _ = pure SimpleJsonErrBody
mkErrBody _ _ =
let errBody = Aeson.encode $
Aeson.object
[ ( "message",
"We received too many requests from your device in a short time, please try again in a few minutes."
)
]
headers = [(hContentType, renderHeader $ contentType (Proxy @JSON))]
in (errBody, headers)
After that, we can implement a data type similar to 'RateLimitbut with custom
errBody`.
data RateLimitCustom errBody strategy policy
You may want to introduce a breaking change in a form of changing the existing RateLimit
to this one or reimplementing RateLimit
as type RateLimit = RateLimitCustom EmptyErrorBody
but I personally tend to avoid introducing breaking changes (at least unless there's a clear migration guide).
After that, it's possible to implement a HasServer
instance for newly introduced RateLimitCustom
. It's almost the same as the existing instance with a few changes:
instance
( HasServer api ctx
, HasContextEntry ctx (Backend key)
+ , HasRateLimitErrBody errBody,
, HasRateLimitStrategy strategy
, HasRateLimitPolicy policy
, key ~ RateLimitPolicyKey policy
- ) => HasServer (RateLimit strategy policy :> api) ctx
+ ) => HasServer (RateLimitCustom errBody strategy policy :> api) ctx
where
type ServerT (RateLimit strategy policy :> api) m = ServerT api m
hoistServerWithContext _ pc nt s =
hoistServerWithContext (Proxy :: Proxy api) pc nt s
route _ context subserver = do
-- retrieve the backend from the Servant context
let backend = getContextEntry context
-- retrieve the rate-limiting policy used to identify clients
let policy = policyGetIdentifier @policy
-- retrieve the rate-limiting strategy used to limit access
let strategy = strategyValue @strategy @key backend policy
let rateCheck = withRequest $ \req -> do
-- apply the rate-limiting strategy to the request
allowRequest <- liftIO $ strategyOnRequest strategy req
-- fail if the rate limit has been exceeded
unless allowRequest $ do
+ errBodySetter <- liftIO $ getErrBodySetter @errBody req
+ let (customErrBody, customHeaders) = mkErrBody errBodySetter req
delayedFailFatal $ ServerError{
errHTTPCode = 429,
errReasonPhrase = "Rate limit exceeded",
- errBody = "",
+ errBody = customErrBody,
- errHeaders = []
+ errHeaders = customHeaders
}
-- add the check for whether the rate limit has been exceeded to the
-- server and return it
route (Proxy :: Proxy api) context $
subserver `addAcceptCheck` rateCheck
If this is something you would like to have in your library, I'm happy to contribute the implementation π€