Coverage for app/venv/lib/python3.14/site-packages/weblate/api/middleware.py: 89%

27 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-07 07:15 +0000

1# Copyright © Michal Čihař <michal@weblate.org> 

2# 

3# SPDX-License-Identifier: GPL-3.0-or-later 

4 

5from __future__ import annotations 

6 

7import contextlib 

8from typing import TYPE_CHECKING 

9 

10from django.conf import settings 

11from django.http.request import UnreadablePostError 

12 

13if TYPE_CHECKING: 13 ↛ 14line 13 didn't jump to line 14 because the condition on line 13 was never true

14 from weblate.auth.models import AuthenticatedHttpRequest 

15 

16 

17RATELIMIT_LIMIT_HEADER = "X-RateLimit-Limit" 

18RATELIMIT_REMAINING_HEADER = "X-RateLimit-Remaining" 

19RATELIMIT_RESET_HEADER = "X-RateLimit-Reset" 

20 

21 

22class ThrottlingMiddleware: 

23 def __init__(self, get_response=None) -> None: 

24 self.get_response = get_response 

25 

26 def __call__(self, request: AuthenticatedHttpRequest): 

27 response = self.get_response(request) 

28 

29 # API payload workaround 

30 if request.method != "GET" and request.path_info.startswith( 

31 f"{settings.URL_PREFIX}/api" 

32 ): 

33 # Make sure full request is read. Django REST Framework lazily loads 

34 # request body when needed, but keeping the payload in the stream the client 

35 # ends up with an error: 

36 # 

37 # - HTTP/1.1: transfer closed with outstanding read data remaining 

38 # - HTTP/2: stream was not closed cleanly 

39 with contextlib.suppress(UnreadablePostError): 

40 request.read() 

41 

42 # Actual throttling 

43 throttling = request.META.get("throttling_state", None) 

44 if throttling is not None: 

45 response[RATELIMIT_LIMIT_HEADER] = throttling.num_requests 

46 response[RATELIMIT_REMAINING_HEADER] = throttling.num_requests - len( 

47 throttling.history 

48 ) 

49 if throttling.history: 49 ↛ 54line 49 didn't jump to line 54 because the condition on line 49 was always true

50 remaining_duration = throttling.duration - ( 

51 throttling.now - throttling.history[-1] 

52 ) 

53 else: 

54 remaining_duration = throttling.duration 

55 response[RATELIMIT_RESET_HEADER] = int(remaining_duration) 

56 return response