How to throttle requests in a Web Api?
Asked Answered
W

11

60

I'm trying to implement request throttling via the following:

Best way to implement request throttling in ASP.NET MVC?

I've pulled that code into my solution and decorated an API controller endpoint with the attribute:

[Route("api/dothis/{id}")]
[AcceptVerbs("POST")]
[Throttle(Name = "TestThrottle", Message = "You must wait {n} seconds before accessing this url again.", Seconds = 5)]
[Authorize]
public HttpResponseMessage DoThis(int id) {...}

This compiles but the attribute's code doesn't get hit, and the throttling doesn't work. I don't get any errors though. What am I missing?

Woodchuck answered 28/12, 2013 at 17:25 Comment(0)
G
53

You seem to be confusing action filters for an ASP.NET MVC controller and action filters for an ASP.NET Web API controller. Those are 2 completely different classes:

It appears that what you have shown is a Web API controller action (one that is declared inside a controller deriving from ApiController). So if you want to apply custom filters to it, they must derive from System.Web.Http.Filters.ActionFilterAttribute.

So let's go ahead and adapt the code for Web API:

public class ThrottleAttribute : ActionFilterAttribute
{
    /// <summary>
    /// A unique name for this Throttle.
    /// </summary>
    /// <remarks>
    /// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
    /// </remarks>
    public string Name { get; set; }

    /// <summary>
    /// The number of seconds clients must wait before executing this decorated route again.
    /// </summary>
    public int Seconds { get; set; }

    /// <summary>
    /// A text message that will be sent to the client upon throttling.  You can include the token {n} to
    /// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
    /// </summary>
    public string Message { get; set; }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var key = string.Concat(Name, "-", GetClientIp(actionContext.Request));
        var allowExecute = false;

        if (HttpRuntime.Cache[key] == null)
        {
            HttpRuntime.Cache.Add(key,
                true, // is this the smallest data we can have?
                null, // no dependencies
                DateTime.Now.AddSeconds(Seconds), // absolute expiration
                Cache.NoSlidingExpiration,
                CacheItemPriority.Low,
                null); // no callback

            allowExecute = true;
        }

        if (!allowExecute)
        {
            if (string.IsNullOrEmpty(Message))
            {
                Message = "You may only perform this action every {n} seconds.";
            }

            actionContext.Response = actionContext.Request.CreateResponse(
                HttpStatusCode.Conflict, 
                Message.Replace("{n}", Seconds.ToString())
            );
        }
    }
}

where the GetClientIp method comes from this post.

Now you can use this attribute on your Web API controller action.

Gobioid answered 28/12, 2013 at 17:34 Comment(3)
Awesome! I believe I have to change this.Request to just request in the GetClientIp method, right? Otherwise I get 'cannot resolve symbol'. This is a huge help, thank you very much.Woodchuck
It should be GetClientIp(actionContext.Request).Gobioid
I'm sorry, I meant inside the method definition, not it's usage. Line 10 in the referenced code.Woodchuck
B
64

The proposed solution is not accurate. There are at least 5 reasons for it.

  1. The cache does not provide interlocking control between different threads, therefore multiple requests can be process at the same time introducing extra calls skipping through the throttle.
  2. The Filter is being processed 'too late in the game' within web API pipeline, so lots of resources are being spent before you decide that request should not be processed. The DelegatingHandler should be used because it can be set to run at the beginning of the Web API pipeline and cutting off the request prior doing any additional work.
  3. The Http cache itself is dependency that might not be available with new runtimes, like self-hosted options. It is best to avoid this dependency.
  4. Cache in the above example does not guarantee its survival between the calls as it might be removed due to memory pressure, especially being low priority.
  5. Although it is not too bad issue, setting response status to 'conflict' does not seem to be the best option. It is better to use '429-too many requests' instead.

There are many more issues and hidden obstacles to solve while implementing the throttling. There are free open source options available. I recommend to look at https://throttlewebapi.codeplex.com/, for example.

Barbrabarbuda answered 20/5, 2014 at 2:38 Comment(3)
+1 for not reinventing the wheel. I'm currently evaluation nuget.org/packages/WebApiThrottle which looks promising.Mauritius
github.com/stefanprodan/WebApiThrottle I believe is the github for the project. I myself first implemented the accepted solutions, and it was entertaining to learn them, but they lack so many wanted features. There is no reason to re invent the wheel, this module is topAftershock
I have implemented webapithrottle per the github instructions but it is not working for any of the webmethods in my webservice. Is there more to the implementation than setting up the general throttlehandler in the webapiconfig? I also added enablethrottling in the webservice.Lunular
J
54

WebApiThrottle is quite the champ now in this area.

It's super easy to integrate. Just add the following to App_Start\WebApiConfig.cs:

config.MessageHandlers.Add(new ThrottlingHandler()
{
    // Generic rate limit applied to ALL APIs
    Policy = new ThrottlePolicy(perSecond: 1, perMinute: 20, perHour: 200)
    {
        IpThrottling = true,
        ClientThrottling = true,
        EndpointThrottling = true,
        EndpointRules = new Dictionary<string, RateLimits>
        { 
             //Fine tune throttling per specific API here
            { "api/search", new RateLimits { PerSecond = 10, PerMinute = 100, PerHour = 1000 } }
        }
    },
    Repository = new CacheRepository()
});

It's available as a nuget too with the same name.

Juliojulis answered 12/6, 2016 at 12:27 Comment(7)
Do you have any idea about how to White-List the IP at API level in WebApiThrottling?Trantrance
Where's the upvote love @maheshsharma ? :) for your question, check github.com/stefanprodan/…Juliojulis
I have gone through with this link but I don't understand how would make it for specific apiTrantrance
I don't think whitelisting IP per API is supported. You may whitelist using API Key as alternative to IP? ClientWhitelist = new List<string> { "admin-key" }Juliojulis
or use customkeys github.com/stefanprodan/… ClientRules = new Dictionary<string, RateLimits> { { "api-client-key-1", new RateLimits { PerMinute = 40, PerHour = 400 } }, { "api-client-key-9", new RateLimits { PerDay = 2000 } } }Juliojulis
This is not working for me. My api calls are webmethods inside of a webservice. Can someone share a full example?Lunular
IpWhitelist = new List<string> { "::1", "192.168.0.0/24" },-- > for ip white listMechanotherapy
G
53

You seem to be confusing action filters for an ASP.NET MVC controller and action filters for an ASP.NET Web API controller. Those are 2 completely different classes:

It appears that what you have shown is a Web API controller action (one that is declared inside a controller deriving from ApiController). So if you want to apply custom filters to it, they must derive from System.Web.Http.Filters.ActionFilterAttribute.

So let's go ahead and adapt the code for Web API:

public class ThrottleAttribute : ActionFilterAttribute
{
    /// <summary>
    /// A unique name for this Throttle.
    /// </summary>
    /// <remarks>
    /// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
    /// </remarks>
    public string Name { get; set; }

    /// <summary>
    /// The number of seconds clients must wait before executing this decorated route again.
    /// </summary>
    public int Seconds { get; set; }

    /// <summary>
    /// A text message that will be sent to the client upon throttling.  You can include the token {n} to
    /// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
    /// </summary>
    public string Message { get; set; }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var key = string.Concat(Name, "-", GetClientIp(actionContext.Request));
        var allowExecute = false;

        if (HttpRuntime.Cache[key] == null)
        {
            HttpRuntime.Cache.Add(key,
                true, // is this the smallest data we can have?
                null, // no dependencies
                DateTime.Now.AddSeconds(Seconds), // absolute expiration
                Cache.NoSlidingExpiration,
                CacheItemPriority.Low,
                null); // no callback

            allowExecute = true;
        }

        if (!allowExecute)
        {
            if (string.IsNullOrEmpty(Message))
            {
                Message = "You may only perform this action every {n} seconds.";
            }

            actionContext.Response = actionContext.Request.CreateResponse(
                HttpStatusCode.Conflict, 
                Message.Replace("{n}", Seconds.ToString())
            );
        }
    }
}

where the GetClientIp method comes from this post.

Now you can use this attribute on your Web API controller action.

Gobioid answered 28/12, 2013 at 17:34 Comment(3)
Awesome! I believe I have to change this.Request to just request in the GetClientIp method, right? Otherwise I get 'cannot resolve symbol'. This is a huge help, thank you very much.Woodchuck
It should be GetClientIp(actionContext.Request).Gobioid
I'm sorry, I meant inside the method definition, not it's usage. Line 10 in the referenced code.Woodchuck
C
6

Double check the using statements in your action filter. As you're using an API controller, ensure that you are referencing the ActionFilterAttribute in System.Web.Http.Filters and not the one in System.Web.Mvc.

using System.Web.Http.Filters;
Cobblestone answered 28/12, 2013 at 17:29 Comment(1)
Ah, yeah that'll do it. Though this introduces a lot of errors because everything was depending on ActionExecutingContext, which I believe now needs to be HttpActionContext - working through it now. Thank you!Woodchuck
P
3

I am using ThrottleAttribute to limit the calling rate of my short-message sending API, but I found it not working sometimes. API may been called many times until the throttle logic works, finally I am using System.Web.Caching.MemoryCache instead of HttpRuntime.Cache and the problem seems to solved.

if (MemoryCache.Default[key] == null)
{
    MemoryCache.Default.Set(key, true, DateTime.Now.AddSeconds(Seconds));
    allowExecute = true;
}
Panhellenism answered 9/6, 2015 at 9:59 Comment(0)
B
3

For .NET Core you can use the AspNetCoreRateLimit nuget package (which is a port from WebApiThrottle by the same dev).

There's a well documented setup page: https://github.com/stefanprodan/AspNetCoreRateLimit/wiki/IpRateLimitMiddleware#setup

Baugher answered 20/9, 2021 at 6:50 Comment(0)
E
2

My 2 cents is add some extra info for 'key' about the request info on parameters, so that different paramter request is allowed from the same IP.

key = Name + clientIP + actionContext.ActionArguments.Values.ToString()

Also, my little concern about the 'clientIP', is it possible that two different user use the same ISP has the same 'clientIP'? If yes, then one client my be throttled wrongly.

Evident answered 7/3, 2016 at 9:41 Comment(0)
S
2

For WebAPI use this:

using Microsoft.Owin;
using System;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Caching;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

namespace MyProject.Web.Resources
{
    public enum TimeUnit
    {
        Minute = 60,
        Hour = 3600,
        Day = 86400
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
    public class ThrottleAttribute : ActionFilterAttribute
    {
        public TimeUnit TimeUnit { get; set; }
        public int Count { get; set; }

        public override void OnActionExecuting(HttpActionContext filterContext)
        {
            var seconds = Convert.ToInt32(TimeUnit);

            var key = string.Join(
                "-",
                seconds,
                filterContext.Request.Method,
                filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
                filterContext.ActionDescriptor.ActionName,
                GetClientIpAddress(filterContext.Request)
            );

            // increment the cache value
            var cnt = 1;
            if (HttpRuntime.Cache[key] != null)
            {
                cnt = (int)HttpRuntime.Cache[key] + 1;
            }
            HttpRuntime.Cache.Insert(
                key,
                cnt,
                null,
                DateTime.UtcNow.AddSeconds(seconds),
                Cache.NoSlidingExpiration,
                CacheItemPriority.Low,
                null
            );

            if (cnt > Count)
            {
                filterContext.Response = new HttpResponseMessage
                {
                    Content = new StringContent("You are allowed to make only " + Count + " requests per " + TimeUnit.ToString().ToLower())
                };
                filterContext.Response.StatusCode = (HttpStatusCode)429; //To Many Requests
            }
        }

        private string GetClientIpAddress(HttpRequestMessage request)
        {
            if (request.Properties.ContainsKey("MS_HttpContext"))
            {
                return IPAddress.Parse(((HttpContextBase)request.Properties["MS_HttpContext"]).Request.UserHostAddress).ToString();
            }
            if (request.Properties.ContainsKey("MS_OwinContext"))
            {
                return IPAddress.Parse(((OwinContext)request.Properties["MS_OwinContext"]).Request.RemoteIpAddress).ToString();
            }
            return String.Empty;
        }
    }
}
Schechter answered 24/2, 2021 at 4:18 Comment(0)
R
2

You can use this code

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class RateLimitAttribute : ActionFilterAttribute
{
    public int Seconds { get; set; }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        var key =
            $"{actionContext.ActionDescriptor.ControllerDescriptor.ControllerName}-{actionContext.ActionDescriptor.ActionName}-{actionContext.ControllerContext.RequestContext.Principal.Identity.Name}";
        var allowExecute = false;

        if (HttpRuntime.Cache[key] == null)
        {
            HttpRuntime.Cache.Add(key,
                true,
                null,
                DateTime.Now.AddSeconds(Seconds),
                Cache.NoSlidingExpiration,
                CacheItemPriority.Low,
                null);
            allowExecute = true;
        }

        if (!allowExecute)
        {
            actionContext.Response.Content = new StringContent($"سرویس های اسکنر را تنها می توانید هر {Seconds} استفاده کنید");
            actionContext.Response.StatusCode = HttpStatusCode.Conflict;
        }

        base.OnActionExecuting(actionContext);
    }
}
Raccoon answered 24/6, 2021 at 8:21 Comment(0)
C
1

It is very easily solved in .NET Core. In this case, I used IMemoryCache, which is 'in-memory per service'. However, if you want it based on Redis e.g. just change the interface to IDistributedCache… (make sure you configure Redis of course)

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using System;
using System.Net;

namespace My.ActionFilters
{
    /// <summary>
    /// Decorates any MVC route that needs to have client requests limited by time.
    /// </summary>
    /// <remarks>
    /// Uses the current System.Web.Caching.Cache to store each client request to the decorated route.
    /// </remarks>
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class ThrottleFilterAttribute : ActionFilterAttribute
    {
        public ThrottleFilterAttribute()
        {

        }
        /// <summary>
        /// A unique name for this Throttle.
        /// </summary>
        /// <remarks>
        /// We'll be inserting a Cache record based on this name and client IP, e.g. "Name-192.168.0.1"
        /// </remarks>
        public string Name { get; set; }

        /// <summary>
        /// The number of seconds clients must wait before executing this decorated route again.
        /// </summary>
        public int Seconds { get; set; }

        /// <summary>
        /// A text message that will be sent to the client upon throttling.  You can include the token {n} to
        /// show this.Seconds in the message, e.g. "Wait {n} seconds before trying again".
        /// </summary>
        public string Message { get; set; }

        public override void OnActionExecuting(ActionExecutingContext c)
        {
             var memCache = (IMemoryCache)c.HttpContext.RequestServices.GetService(typeof(IMemoryCache));
        var testProxy = c.HttpContext.Request.Headers.ContainsKey("X-Forwarded-For");
        var key = 0;
        if (testProxy)
        {
            var ipAddress = IPAddress.TryParse(c.HttpContext.Request.Headers["X-Forwarded-For"], out IPAddress realClient);
            if (ipAddress)
            {
                key = realClient.GetHashCode(); 
            }
        }
        if (key != 0)
        {
            key = c.HttpContext.Connection.RemoteIpAddress.GetHashCode();
        }
         memCache.TryGetValue(key, out bool forbidExecute);

        memCache.Set(key, true, new MemoryCacheEntryOptions() { SlidingExpiration = TimeSpan.FromMilliseconds(Milliseconds) });

        if (forbidExecute)
        {
            if (String.IsNullOrEmpty(Message))
                Message = $"You may only perform this action every {Milliseconds}ms.";

            c.Result = new ContentResult { Content = Message, ContentType = "text/plain" };
            // see 409 - http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
            c.HttpContext.Response.StatusCode = StatusCodes.Status409Conflict;
        }
    }
    }
}
Cheek answered 13/12, 2019 at 11:31 Comment(0)
S
0

In a scenario where the Web API being accessed requires authorization, the suggestions that utilize ActionFilterAttribute will not actually limit a client contacting an API that has not been authorized. The client can keep calling the api without any throttling.

The WebApiThrottling project uses a DelegatingHandler to overcome this. The following is an example DelegatingHandler that basically does the same thing as the other answers that use an ActionFilterAttribute. The added benefit is that it will work for authorized and unauthorized clients.

public enum TimeUnit
{
    Minute = 60,
    Hour = 3600,
    Day = 86400
}

public class ThrottleHandler : DelegatingHandler
{
    private class Error
    {
        public string Message;
    }

    private TimeUnit _timeUnit;
    private int _count;

    public ThrottleHandler(TimeUnit unit, int count)
    {
        _timeUnit = unit;
        _count = count;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var seconds = Convert.ToInt32(TimeUnit);

        var key = string.Join(
            "-",
            seconds,
            request.Method,
            request.RequestUri.AbsolutePath,
            GetClientIpAddress(request)
        );

        // increment the cache value
        var cnt = 1;
        if (HttpRuntime.Cache[key] != null)
        {
            cnt = (int)HttpRuntime.Cache[key] + 1;
        }

        HttpRuntime.Cache.Insert(
            key,
            cnt,
            null,
            DateTime.UtcNow.AddSeconds(seconds),
            Cache.NoSlidingExpiration,
            CacheItemPriority.Low,
            null
        );

        if (cnt > _count)
        {
            // break out of execution
            var response = request.CreateResponse((HttpStatusCode)429, new Error() { Message = "API call quota exceeded! {Count} calls per {TimeUnit} allowed." });
            return Task.FromResult(response);
        }

        return base.SendAsync(request, cancellationToken);
    }

    private string GetClientIpAddress(HttpRequestMessage request)
    {
        if (request.Properties.ContainsKey("MS_HttpContext"))
        {
            return ((HttpContextWrapper)request.Properties["MS_HttpContext"]).Request.UserHostAddress;
        }

        if (request.Properties.ContainsKey(RemoteEndpointMessageProperty.Name))
        {
            RemoteEndpointMessageProperty prop = (RemoteEndpointMessageProperty)request.Properties[RemoteEndpointMessageProperty.Name];
            return prop.Address;
        }

        if (HttpContext.Current != null)
        {
            return HttpContext.Current.Request.UserHostAddress;
        }

        return String.Empty;
    }
}
Spandau answered 1/2, 2023 at 0:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.