Giraffe icon indicating copy to clipboard operation
Giraffe copied to clipboard

Asp.NET Rate Limiter middleware support

Open GallaFrancesco opened this issue 1 year ago • 8 comments

https://learn.microsoft.com/en-us/aspnet/core/performance/rate-limit?view=aspnetcore-8.0 is a rate limiting middleware implementation and I'm struggling to make this work in Giraffe. Two main reasons:

  1. I couldn't find a way to enable the rate limiter on specific endpoints using RequireRateLimiting() in a similar way as this sample Asp.NET project: https://github.com/dotnet/AspNetCore.Docs.Samples/blob/23fbafb68257b59bf6f3775fe12c51afc2f242e3/fundamentals/middleware/rate-limit/WebRateLimitAuth/Program.cs#L163

It would be ideal to support a middleware such as:

route "/limited" >=> text "This is rate limited" >=> requireRateLimiting policy

Though I could not find any example of this.

  1. I find it difficult to configure the middleware from F#, but I might be doing something wrong or excessively complicated. AddTokenBucketLimiter is failing when trying to call it from F#, with error FS0193: Type constraint mismatch. The type 'RateLimiterOptions' is not compatible with type 'unit', see the following snippet.
type SampleRateLimitOptions =
    { ReplenishmentPeriod: int
      QueueProcessingOrder: QueueProcessingOrder
      QueueLimit: int 
      TokenLimit: int 
      TokensPerPeriod: int 
      AutoReplenishment: bool }

    static member Default : SampleRateLimitOptions = // times in seconds
        { ReplenishmentPeriod = 1
          QueueProcessingOrder = QueueProcessingOrder.OldestFirst
          QueueLimit = 10
          TokenLimit = 10
          TokensPerPeriod = 4
          AutoReplenishment = true }

let configureRateLimiter (builder: WebApplicationBuilder) =
    let rateLimitOptions = SampleRateLimitOptions.Default 

    let loadRateLimiterOptions (options: TokenBucketRateLimiterOptions) =
        options.TokenLimit <- rateLimitOptions.TokenLimit
        options.QueueProcessingOrder <- QueueProcessingOrder.OldestFirst
        options.QueueLimit <- rateLimitOptions.QueueLimit
        options.ReplenishmentPeriod <- TimeSpan.FromSeconds(rateLimitOptions.ReplenishmentPeriod)
        options.TokensPerPeriod <- rateLimitOptions.TokensPerPeriod
        options.AutoReplenishment <- rateLimitOptions.AutoReplenishment

    builder
       .Services
       .AddRateLimiter(fun (r: RateLimiterOptions) -> 
           r.AddTokenBucketLimiter("token", Action<TokenBucketRateLimiterOptions> loadRateLimiterOptions)) // This does not build

GallaFrancesco avatar Oct 02 '24 10:10 GallaFrancesco

Hello @GallaFrancesco, thanks for opening this issue.

I'll add a new sample to this project showing how one can use this ASP.NET rate limiter middleware with Giraffe either this week or the next.

64J0 avatar Oct 02 '24 15:10 64J0

Sounds great, looking forward to it. Thank you!

GallaFrancesco avatar Oct 03 '24 13:10 GallaFrancesco

I created this PR right now -> https://github.com/giraffe-fsharp/Giraffe/pull/619. It adds a global rate limiting middleware sample.

I'm not sure if we can use the per-endpoint rate limiting configuration. I can take a proper look at this in the future, when I have more free time.

64J0 avatar Oct 05 '24 23:10 64J0

It may be worth tweaking this function -> https://github.com/giraffe-fsharp/Giraffe/blob/master/src/Giraffe/EndpointRouting.fs#L278.

We can eventually pattern match to the case where the user wants to add the rate limiting middleware on specific endpoints only.

64J0 avatar Oct 05 '24 23:10 64J0

Thank you @64J0 that's really handy.

I can look into modifying the Map* functions to be able to rate limit specific endpoints - the global rate limiter can be useful but having per-endpoint would allow Giraffe to support the same level of granularity allowed by Asp.NET.

GallaFrancesco avatar Oct 07 '24 13:10 GallaFrancesco

I was considering other approaches for this implementation, and maybe we can start using custom attributes on top of the handlers, like what we have for ASP.NET. So, based on these attributes (or the lack of them), we can map to the appropriate ASP.NET route configuration.

Got this idea after reading this documentation for output caching on .NET 8: https://learn.microsoft.com/en-us/aspnet/core/performance/caching/output?view=aspnetcore-8.0#configure-one-endpoint-or-page.

This code could give some ideas: https://github.com/haf/expecto/blob/main/Expecto/Model.fs#L108.

  • Related issue: https://github.com/giraffe-fsharp/Giraffe/issues/555#issue-1873648993
  • Related issue: https://github.com/dotnet/fsharp/issues/17858

64J0 avatar Oct 08 '24 14:10 64J0

I (personally) find that using attributes might be less consistent with the Giraffe approach to middlewares, while the API for rate limiting might be similar to Giraffe's authentication support: https://github.com/giraffe-fsharp/Giraffe/blob/master/DOCUMENTATION.md#requiresauthentication

I realized though that the feature request in the original issue description is wrong: the user-defined HTTP handler should always be the last in the middleware pipeline, while the rate limiter could be applied directly to the request handlers, e.g.:


let webApp =
    choose [
        route "/"     >=> text "Hello World"
        route "/limited" >=>
            requiresRateLimiting policy >=>
                choose [
                    GET  >=> getHandler
                    POST >=> submitHandler
                ]
    ]

GallaFrancesco avatar Oct 11 '24 08:10 GallaFrancesco

I wish it was that simple. In this case, since we're aiming to use the ASP.NET feature, we would need to rewrite the Giraffe routing mechanism to account for scenarios that we want to use something like app.Map(...).CacheOutput("Expire20"), or considering the rate limiting app.Map(...).RequireRateLimiting("fixed"), eventually combining those.

64J0 avatar Oct 11 '24 12:10 64J0

I updated the PR code to add a mechanism that lets you configure these ASP.NET extensions passing a list. Please let me know what you think.

64J0 avatar Oct 22 '24 22:10 64J0

Hey @GallaFrancesco, with the new alpha release you can have per-route rate limiting leveraging ASP.NET native middleware. Check this sample for more details: https://github.com/giraffe-fsharp/Giraffe/blob/master/samples/RateLimiting/Program.fs. There's a README explaining how to run it locally too.

64J0 avatar Feb 12 '25 14:02 64J0

Since there were no more interactions, and considering the new routing functions ...WithExtensions which enables fine-grained ASP.NET built-in middleware usage, I'm going to close this issue for now.

Check this sample for an example on how to use this ...WithExtensions to leverage the ASP.NET built-in rate limiting feature.

Feel free to open a new issue for further discussions.

64J0 avatar Mar 31 '25 16:03 64J0