extension.py 51 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324
  1. """
  2. Flask-Limiter Extension
  3. """
  4. from __future__ import annotations
  5. import dataclasses
  6. import datetime
  7. import functools
  8. import itertools
  9. import logging
  10. import time
  11. import traceback
  12. import warnings
  13. import weakref
  14. from collections import defaultdict
  15. from functools import partial, wraps
  16. from types import TracebackType
  17. from typing import overload
  18. import flask
  19. import flask.wrappers
  20. from limits.errors import ConfigurationError
  21. from limits.storage import MemoryStorage, Storage, storage_from_string
  22. from limits.strategies import STRATEGIES, RateLimiter
  23. from ordered_set import OrderedSet
  24. from werkzeug.http import http_date, parse_date
  25. from ._compat import request_context
  26. from .constants import MAX_BACKEND_CHECKS, ConfigVars, ExemptionScope, HeaderNames
  27. from .errors import RateLimitExceeded
  28. from .manager import LimitManager
  29. from .typing import (
  30. Callable,
  31. P,
  32. R,
  33. Sequence,
  34. cast,
  35. )
  36. from .util import get_qualified_name
  37. from .wrappers import Limit, LimitGroup, RequestLimit
  38. @dataclasses.dataclass
  39. class LimiterContext:
  40. view_rate_limit: RequestLimit | None = None
  41. view_rate_limits: list[RequestLimit] = dataclasses.field(default_factory=list)
  42. conditional_deductions: dict[Limit, list[str]] = dataclasses.field(
  43. default_factory=dict
  44. )
  45. seen_limits: OrderedSet[Limit] = dataclasses.field(default_factory=OrderedSet)
  46. def reset(self) -> None:
  47. self.view_rate_limit = None
  48. self.view_rate_limits.clear()
  49. self.conditional_deductions.clear()
  50. self.seen_limits.clear()
  51. class Limiter:
  52. """
  53. The :class:`Limiter` class initializes the Flask-Limiter extension.
  54. :param key_func: a callable that returns the domain to rate limit
  55. by.
  56. :param app: :class:`flask.Flask` instance to initialize the extension with.
  57. :param default_limits: a variable list of strings or callables
  58. returning strings denoting default limits to apply to all routes that are
  59. not explicitely decorated with a limit. :ref:`ratelimit-string` for more details.
  60. :param default_limits_per_method: whether default limits are applied
  61. per method, per route or as a combination of all method per route.
  62. :param default_limits_exempt_when: a function that should return
  63. True/False to decide if the default limits should be skipped
  64. :param default_limits_deduct_when: a function that receives the
  65. current :class:`flask.Response` object and returns True/False to decide
  66. if a deduction should be made from the default rate limit(s)
  67. :param default_limits_cost: The cost of a hit to the default limits as an
  68. integer or a function that takes no parameters and returns an integer
  69. (Default: ``1``).
  70. :param application_limits: a variable list of strings or callables
  71. returning strings for limits that are applied to the entire application
  72. (i.e a shared limit for all routes)
  73. :param application_limits_per_method: whether application limits are applied
  74. per method, per route or as a combination of all method per route.
  75. :param application_limits_exempt_when: a function that should return
  76. True/False to decide if the application limits should be skipped
  77. :param application_limits_deduct_when: a function that receives the
  78. current :class:`flask.Response` object and returns True/False to decide
  79. if a deduction should be made from the application rate limit(s)
  80. :param application_limits_cost: The cost of a hit to the global application
  81. limits as an integer or a function that takes no parameters and returns an
  82. integer (Default: ``1``).
  83. :param headers_enabled: whether ``X-RateLimit`` response headers are
  84. written.
  85. :param header_name_mapping: Mapping of header names to use if
  86. :paramref:`Limiter.headers_enabled` is ``True``. If no mapping is provided
  87. the default values will be used.
  88. :param strategy: the strategy to use. Refer to :ref:`ratelimit-strategy`
  89. :param storage_uri: the storage location.
  90. Refer to :data:`RATELIMIT_STORAGE_URI`
  91. :param storage_options: kwargs to pass to the storage implementation
  92. upon instantiation.
  93. :param auto_check: whether to automatically check the rate limit in
  94. the before_request chain of the application. default ``True``
  95. :param swallow_errors: whether to swallow any errors when hitting a rate
  96. limit. An exception will still be logged. default ``False``
  97. :param fail_on_first_breach: whether to stop processing remaining limits
  98. after the first breach. default ``True``
  99. :param on_breach: a function that will be called when any limit in this
  100. extension is breached. If the function returns an instance of :class:`flask.Response`
  101. that will be the response embedded into the :exc:`RateLimitExceeded` exception
  102. raised.
  103. :param meta_limits: a variable list of strings or callables
  104. returning strings for limits that are used to control the upper limit of
  105. a requesting client hitting any configured rate limit. Once a meta limit is
  106. exceeded all subsequent requests will raise a :class:`~flask_limiter.RateLimitExceeded`
  107. for the duration of the meta limit window.
  108. :param on_meta_breach: a function that will be called when a meta limit in this
  109. extension is breached. If the function returns an instance of :class:`flask.Response`
  110. that will be the response embedded into the :exc:`RateLimitExceeded` exception
  111. raised.
  112. :param in_memory_fallback: a variable list of strings or callables
  113. returning strings denoting fallback limits to apply when the storage is
  114. down.
  115. :param in_memory_fallback_enabled: fall back to in memory
  116. storage when the main storage is down and inherits the original limits.
  117. default ``False``
  118. :param retry_after: Allows configuration of how the value of the
  119. `Retry-After` header is rendered. One of `http-date` or `delta-seconds`.
  120. :param key_prefix: prefix prepended to rate limiter keys and app context global names.
  121. :param request_identifier: a callable that returns the unique identity the current request.
  122. Defaults to :attr:`flask.Request.endpoint`
  123. :param enabled: Whether the extension is enabled or not
  124. """
  125. def __init__(
  126. self,
  127. key_func: Callable[[], str],
  128. *,
  129. app: flask.Flask | None = None,
  130. default_limits: list[str | Callable[[], str]] | None = None,
  131. default_limits_per_method: bool | None = None,
  132. default_limits_exempt_when: Callable[[], bool] | None = None,
  133. default_limits_deduct_when: None
  134. | (Callable[[flask.wrappers.Response], bool]) = None,
  135. default_limits_cost: int | Callable[[], int] | None = None,
  136. application_limits: list[str | Callable[[], str]] | None = None,
  137. application_limits_per_method: bool | None = None,
  138. application_limits_exempt_when: Callable[[], bool] | None = None,
  139. application_limits_deduct_when: None
  140. | (Callable[[flask.wrappers.Response], bool]) = None,
  141. application_limits_cost: int | Callable[[], int] | None = None,
  142. headers_enabled: bool | None = None,
  143. header_name_mapping: dict[HeaderNames, str] | None = None,
  144. strategy: str | None = None,
  145. storage_uri: str | None = None,
  146. storage_options: dict[str, str | int] | None = None,
  147. auto_check: bool = True,
  148. swallow_errors: bool | None = None,
  149. fail_on_first_breach: bool | None = None,
  150. on_breach: None
  151. | (Callable[[RequestLimit], flask.wrappers.Response | None]) = None,
  152. meta_limits: list[str | Callable[[], str]] | None = None,
  153. on_meta_breach: None
  154. | (Callable[[RequestLimit], flask.wrappers.Response | None]) = None,
  155. in_memory_fallback: list[str] | None = None,
  156. in_memory_fallback_enabled: bool | None = None,
  157. retry_after: str | None = None,
  158. key_prefix: str = "",
  159. request_identifier: Callable[..., str] | None = None,
  160. enabled: bool = True,
  161. ) -> None:
  162. self.app = app
  163. self.logger = logging.getLogger("flask-limiter")
  164. self.enabled = enabled
  165. self.initialized = False
  166. self._default_limits_per_method = default_limits_per_method
  167. self._default_limits_exempt_when = default_limits_exempt_when
  168. self._default_limits_deduct_when = default_limits_deduct_when
  169. self._default_limits_cost = default_limits_cost
  170. self._application_limits_per_method = application_limits_per_method
  171. self._application_limits_exempt_when = application_limits_exempt_when
  172. self._application_limits_deduct_when = application_limits_deduct_when
  173. self._application_limits_cost = application_limits_cost
  174. self._in_memory_fallback = []
  175. self._in_memory_fallback_enabled = in_memory_fallback_enabled or (
  176. in_memory_fallback and len(in_memory_fallback) > 0
  177. )
  178. self._route_exemptions: dict[str, ExemptionScope] = {}
  179. self._blueprint_exemptions: dict[str, ExemptionScope] = {}
  180. self._request_filters: list[Callable[[], bool]] = []
  181. self._headers_enabled = headers_enabled
  182. self._header_mapping = header_name_mapping or {}
  183. self._retry_after = retry_after
  184. self._strategy = strategy
  185. self._storage_uri = storage_uri
  186. self._storage_options = storage_options or {}
  187. self._auto_check = auto_check
  188. self._swallow_errors = swallow_errors
  189. self._fail_on_first_breach = fail_on_first_breach
  190. self._on_breach = on_breach
  191. self._on_meta_breach = on_meta_breach
  192. self._key_func = key_func
  193. self._key_prefix = key_prefix
  194. self._request_identifier = request_identifier
  195. _default_limits = (
  196. [
  197. LimitGroup(
  198. limit_provider=limit,
  199. key_function=self._key_func,
  200. )
  201. for limit in default_limits
  202. ]
  203. if default_limits
  204. else []
  205. )
  206. _application_limits = (
  207. [
  208. LimitGroup(
  209. limit_provider=limit,
  210. key_function=self._key_func,
  211. scope="global",
  212. shared=True,
  213. )
  214. for limit in application_limits
  215. ]
  216. if application_limits
  217. else []
  218. )
  219. self._meta_limits = (
  220. [
  221. LimitGroup(
  222. limit_provider=limit,
  223. key_function=self._key_func,
  224. scope="meta",
  225. shared=True,
  226. )
  227. for limit in meta_limits
  228. ]
  229. if meta_limits
  230. else []
  231. )
  232. if in_memory_fallback:
  233. for limit in in_memory_fallback:
  234. self._in_memory_fallback.append(
  235. LimitGroup(
  236. limit_provider=limit,
  237. key_function=self._key_func,
  238. )
  239. )
  240. self._storage: Storage | None = None
  241. self._limiter: RateLimiter | None = None
  242. self._storage_dead = False
  243. self._fallback_limiter: RateLimiter | None = None
  244. self.__check_backend_count = 0
  245. self.__last_check_backend = time.time()
  246. self._marked_for_limiting: set[str] = set()
  247. self.logger.addHandler(logging.NullHandler())
  248. self.limit_manager = LimitManager(
  249. application_limits=_application_limits,
  250. default_limits=_default_limits,
  251. decorated_limits={},
  252. blueprint_limits={},
  253. route_exemptions=self._route_exemptions,
  254. blueprint_exemptions=self._blueprint_exemptions,
  255. )
  256. if app:
  257. self.init_app(app)
  258. def init_app(self, app: flask.Flask) -> None:
  259. """
  260. :param app: :class:`flask.Flask` instance to rate limit.
  261. """
  262. config = app.config
  263. self.enabled = config.setdefault(ConfigVars.ENABLED, self.enabled)
  264. if not self.enabled:
  265. return
  266. if self._default_limits_per_method is None:
  267. self._default_limits_per_method = bool(
  268. config.get(ConfigVars.DEFAULT_LIMITS_PER_METHOD, False)
  269. )
  270. self._default_limits_exempt_when = (
  271. self._default_limits_exempt_when
  272. or config.get(ConfigVars.DEFAULT_LIMITS_EXEMPT_WHEN)
  273. )
  274. self._default_limits_deduct_when = (
  275. self._default_limits_deduct_when
  276. or config.get(ConfigVars.DEFAULT_LIMITS_DEDUCT_WHEN)
  277. )
  278. self._default_limits_cost = self._default_limits_cost or config.get(
  279. ConfigVars.DEFAULT_LIMITS_COST, 1
  280. )
  281. if self._swallow_errors is None:
  282. self._swallow_errors = bool(config.get(ConfigVars.SWALLOW_ERRORS, False))
  283. if self._fail_on_first_breach is None:
  284. self._fail_on_first_breach = bool(
  285. config.get(ConfigVars.FAIL_ON_FIRST_BREACH, True)
  286. )
  287. if self._headers_enabled is None:
  288. self._headers_enabled = bool(config.get(ConfigVars.HEADERS_ENABLED, False))
  289. self._storage_options.update(config.get(ConfigVars.STORAGE_OPTIONS, {}))
  290. storage_uri_from_config = config.get(ConfigVars.STORAGE_URI, None)
  291. if not storage_uri_from_config:
  292. if not self._storage_uri:
  293. warnings.warn(
  294. "Using the in-memory storage for tracking rate limits as no storage "
  295. "was explicitly specified. This is not recommended for production use. "
  296. "See: https://flask-limiter.readthedocs.io#configuring-a-storage-backend "
  297. "for documentation about configuring the storage backend."
  298. )
  299. storage_uri_from_config = "memory://"
  300. self._storage = cast(
  301. Storage,
  302. storage_from_string(
  303. self._storage_uri or storage_uri_from_config, **self._storage_options
  304. ),
  305. )
  306. self._strategy = self._strategy or config.setdefault(
  307. ConfigVars.STRATEGY, "fixed-window"
  308. )
  309. if self._strategy not in STRATEGIES:
  310. raise ConfigurationError(
  311. "Invalid rate limiting strategy %s" % self._strategy
  312. )
  313. self._limiter = STRATEGIES[self._strategy](self._storage)
  314. self._header_mapping = {
  315. HeaderNames.RESET: self._header_mapping.get(
  316. HeaderNames.RESET,
  317. config.get(ConfigVars.HEADER_RESET, HeaderNames.RESET.value),
  318. ),
  319. HeaderNames.REMAINING: self._header_mapping.get(
  320. HeaderNames.REMAINING,
  321. config.get(ConfigVars.HEADER_REMAINING, HeaderNames.REMAINING.value),
  322. ),
  323. HeaderNames.LIMIT: self._header_mapping.get(
  324. HeaderNames.LIMIT,
  325. config.get(ConfigVars.HEADER_LIMIT, HeaderNames.LIMIT.value),
  326. ),
  327. HeaderNames.RETRY_AFTER: self._header_mapping.get(
  328. HeaderNames.RETRY_AFTER,
  329. config.get(
  330. ConfigVars.HEADER_RETRY_AFTER, HeaderNames.RETRY_AFTER.value
  331. ),
  332. ),
  333. }
  334. self._retry_after = self._retry_after or config.get(
  335. ConfigVars.HEADER_RETRY_AFTER_VALUE
  336. )
  337. self._key_prefix = self._key_prefix or config.get(ConfigVars.KEY_PREFIX, "")
  338. self._request_identifier = self._request_identifier or config.get(
  339. ConfigVars.REQUEST_IDENTIFIER, lambda: flask.request.endpoint or ""
  340. )
  341. app_limits = config.get(ConfigVars.APPLICATION_LIMITS, None)
  342. self._application_limits_cost = self._application_limits_cost or config.get(
  343. ConfigVars.APPLICATION_LIMITS_COST, 1
  344. )
  345. if self._application_limits_per_method is None:
  346. self._application_limits_per_method = bool(
  347. config.get(ConfigVars.APPLICATION_LIMITS_PER_METHOD, False)
  348. )
  349. self._application_limits_exempt_when = (
  350. self._application_limits_exempt_when
  351. or config.get(ConfigVars.APPLICATION_LIMITS_EXEMPT_WHEN)
  352. )
  353. self._application_limits_deduct_when = (
  354. self._application_limits_deduct_when
  355. or config.get(ConfigVars.APPLICATION_LIMITS_DEDUCT_WHEN)
  356. )
  357. if not self.limit_manager._application_limits and app_limits:
  358. self.limit_manager.set_application_limits(
  359. [
  360. LimitGroup(
  361. limit_provider=app_limits,
  362. key_function=self._key_func,
  363. scope="global",
  364. shared=True,
  365. per_method=self._application_limits_per_method,
  366. exempt_when=self._application_limits_exempt_when,
  367. deduct_when=self._application_limits_deduct_when,
  368. cost=self._application_limits_cost,
  369. )
  370. ]
  371. )
  372. else:
  373. app_limits = self.limit_manager._application_limits
  374. for group in app_limits:
  375. group.cost = self._application_limits_cost
  376. group.per_method = self._application_limits_per_method
  377. group.exempt_when = self._application_limits_exempt_when
  378. group.deduct_when = self._application_limits_deduct_when
  379. self.limit_manager.set_application_limits(app_limits)
  380. conf_limits = config.get(ConfigVars.DEFAULT_LIMITS, None)
  381. if not self.limit_manager._default_limits and conf_limits:
  382. self.limit_manager.set_default_limits(
  383. [
  384. LimitGroup(
  385. limit_provider=conf_limits,
  386. key_function=self._key_func,
  387. per_method=self._default_limits_per_method,
  388. exempt_when=self._default_limits_exempt_when,
  389. deduct_when=self._default_limits_deduct_when,
  390. cost=self._default_limits_cost,
  391. )
  392. ]
  393. )
  394. else:
  395. default_limit_groups = self.limit_manager._default_limits
  396. for group in default_limit_groups:
  397. group.per_method = self._default_limits_per_method
  398. group.exempt_when = self._default_limits_exempt_when
  399. group.deduct_when = self._default_limits_deduct_when
  400. group.cost = self._default_limits_cost
  401. self.limit_manager.set_default_limits(default_limit_groups)
  402. meta_limits = config.get(ConfigVars.META_LIMITS, None)
  403. if not self._meta_limits and meta_limits:
  404. self._meta_limits = [
  405. LimitGroup(
  406. limit_provider=meta_limits,
  407. key_function=self._key_func,
  408. scope="meta",
  409. shared=True,
  410. )
  411. ]
  412. self._on_breach = self._on_breach or config.get(ConfigVars.ON_BREACH, None)
  413. self._on_meta_breach = self._on_meta_breach or config.get(
  414. ConfigVars.ON_META_BREACH, None
  415. )
  416. self.__configure_fallbacks(app, self._strategy)
  417. if self not in app.extensions.setdefault("limiter", set()):
  418. if self._auto_check:
  419. app.before_request(self._check_request_limit)
  420. app.after_request(partial(Limiter.__inject_headers, self))
  421. app.teardown_request(self.__release_context)
  422. app.extensions["limiter"].add(self)
  423. self.initialized = True
  424. @property
  425. def context(self) -> LimiterContext:
  426. """
  427. The context is meant to exist for the lifetime
  428. of a request/response cycle per instance of the extension
  429. so as to keep track of any state used at different steps
  430. in the lifecycle (for example to pass information
  431. from the before request hook to the after_request hook)
  432. :meta private:
  433. """
  434. ctx = request_context()
  435. if not hasattr(ctx, "_limiter_request_context"):
  436. ctx._limiter_request_context = defaultdict(LimiterContext) # type: ignore
  437. return cast(
  438. dict[Limiter, LimiterContext],
  439. ctx._limiter_request_context, # type: ignore
  440. )[self]
  441. def limit(
  442. self,
  443. limit_value: str | Callable[[], str],
  444. *,
  445. key_func: Callable[[], str] | None = None,
  446. per_method: bool = False,
  447. methods: list[str] | None = None,
  448. error_message: str | None = None,
  449. exempt_when: Callable[[], bool] | None = None,
  450. override_defaults: bool = True,
  451. deduct_when: Callable[[flask.wrappers.Response], bool] | None = None,
  452. on_breach: None
  453. | (Callable[[RequestLimit], flask.wrappers.Response | None]) = None,
  454. cost: int | Callable[[], int] = 1,
  455. scope: str | Callable[[str], str] | None = None,
  456. ) -> LimitDecorator:
  457. """
  458. Decorator to be used for rate limiting individual routes or blueprints.
  459. :param limit_value: rate limit string or a callable that returns a
  460. string. :ref:`ratelimit-string` for more details.
  461. :param key_func: function/lambda to extract the unique
  462. identifier for the rate limit. defaults to remote address of the
  463. request.
  464. :param per_method: whether the limit is sub categorized into the
  465. http method of the request.
  466. :param methods: if specified, only the methods in this list will
  467. be rate limited (default: ``None``).
  468. :param error_message: string (or callable that returns one) to override
  469. the error message used in the response.
  470. :param exempt_when: function/lambda used to decide if the rate
  471. limit should skipped.
  472. :param override_defaults: whether the decorated limit overrides
  473. the default limits (Default: ``True``).
  474. .. note:: When used with a :class:`~flask.Blueprint` the meaning
  475. of the parameter extends to any parents the blueprint instance is
  476. registered under. For more details see :ref:`recipes:nested blueprints`
  477. :param deduct_when: a function that receives the current
  478. :class:`flask.Response` object and returns True/False to decide if a
  479. deduction should be done from the rate limit
  480. :param on_breach: a function that will be called when this limit
  481. is breached. If the function returns an instance of :class:`flask.Response`
  482. that will be the response embedded into the :exc:`RateLimitExceeded` exception
  483. raised.
  484. :param cost: The cost of a hit or a function that
  485. takes no parameters and returns the cost as an integer (Default: ``1``).
  486. :param scope: a string or callable that returns a string
  487. for further categorizing the rate limiting scope. This scope is combined
  488. with the current endpoint of the request.
  489. Changes
  490. - .. versionadded:: 2.9.0
  491. The returned object can also be used as a context manager
  492. for rate limiting a code block inside a view. For example::
  493. @app.route("/")
  494. def route():
  495. try:
  496. with limiter.limit("10/second"):
  497. # something expensive
  498. except RateLimitExceeded: pass
  499. """
  500. return LimitDecorator(
  501. self,
  502. limit_value,
  503. key_func,
  504. False,
  505. scope,
  506. per_method=per_method,
  507. methods=methods,
  508. error_message=error_message,
  509. exempt_when=exempt_when,
  510. override_defaults=override_defaults,
  511. deduct_when=deduct_when,
  512. on_breach=on_breach,
  513. cost=cost,
  514. )
  515. def shared_limit(
  516. self,
  517. limit_value: str | Callable[[], str],
  518. scope: str | Callable[[str], str],
  519. *,
  520. key_func: Callable[[], str] | None = None,
  521. per_method: bool = False,
  522. methods: list[str] | None = None,
  523. error_message: str | None = None,
  524. exempt_when: Callable[[], bool] | None = None,
  525. override_defaults: bool = True,
  526. deduct_when: Callable[[flask.wrappers.Response], bool] | None = None,
  527. on_breach: None
  528. | (Callable[[RequestLimit], flask.wrappers.Response | None]) = None,
  529. cost: int | Callable[[], int] = 1,
  530. ) -> LimitDecorator:
  531. """
  532. decorator to be applied to multiple routes sharing the same rate limit.
  533. :param limit_value: rate limit string or a callable that returns a
  534. string. :ref:`ratelimit-string` for more details.
  535. :param scope: a string or callable that returns a string
  536. for defining the rate limiting scope.
  537. :param key_func: function/lambda to extract the unique
  538. identifier for the rate limit. defaults to remote address of the
  539. request.
  540. :param per_method: whether the limit is sub categorized into the
  541. http method of the request.
  542. :param methods: if specified, only the methods in this list will
  543. be rate limited (default: ``None``).
  544. :param error_message: string (or callable that returns one) to override
  545. the error message used in the response.
  546. :param function exempt_when: function/lambda used to decide if the rate
  547. limit should skipped.
  548. :param override_defaults: whether the decorated limit overrides
  549. the default limits. (default: ``True``)
  550. .. note:: When used with a :class:`~flask.Blueprint` the meaning
  551. of the parameter extends to any parents the blueprint instance is
  552. registered under. For more details see :ref:`recipes:nested blueprints`
  553. :param deduct_when: a function that receives the current
  554. :class:`flask.Response` object and returns True/False to decide if a
  555. deduction should be done from the rate limit
  556. :param on_breach: a function that will be called when this limit
  557. is breached. If the function returns an instance of :class:`flask.Response`
  558. that will be the response embedded into the :exc:`RateLimitExceeded` exception
  559. raised.
  560. :param cost: The cost of a hit or a function that
  561. takes no parameters and returns the cost as an integer (default: ``1``).
  562. """
  563. return LimitDecorator(
  564. self,
  565. limit_value,
  566. key_func,
  567. True,
  568. scope,
  569. per_method=per_method,
  570. methods=methods,
  571. error_message=error_message,
  572. exempt_when=exempt_when,
  573. override_defaults=override_defaults,
  574. deduct_when=deduct_when,
  575. on_breach=on_breach,
  576. cost=cost,
  577. )
  578. @overload
  579. def exempt(
  580. self,
  581. obj: flask.Blueprint,
  582. *,
  583. flags: ExemptionScope = ExemptionScope.APPLICATION
  584. | ExemptionScope.DEFAULT
  585. | ExemptionScope.META,
  586. ) -> flask.Blueprint: ...
  587. @overload
  588. def exempt(
  589. self,
  590. obj: Callable[..., R],
  591. *,
  592. flags: ExemptionScope = ExemptionScope.APPLICATION
  593. | ExemptionScope.DEFAULT
  594. | ExemptionScope.META,
  595. ) -> Callable[..., R]: ...
  596. @overload
  597. def exempt(
  598. self,
  599. *,
  600. flags: ExemptionScope = ExemptionScope.APPLICATION
  601. | ExemptionScope.DEFAULT
  602. | ExemptionScope.META,
  603. ) -> (
  604. Callable[[Callable[P, R]], Callable[P, R]]
  605. | Callable[[flask.Blueprint], flask.Blueprint]
  606. ): ...
  607. def exempt(
  608. self,
  609. obj: Callable[..., R] | flask.Blueprint | None = None,
  610. *,
  611. flags: ExemptionScope = ExemptionScope.APPLICATION
  612. | ExemptionScope.DEFAULT
  613. | ExemptionScope.META,
  614. ) -> (
  615. Callable[..., R]
  616. | flask.Blueprint
  617. | Callable[[Callable[P, R]], Callable[P, R]]
  618. | Callable[[flask.Blueprint], flask.Blueprint]
  619. ):
  620. """
  621. Mark a view function or all views in a blueprint as exempt from
  622. rate limits.
  623. :param obj: view function or blueprint to mark as exempt.
  624. :param flags: Controls the scope of the exemption. By default
  625. application wide limits, defaults configured on the extension and meta limits
  626. are opted out of. Additional flags can be used to control the behavior
  627. when :paramref:`obj` is a Blueprint that is nested under another Blueprint
  628. or has other Blueprints nested under it (See :ref:`recipes:nested blueprints`)
  629. The method can be used either as a decorator without any arguments (the default
  630. flags will apply and the route will be exempt from default and application limits::
  631. @app.route("...")
  632. @limiter.exempt
  633. def route(...):
  634. ...
  635. Specific exemption flags can be provided at decoration time::
  636. @app.route("...")
  637. @limiter.exempt(flags=ExemptionScope.APPLICATION)
  638. def route(...):
  639. ...
  640. If an entire blueprint (i.e. all routes under it) are to be exempted the method
  641. can be called with the blueprint as the first parameter and any additional flags::
  642. bp = Blueprint(...)
  643. limiter.exempt(bp)
  644. limiter.exempt(
  645. bp,
  646. flags=ExemptionScope.DEFAULT|ExemptionScope.APPLICATION|ExemptionScope.ANCESTORS
  647. )
  648. """
  649. if isinstance(obj, flask.Blueprint):
  650. self.limit_manager.add_blueprint_exemption(obj.name, flags)
  651. elif obj:
  652. self.limit_manager.add_route_exemption(get_qualified_name(obj), flags)
  653. else:
  654. return functools.partial(self.exempt, flags=flags)
  655. return obj
  656. def request_filter(self, fn: Callable[[], bool]) -> Callable[[], bool]:
  657. """
  658. decorator to mark a function as a filter to be executed
  659. to check if the request is exempt from rate limiting.
  660. :param fn: The function will be called before evaluating any rate limits
  661. to decide whether to perform rate limit or skip it.
  662. """
  663. self._request_filters.append(fn)
  664. return fn
  665. def __configure_fallbacks(self, app: flask.Flask, strategy: str) -> None:
  666. config = app.config
  667. fallback_enabled = config.get(ConfigVars.IN_MEMORY_FALLBACK_ENABLED, False)
  668. fallback_limits = config.get(ConfigVars.IN_MEMORY_FALLBACK, None)
  669. if not self._in_memory_fallback and fallback_limits:
  670. self._in_memory_fallback = [
  671. LimitGroup(
  672. limit_provider=fallback_limits,
  673. key_function=self._key_func,
  674. scope=None,
  675. per_method=False,
  676. cost=1,
  677. )
  678. ]
  679. if not self._in_memory_fallback_enabled:
  680. self._in_memory_fallback_enabled = (
  681. fallback_enabled or len(self._in_memory_fallback) > 0
  682. )
  683. if self._in_memory_fallback_enabled:
  684. self._fallback_storage = MemoryStorage()
  685. self._fallback_limiter = STRATEGIES[strategy](self._fallback_storage)
  686. def __should_check_backend(self) -> bool:
  687. if self.__check_backend_count > MAX_BACKEND_CHECKS:
  688. self.__check_backend_count = 0
  689. if time.time() - self.__last_check_backend > pow(2, self.__check_backend_count):
  690. self.__last_check_backend = time.time()
  691. self.__check_backend_count += 1
  692. return True
  693. return False
  694. def check(self) -> None:
  695. """
  696. Explicitly check the limits for the current request. This is only relevant
  697. if the extension was initialized with :paramref:`~flask_limiter.Limiter.auto_check`
  698. set to ``False``
  699. :raises: RateLimitExceeded
  700. """
  701. self._check_request_limit(in_middleware=False)
  702. def reset(self) -> None:
  703. """
  704. resets the storage if it supports being reset
  705. """
  706. try:
  707. self.storage.reset()
  708. self.logger.info("Storage has been reset and all limits cleared")
  709. except NotImplementedError:
  710. self.logger.warning("This storage type does not support being reset")
  711. @property
  712. def storage(self) -> Storage:
  713. """
  714. The backend storage configured for the rate limiter
  715. """
  716. assert self._storage
  717. return self._storage
  718. @property
  719. def limiter(self) -> RateLimiter:
  720. """
  721. Instance of the rate limiting strategy used for performing
  722. rate limiting.
  723. """
  724. if self._storage_dead and self._in_memory_fallback_enabled:
  725. limiter = self._fallback_limiter
  726. else:
  727. limiter = self._limiter
  728. assert limiter
  729. return limiter
  730. @property
  731. def current_limit(self) -> RequestLimit | None:
  732. """
  733. Get details for the most relevant rate limit used in this request.
  734. In a scenario where multiple rate limits are active for a single request
  735. and none are breached, the rate limit which applies to the smallest
  736. time window will be returned.
  737. .. important:: The value of ``remaining`` in :class:`RequestLimit` is after
  738. deduction for the current request.
  739. For example::
  740. @limit("1/second")
  741. @limit("60/minute")
  742. @limit("2/day")
  743. def route(...):
  744. ...
  745. - Request 1 at ``t=0`` (no breach): this will return the details for for ``1/second``
  746. - Request 2 at ``t=1`` (no breach): it will still return the details for ``1/second``
  747. - Request 3 at ``t=2`` (breach): it will return the details for ``2/day``
  748. """
  749. return self.context.view_rate_limit
  750. @property
  751. def current_limits(self) -> list[RequestLimit]:
  752. """
  753. Get a list of all rate limits that were applicable and evaluated
  754. within the context of this request.
  755. The limits are returned in a sorted order by smallest window size first.
  756. """
  757. return self.context.view_rate_limits
  758. def identify_request(self) -> str:
  759. """
  760. Returns the identity of the request (by default this is the
  761. :attr:`flask.Request.endpoint` associated by the view function
  762. that is handling the request). The behavior can be customized
  763. by initializing the extension with a callable argument for
  764. :paramref:`~flask_limiter.Limiter.request_identifier`.
  765. """
  766. if self.initialized and self.enabled:
  767. assert self._request_identifier
  768. return self._request_identifier()
  769. return ""
  770. def __check_conditional_deductions(self, response: flask.wrappers.Response) -> None:
  771. for lim, args in self.context.conditional_deductions.items():
  772. if lim.deduct_when and lim.deduct_when(response):
  773. try:
  774. self.limiter.hit(lim.limit, *args, cost=lim.cost)
  775. except Exception as err:
  776. if self._swallow_errors:
  777. self.logger.exception(
  778. "Failed to deduct rate limit. Swallowing error"
  779. )
  780. else:
  781. raise err
  782. def __inject_headers(
  783. self, response: flask.wrappers.Response
  784. ) -> flask.wrappers.Response:
  785. self.__check_conditional_deductions(response)
  786. header_limit = self.current_limit
  787. if (
  788. self.enabled
  789. and self._headers_enabled
  790. and header_limit
  791. and self._header_mapping
  792. ):
  793. try:
  794. reset_at = header_limit.reset_at
  795. response.headers.add(
  796. self._header_mapping[HeaderNames.LIMIT],
  797. str(header_limit.limit.amount),
  798. )
  799. response.headers.add(
  800. self._header_mapping[HeaderNames.REMAINING],
  801. str(header_limit.remaining),
  802. )
  803. response.headers.add(
  804. self._header_mapping[HeaderNames.RESET], str(reset_at)
  805. )
  806. # response may have an existing retry after
  807. existing_retry_after_header = response.headers.get("Retry-After")
  808. if existing_retry_after_header is not None:
  809. # might be in http-date format
  810. retry_after: float | datetime.datetime | None = parse_date(
  811. existing_retry_after_header
  812. )
  813. # parse_date failure returns None
  814. if retry_after is None:
  815. retry_after = time.time() + int(existing_retry_after_header)
  816. if isinstance(retry_after, datetime.datetime):
  817. retry_after = time.mktime(retry_after.timetuple())
  818. reset_at = max(int(retry_after), reset_at)
  819. # set the header instead of using add
  820. response.headers.set(
  821. self._header_mapping[HeaderNames.RETRY_AFTER],
  822. str(
  823. http_date(reset_at)
  824. if self._retry_after == "http-date"
  825. else int(reset_at - time.time())
  826. ),
  827. )
  828. except Exception as e: # noqa: E722
  829. if self._in_memory_fallback_enabled and not self._storage_dead:
  830. self.logger.warning(
  831. "Rate limit storage unreachable - falling back to"
  832. " in-memory storage"
  833. )
  834. self._storage_dead = True
  835. response = self.__inject_headers(response)
  836. else:
  837. if self._swallow_errors:
  838. self.logger.exception(
  839. "Failed to update rate limit headers. Swallowing error"
  840. )
  841. else:
  842. raise e
  843. return response
  844. def __check_all_limits_exempt(
  845. self,
  846. endpoint: str | None,
  847. ) -> bool:
  848. return bool(
  849. not endpoint
  850. or not (self.enabled and self.initialized)
  851. or endpoint.split(".")[-1] == "static"
  852. or any(fn() for fn in self._request_filters)
  853. )
  854. def __filter_limits(
  855. self,
  856. endpoint: str | None,
  857. blueprint: str | None,
  858. callable_name: str | None,
  859. in_middleware: bool = False,
  860. ) -> list[Limit]:
  861. if callable_name:
  862. name = callable_name
  863. else:
  864. view_func = flask.current_app.view_functions.get(endpoint or "", None)
  865. name = get_qualified_name(view_func) if view_func else ""
  866. if self.__check_all_limits_exempt(endpoint):
  867. return []
  868. marked_for_limiting = (
  869. name in self._marked_for_limiting
  870. or self.limit_manager.has_hints(endpoint or "")
  871. )
  872. fallback_limits = []
  873. if self._storage_dead and self._fallback_limiter:
  874. if in_middleware and name in self._marked_for_limiting:
  875. pass
  876. else:
  877. if (
  878. self.__should_check_backend()
  879. and self._storage
  880. and self._storage.check()
  881. ):
  882. self.logger.info("Rate limit storage recovered")
  883. self._storage_dead = False
  884. self.__check_backend_count = 0
  885. else:
  886. fallback_limits = list(itertools.chain(*self._in_memory_fallback))
  887. if fallback_limits:
  888. return fallback_limits
  889. defaults, decorated = self.limit_manager.resolve_limits(
  890. flask.current_app,
  891. endpoint,
  892. blueprint,
  893. name,
  894. in_middleware,
  895. marked_for_limiting,
  896. )
  897. limits = OrderedSet(defaults) - self.context.seen_limits
  898. self.context.seen_limits.update(defaults)
  899. return list(limits) + list(decorated)
  900. def __evaluate_limits(self, endpoint: str, limits: list[Limit]) -> None:
  901. failed_limits: list[tuple[Limit, list[str]]] = []
  902. limit_for_header: RequestLimit | None = None
  903. view_limits: list[RequestLimit] = []
  904. meta_limits = list(itertools.chain(*self._meta_limits))
  905. if not (
  906. ExemptionScope.META
  907. & self.limit_manager.exemption_scope(
  908. flask.current_app, endpoint, flask.request.blueprint
  909. )
  910. ):
  911. for lim in meta_limits:
  912. limit_key, scope = lim.key_func(), lim.scope_for(endpoint, None)
  913. args = [limit_key, scope]
  914. if not self.limiter.test(lim.limit, *args, cost=lim.cost):
  915. breached_meta_limit = RequestLimit(
  916. self, lim.limit, args, True, lim.shared
  917. )
  918. self.context.view_rate_limit = breached_meta_limit
  919. self.context.view_rate_limits = [breached_meta_limit]
  920. meta_breach_response = None
  921. if self._on_meta_breach:
  922. try:
  923. cb_response = self._on_meta_breach(breached_meta_limit)
  924. if isinstance(cb_response, flask.wrappers.Response):
  925. meta_breach_response = cb_response
  926. except Exception as err: # noqa
  927. if self._swallow_errors:
  928. self.logger.exception(
  929. "on_meta_breach callback failed with error %s", err
  930. )
  931. else:
  932. raise err
  933. raise RateLimitExceeded(lim, response=meta_breach_response)
  934. for lim in sorted(limits, key=lambda x: x.limit):
  935. if lim.is_exempt or lim.method_exempt:
  936. continue
  937. limit_scope = lim.scope_for(endpoint, flask.request.method)
  938. limit_key = lim.key_func()
  939. args = [limit_key, limit_scope]
  940. kwargs = {}
  941. if not all(args):
  942. self.logger.error(
  943. f"Skipping limit: {lim.limit}. Empty value found in parameters."
  944. )
  945. continue
  946. if self._key_prefix:
  947. args = [self._key_prefix, *args]
  948. if lim.deduct_when:
  949. self.context.conditional_deductions[lim] = args
  950. method = self.limiter.test
  951. else:
  952. method = self.limiter.hit
  953. kwargs["cost"] = lim.cost
  954. request_limit = RequestLimit(self, lim.limit, args, False, lim.shared)
  955. view_limits.append(request_limit)
  956. if not method(lim.limit, *args, **kwargs):
  957. self.logger.info(
  958. "ratelimit %s (%s) exceeded at endpoint: %s",
  959. lim.limit,
  960. limit_key,
  961. limit_scope,
  962. )
  963. failed_limits.append((lim, args))
  964. view_limits[-1].breached = True
  965. limit_for_header = view_limits[-1]
  966. if self._fail_on_first_breach:
  967. break
  968. if not limit_for_header and view_limits:
  969. # Pick a non shared limit over a shared one if possible
  970. # when no rate limit has been hit. This should be the best hint
  971. # for the client.
  972. explicit = [limit for limit in view_limits if not limit.shared]
  973. limit_for_header = explicit[0] if explicit else view_limits[0]
  974. self.context.view_rate_limit = limit_for_header or None
  975. self.context.view_rate_limits = view_limits
  976. on_breach_response = None
  977. for limit in failed_limits:
  978. request_limit = RequestLimit(
  979. self, limit[0].limit, limit[1], True, limit[0].shared
  980. )
  981. for cb in dict.fromkeys([self._on_breach, limit[0].on_breach]):
  982. if cb:
  983. try:
  984. cb_response = cb(request_limit)
  985. if isinstance(cb_response, flask.wrappers.Response):
  986. on_breach_response = cb_response
  987. except Exception as err: # noqa
  988. if self._swallow_errors:
  989. self.logger.exception(
  990. "on_breach callback failed with error %s", err
  991. )
  992. else:
  993. raise err
  994. if failed_limits:
  995. for lim in meta_limits:
  996. limit_scope = lim.scope_for(endpoint, flask.request.method)
  997. limit_key = lim.key_func()
  998. args = [limit_key, limit_scope]
  999. self.limiter.hit(lim.limit, *args)
  1000. raise RateLimitExceeded(
  1001. sorted(failed_limits, key=lambda x: x[0].limit)[0][0],
  1002. response=on_breach_response,
  1003. )
  1004. def _check_request_limit(
  1005. self, callable_name: str | None = None, in_middleware: bool = True
  1006. ) -> None:
  1007. endpoint = self.identify_request()
  1008. try:
  1009. all_limits = self.__filter_limits(
  1010. endpoint,
  1011. flask.request.blueprint,
  1012. callable_name,
  1013. in_middleware,
  1014. )
  1015. self.__evaluate_limits(endpoint, all_limits)
  1016. except Exception as e:
  1017. if isinstance(e, RateLimitExceeded):
  1018. raise e
  1019. if self._in_memory_fallback_enabled and not self._storage_dead:
  1020. self.logger.warning(
  1021. "Rate limit storage unreachable - falling back to in-memory storage"
  1022. )
  1023. self._storage_dead = True
  1024. self.context.seen_limits.clear()
  1025. self._check_request_limit(
  1026. callable_name=callable_name, in_middleware=in_middleware
  1027. )
  1028. else:
  1029. if self._swallow_errors:
  1030. self.logger.exception("Failed to rate limit. Swallowing error")
  1031. else:
  1032. raise e
  1033. def __release_context(self, _: BaseException | None = None) -> None:
  1034. self.context.reset()
  1035. class LimitDecorator:
  1036. """
  1037. Wrapper used by :meth:`~flask_limiter.Limiter.limit`
  1038. and :meth:`~flask_limiter.Limiter.shared_limit`
  1039. when wrapping view functions or blueprints.
  1040. """
  1041. def __init__(
  1042. self,
  1043. limiter: Limiter,
  1044. limit_value: Callable[[], str] | str,
  1045. key_func: Callable[[], str] | None = None,
  1046. shared: bool = False,
  1047. scope: Callable[[str], str] | str | None = None,
  1048. per_method: bool = False,
  1049. methods: Sequence[str] | None = None,
  1050. error_message: str | None = None,
  1051. exempt_when: Callable[[], bool] | None = None,
  1052. override_defaults: bool = True,
  1053. deduct_when: Callable[[flask.wrappers.Response], bool] | None = None,
  1054. on_breach: None
  1055. | (Callable[[RequestLimit], flask.wrappers.Response | None]) = None,
  1056. cost: Callable[[], int] | int = 1,
  1057. ):
  1058. self.limiter: weakref.ProxyType[Limiter] = weakref.proxy(limiter)
  1059. self.limit_value = limit_value
  1060. self.key_func = key_func or self.limiter._key_func
  1061. self.scope = scope
  1062. self.per_method = per_method
  1063. self.methods = tuple(methods) if methods else None
  1064. self.error_message = error_message
  1065. self.exempt_when = exempt_when
  1066. self.override_defaults = override_defaults
  1067. self.deduct_when = deduct_when
  1068. self.on_breach = on_breach
  1069. self.cost = cost
  1070. self.is_static = not callable(self.limit_value)
  1071. self.shared = shared
  1072. @property
  1073. def limit_group(self) -> LimitGroup:
  1074. return LimitGroup(
  1075. limit_provider=self.limit_value,
  1076. key_function=self.key_func,
  1077. scope=self.scope,
  1078. per_method=self.per_method,
  1079. methods=self.methods,
  1080. error_message=self.error_message,
  1081. exempt_when=self.exempt_when,
  1082. override_defaults=self.override_defaults,
  1083. deduct_when=self.deduct_when,
  1084. on_breach=self.on_breach,
  1085. cost=self.cost,
  1086. shared=self.shared,
  1087. )
  1088. def __enter__(self) -> None:
  1089. tb = traceback.extract_stack(limit=2)
  1090. qualified_location = f"{tb[0].filename}:{tb[0].name}:{tb[0].lineno}"
  1091. # TODO: if use as a context manager becomes interesting/valuable
  1092. # a less hacky approach than using the traceback and piggy backing
  1093. # on the limit manager's knowledge of decorated limits might be worth it.
  1094. self.limiter.limit_manager.add_decorated_limit(
  1095. qualified_location, self.limit_group, override=True
  1096. )
  1097. self.limiter.limit_manager.add_endpoint_hint(
  1098. self.limiter.identify_request(), qualified_location
  1099. )
  1100. self.limiter._check_request_limit(
  1101. in_middleware=False, callable_name=qualified_location
  1102. )
  1103. def __exit__(
  1104. self,
  1105. exc_type: type[BaseException] | None,
  1106. exc_value: BaseException | None,
  1107. traceback: TracebackType | None,
  1108. ) -> None: ...
  1109. @overload
  1110. def __call__(self, obj: Callable[P, R]) -> Callable[P, R]: ...
  1111. @overload
  1112. def __call__(self, obj: flask.Blueprint) -> None: ...
  1113. def __call__(self, obj: Callable[P, R] | flask.Blueprint) -> Callable[P, R] | None:
  1114. if isinstance(obj, flask.Blueprint):
  1115. name = obj.name
  1116. else:
  1117. name = get_qualified_name(obj)
  1118. if isinstance(obj, flask.Blueprint):
  1119. self.limiter.limit_manager.add_blueprint_limit(name, self.limit_group)
  1120. return None
  1121. else:
  1122. self.limiter._marked_for_limiting.add(name)
  1123. self.limiter.limit_manager.add_decorated_limit(name, self.limit_group)
  1124. @wraps(obj)
  1125. def __inner(*a: P.args, **k: P.kwargs) -> R:
  1126. if (
  1127. self.limiter._auto_check
  1128. and not getattr(obj, "__wrapper-limiter-instance", None)
  1129. == self.limiter
  1130. ):
  1131. identity = self.limiter.identify_request()
  1132. if identity:
  1133. view_func = flask.current_app.view_functions.get(identity, None)
  1134. if view_func and not get_qualified_name(view_func) == name:
  1135. self.limiter.limit_manager.add_endpoint_hint(identity, name)
  1136. self.limiter._check_request_limit(
  1137. in_middleware=False, callable_name=name
  1138. )
  1139. return cast(R, flask.current_app.ensure_sync(obj)(*a, **k))
  1140. # mark this wrapper as wrapped by a decorator from the limiter
  1141. # from which the decorator was created. This ensures that stacked
  1142. # decorations only trigger rate limiting from the inner most
  1143. # decorator from each limiter instance (the weird need for
  1144. # keeping track of the instance is to handle cases where multiple
  1145. # limiter extensions are registered on the same application).
  1146. setattr(__inner, "__wrapper-limiter-instance", self.limiter)
  1147. return __inner