-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add the ability to specify custom error body #5
Comments
Hi @chshersh! I am glad to hear that you are having a good experience with this library so far 😄 Thanks for raising this issue and exploring a potential solution in so much detail as well! That's very much appreciated. I will think a little about this design and then get back to you soon. In the meantime, I am wondering to what extent the I still see merit in the ability to specify custom messages or headers (e.g. to include information about how much capacity is left etc., similar to what you get from the GitHub API) though. |
@mbg Don't worry about replying urgently 🤗 This issue is not urgent as I've already implemented the above mentioned workaround. Just wanted to upstream the implementation if this is something you want to see in your library. Take as much time as you want to come up with the design that satisfies you 🙂
I could in theory circumvent this problem by implementing my own naming scheme for errors and then pattern matching on a string in the Here is the current behaviour without $ curl -XPOST -H "Content-Type: application/json" localhost:8001/ -d '{"msg": "Dmitrii"}' -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8001...
* Connected to localhost (127.0.0.1) port 8001 (#0)
> POST / HTTP/1.1
> Host: localhost:8001
> User-Agent: curl/7.81.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 18
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 429 Rate limit exceeded
< Transfer-Encoding: chunked
< Date: Thu, 19 May 2022 12:45:01 GMT
< Server: Warp/3.3.20
<
* Connection #0 to host localhost left intact And here with $ curl -XPOST -H "Content-Type: application/json" localhost:8001/ -d '{"msg": "Dmitrii"}' -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 127.0.0.1:8001...
* Connected to localhost (127.0.0.1) port 8001 (#0)
> POST / HTTP/1.1
> Host: localhost:8001
> User-Agent: curl/7.81.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 18
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 429 Rate limit exceeded
< Transfer-Encoding: chunked
< Date: Thu, 19 May 2022 12:45:31 GMT
< Server: Warp/3.3.20
< Content-Type: application/json;charset=utf-8
<
* Connection #0 to host localhost left intact
{"status":429,"error":"Rate limit exceeded"} But maybe I'm mistaken here so cc'ing @epicallan for more input! My minimal code snippet with the error reproduction using #!/usr/bin/env cabal
{- cabal:
build-depends:
, base ^>= 4.14.0.0
, aeson < 2.0
, bytestring
, servant-server
, servant-errors ^>= 0.1.7.0
, servant-rate-limit ^>= 0.2.0.0
, text
, time-units-types
, wai ^>= 3.2
, wai-rate-limit ^>= 0.3
, warp
-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
module Main where
import Data.Aeson (FromJSON, ToJSON)
import Data.ByteString (ByteString)
import Data.Proxy (Proxy(..))
import Data.Text (Text)
import Data.Time.TypeLevel
import GHC.Generics (Generic)
import Network.Wai (Application)
import Network.Wai.Handler.Warp (run)
import Network.Wai.Middleware.Servant.Errors (errorMw, HasErrorBody(..))
import Network.Wai.RateLimit.Backend
import Servant
import Servant.RateLimit
import Servant.RateLimit.Server ()
-- | A greet message data type for use as Request Body
newtype Greet = Greet { msg :: Text }
deriving (Generic, Show, FromJSON, ToJSON)
type TestApi
= RateLimit (SlidingWindow ('Second 10) 1) (IPAddressPolicy "sliding:")
:> ReqBody '[JSON] Greet
:> Post '[JSON] Greet
-- servant application
main :: IO ()
main = do
putStrLn "Starting the server..."
run 8001
$ errorMw @JSON @'["error", "status"] -- comment this line to check usage without servant-errors
$ serveWithContext
api
(backend :. EmptyContext )
handler
where
handler = return . id
api = Proxy @TestApi
-- simple backend to always throw rate limit error
backend :: Backend ByteString
backend = MkBackend
{ backendGetUsage = \_ -> pure 1000
, backendIncAndGetUsage = \_ _ -> pure 1000
, backendExpireIn = \_ _ -> pure ()
}
|
Another option would be to go the route that we wanted to go with Instead of introducing a new typeclass, we can add an (optional) error handler to Servant's haskell-servant/servant-auth#168 main = do
putStrLn "Starting the server..."
run 8001
$ serveWithContext
api
(myRateLimitHandler :. backend :. EmptyContext )
handler
where
handler = return . id
api = Proxy @TestApi
myRateLimitHandler = RateLimitHandler $ delayedFailFatal err401{}
-- simple backend to always throw rate limit error
backend :: Backend ByteString
backend = MkBackend
{ backendGetUsage = \_ -> pure 1000
, backendIncAndGetUsage = \_ _ -> pure 1000
, backendExpireIn = \_ _ -> pure ()
}
|
@chshersh: Thanks for the follow-up! It's really useful in understanding what your use case is and how withCustomServerError :: MonadCatch m => ByteString -> m a -> m a
withCustomServerError msg action = action `catch` \(err :: ServerError) ->
if errHTTPCode /= 429
then throwM err
else throwM err{ errBody = msg } Simply install this around the server handlers for the relevant endpoints in your API. If you are using this in combination with This should also allow you to adjust the headers using the @arianvp: Thank you for your suggestion and the interesting pointer to the relevant discussion for I will summarise some thoughts about different points in the design space if the above
type MyRateLimitingCfg
= SlidingWindow ('Second 10) 1
:<> IPAddressPolicy "sliding:"
:<> ErrorMessage "You've exceeded rate limit for testing"
type API = RateLimit MyRateLimitingCfg :> ... Or as a type-level list. This is essentially the approach that e.g.
type API
= IncludeRateLimitHeaders (SlidingWindow ('Second 10) 1) (IPAddressPolicy "sliding:")
:> ErrorMessageFor 421 "You've exceeded the rate limit for testing"
:> RateLimit (SlidingWindow ('Second 10) 1) (IPAddressPolicy "sliding:")
Something like
|
@mbg I appreciate such a detailed response 🤗 Thanks for providing so many alternative solutions to the problem! I will share my thoughts on possible solutions. The proposed In other words, performing the following patch on my minimal code example doesn't show run 8001
$ errorMw @JSON @'["error", "status"]
$ serveWithContext
api
(backend :. EmptyContext)
- handler
+ (withCustomServerError "My Error" . handler)
where
handler = return . id I probably can wrap my whole server in a single Currently, it looks like the three proposed solutions are not flexible enough as they only allow to catch all the exceptions in one single place globally and not per endpoint:
I agree with your thoughts on all new 5 suggestions 👍🏻 I find options 1 and 5 to be easier to implement and it'll work fine for me. For option 1, it's possible to implement it without any breaking changes. You can keep route = route @(RateLimitCustom strategy policy EmptyErrorBody :> api) So no boilerplate 🙂 For option 5, it looks less convenient to me because I have several endpoints with the same rate limit policy but I might want to have different error messages for them. So if this can only be done via the |
Hi again @chshersh. Sorry for the delay in getting back to you and sorry for suggesting something that doesn't actually work. I think your approach is the one to go for then and I would very much welcome a PR which implements it :) |
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
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 few possible instances of the
HasRateLimitErrBody
typeclass:After that, we can implement a data type similar to 'RateLimit
but with custom
errBody`.You may want to introduce a breaking change in a form of changing the existing
RateLimit
to this one or reimplementingRateLimit
astype 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 introducedRateLimitCustom
. It's almost the same as the existing instance with a few changes:If this is something you would like to have in your library, I'm happy to contribute the implementation 🤗
The text was updated successfully, but these errors were encountered: