Rate limiting on boost endpoint and test#87
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds scoped DRF throttles for Boost endpoints with environment-configurable rates merged into REST_FRAMEWORK, updates endpoint wiring, adds test helpers, unit and live plugin tests validating throttling and headers, and renames test/CI artifacts from integration→plugin. ChangesBoost Endpoint Rate Limiting Implementation
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
src/boost_weblate/settings_override.py (1)
80-94: ⚖️ Poor tradeoffThrottle rates are frozen at import time.
BOOST_ENDPOINT_THROTTLE_RATESis evaluated once when this module is first imported, andmerge_boost_endpoint_throttle_ratesconsumes that constant rather than callingboost_endpoint_throttle_rates()fresh. Any process that setsBOOST_ENDPOINT_THROTTLE_*after import (or wants to re-read env) must reload the module. Reading env at merge time would remove that hidden coupling.♻️ Read env at merge time
def merge_boost_endpoint_throttle_rates( rest_framework: dict[str, Any], ) -> dict[str, Any]: """Merge Boost endpoint scoped rates into ``REST_FRAMEWORK``.""" merged = dict(rest_framework) existing = dict(merged.get("DEFAULT_THROTTLE_RATES", {})) - existing.update(BOOST_ENDPOINT_THROTTLE_RATES) + existing.update(boost_endpoint_throttle_rates()) merged["DEFAULT_THROTTLE_RATES"] = existing return merged🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/boost_weblate/settings_override.py` around lines 80 - 94, The current code freezes env reads by evaluating BOOST_ENDPOINT_THROTTLE_RATES at import; update merge_boost_endpoint_throttle_rates to call boost_endpoint_throttle_rates() at merge time (or replace the constant with a function reference) so the environment is re-read when merging. In short, stop consuming the pre-evaluated BOOST_ENDPOINT_THROTTLE_RATES constant and invoke boost_endpoint_throttle_rates() inside merge_boost_endpoint_throttle_rates so changes to BOOST_ENDPOINT_THROTTLE_* take effect without reloading the module.tests/integration/lib/http.py (2)
25-26: ⚡ Quick winHeader lookups become case-sensitive after this conversion.
resp.headers/e.headersare case-insensitiveHTTPMessageobjects, but the dict comprehension produces a case-sensitive plain dict. The integration tests then rely on exact casing (headers.get("Retry-After"),"X-RateLimit-Limit" in headers). This works today because DRF emitsRetry-Afterwith that exact casing, but it's fragile to any proxy/server that re-cases headers. Consider normalizing keys so lookups stay robust.♻️ Normalize header keys to lowercase
-def _response_headers(header_obj) -> dict[str, str]: - return {k: v for k, v in header_obj.items()} +def _response_headers(header_obj) -> dict[str, str]: + return {k.lower(): v for k, v in header_obj.items()}Callers would then look up
headers.get("retry-after")/"x-ratelimit-limit" in headers.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/lib/http.py` around lines 25 - 26, The _response_headers function converts a case-insensitive HTTPMessage to a plain dict which makes subsequent header lookups case-sensitive; update _response_headers(header_obj) to normalize header keys (e.g., to lowercase) when building the dict so callers can reliably use lowercase lookups like headers.get("retry-after") or "x-ratelimit-limit" in headers; ensure you only change key normalization and preserve header values and return type dict[str, str], and update any tests if they expect original casing.
29-65: ⚡ Quick winDuplicated request logic with
http_json.
http_json_with_headersis a near-verbatim copy ofhttp_json(URL build, headers, body encode, request, error handling, decode). Consider implementinghttp_jsonin terms of the new function to avoid drift.♻️ Delegate to the headers variant
def http_json( method: str, path: str, *, token: str | None = None, body: dict[str, Any] | None = None, timeout: float = 30.0, ) -> tuple[int, Any]: """Perform an HTTP request and return ``(status_code, parsed_json_or_text)``.""" - url = f"{base_url()}{path}" - headers: dict[str, str] = {"Accept": "application/json"} - if token is not None: - headers["Authorization"] = auth_header(token) - - data: bytes | None = None - if body is not None: - data = json.dumps(body).encode() - headers["Content-Type"] = "application/json" - - req = urllib.request.Request(url, data=data, headers=headers, method=method) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - raw = resp.read() - code: int = resp.getcode() - except urllib.error.HTTPError as e: - raw = e.read() - code = e.code - - if not raw: - return code, None - try: - return code, json.loads(raw.decode()) - except (json.JSONDecodeError, UnicodeDecodeError): - return code, raw.decode(errors="replace") + code, parsed, _ = http_json_with_headers( + method, path, token=token, body=body, timeout=timeout + ) + return code, parsed🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/integration/lib/http.py` around lines 29 - 65, The http_json function is duplicate of http_json_with_headers; refactor http_json to delegate to http_json_with_headers to avoid duplication: have http_json call http_json_with_headers(method, path, token=token, body=body, timeout=timeout), receive (status, body, headers) and return only (status, body), leaving all header-building, request/exception handling, and decoding in http_json_with_headers; update the http_json signature to match parameters used and ensure callers still get the (status, body) tuple.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/endpoint/test_views.py`:
- Around line 37-47: The helper _reload_throttle_rates currently overwrites
SimpleRateThrottle.THROTTLE_RATES and doesn't restore the previous state or
clear throttle counters, causing cross-test leakage; modify tests using
_reload_throttle_rates (or wrap it in a fixture) to save the original rates
(orig = SimpleRateThrottle.THROTTLE_RATES.copy()), run the override, and in a
finally block restore SimpleRateThrottle.THROTTLE_RATES = orig and clear the
throttle cache (e.g., call cache.clear() or the project's throttle cache clear
helper) so overridden rates and cached counters are always reset between tests;
if using a fixture, implement it as a yield fixture that performs the same
save/restore and cache clear around the test execution.
In `@tests/integration/test_rate_limit.py`:
- Around line 61-83: The test test_add_or_update_returns_429_when_rate_limited
is order-dependent because prior /boost-endpoint/add-or-update/ calls consume
the UserRateThrottle/AddOrUpdateThrottle quota for throttle_scope =
"add-or-update"; before the loop in
test_add_or_update_returns_429_when_rate_limited, either clear the throttle
state for the current user/token or obtain a fresh token so the quota is
isolated: locate the test function and add a step that deletes the throttle
cache keys for scope "add-or-update" (or calls the app's token factory to create
a new WEBLATE_API_TOKEN) so subsequent calls via http_json_with_headers hit an
empty quota and the loop can expect 202 until the limit is reached.
In `@tests/test_settings_override.py`:
- Around line 96-106: The test currently asserts hardcoded defaults for "info"
and "add-or-update" but those values are resolved from environment variables at
import; update the assertions to compare against the resolved environment-backed
values instead of literals: in
test_merge_boost_endpoint_throttle_rates_preserves_upstream(), import or read
the env vars used by boost_weblate.settings_override (e.g. check
os.environ.get("BOOST_ENDPOINT_THROTTLE_INFO", "60/minute") and
os.environ.get("BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE", "10/hour")) and assert
that rates["info"] and rates["add-or-update"] equal those resolved values,
keeping the existing assertions that upstream keys ("user","anon") are preserved
and referencing merge_boost_endpoint_throttle_rates to locate the logic.
---
Nitpick comments:
In `@src/boost_weblate/settings_override.py`:
- Around line 80-94: The current code freezes env reads by evaluating
BOOST_ENDPOINT_THROTTLE_RATES at import; update
merge_boost_endpoint_throttle_rates to call boost_endpoint_throttle_rates() at
merge time (or replace the constant with a function reference) so the
environment is re-read when merging. In short, stop consuming the pre-evaluated
BOOST_ENDPOINT_THROTTLE_RATES constant and invoke
boost_endpoint_throttle_rates() inside merge_boost_endpoint_throttle_rates so
changes to BOOST_ENDPOINT_THROTTLE_* take effect without reloading the module.
In `@tests/integration/lib/http.py`:
- Around line 25-26: The _response_headers function converts a case-insensitive
HTTPMessage to a plain dict which makes subsequent header lookups
case-sensitive; update _response_headers(header_obj) to normalize header keys
(e.g., to lowercase) when building the dict so callers can reliably use
lowercase lookups like headers.get("retry-after") or "x-ratelimit-limit" in
headers; ensure you only change key normalization and preserve header values and
return type dict[str, str], and update any tests if they expect original casing.
- Around line 29-65: The http_json function is duplicate of
http_json_with_headers; refactor http_json to delegate to http_json_with_headers
to avoid duplication: have http_json call http_json_with_headers(method, path,
token=token, body=body, timeout=timeout), receive (status, body, headers) and
return only (status, body), leaving all header-building, request/exception
handling, and decoding in http_json_with_headers; update the http_json signature
to match parameters used and ensure callers still get the (status, body) tuple.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8d7afcf2-1824-4685-a0a2-93fd44fa7de1
📒 Files selected for processing (9)
docker/docker-compose.ci.ymlscripts/integration-auth.shsrc/boost_weblate/endpoint/views.pysrc/boost_weblate/settings_override.pytests/django_qbk_format_settings.pytests/endpoint/test_views.pytests/integration/lib/http.pytests/integration/test_rate_limit.pytests/test_settings_override.py
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/plugin/test_rate_limit.py (1)
38-83:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winFix plugin rate-limit tests to avoid cross-module DRF throttle state leakage
scripts/plugin-auth.shrunstests/plugin/test_auth.pyimmediately beforetests/plugin/test_rate_limit.pyin the same live Weblate stack, and both suites use the sameWEBLATE_API_TOKEN. The endpoints under test useScopedRateThrottlewiththrottle_scope="info"/"add-or-update"(viaBoostEndpointInfoThrottle/AddOrUpdateThrottle), and DRF’sScopedRateThrottlekeys are based onthrottle_scope+ the authenticated user identifier—so the two successful requests intest_auth.pyprime the per-user/per-scope throttle counters. That makestest_rate_limit.py’s “expectlimitsuccesses, then429” expectation order-dependent (e.g., for3/minute, the 3rd request in the loop can become429).🐛 Proposed fix: tolerate prior consumption by driving until the first 429
def test_info_returns_429_when_rate_limited(self, api_token: str) -> None: rate = os.environ.get("BOOST_ENDPOINT_THROTTLE_INFO", "3/minute") limit = _parse_rate_limit(rate) last_headers: dict[str, str] = {} - for _ in range(limit): - code, _body, headers = http_get_with_headers( - "/boost-endpoint/info/", token=api_token - ) - assert code == 200, f"expected 200 before limit: {code}" - last_headers = headers - - code, _body, headers = http_get_with_headers( - "/boost-endpoint/info/", token=api_token - ) - assert code == 429, f"expected 429 after {limit} requests: {code}" + saw_success = False + code = None + headers = {} + for _ in range(limit + 1): + code, _body, headers = http_get_with_headers( + "/boost-endpoint/info/", token=api_token + ) + if code == 429: + break + assert code == 200, f"expected 200 before limit: {code}" + saw_success = True + last_headers = headers + assert saw_success, "expected at least one 200 before throttling" + assert code == 429, f"expected 429 within {limit + 1} requests: {code}" retry_after = headers.get("Retry-After") assert retry_after is not None assert int(retry_after) > 0Apply the analogous change to
test_add_or_update_returns_429_when_rate_limited, expecting202before the first429.A more deterministic alternative is to clear the Django cache/throttle storage in the Weblate container at the start of each test (e.g., using the existing
exec_pythonfixture withfrom django.core.cache import cache; cache.clear()), so throttle counters start empty regardless of test ordering.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/plugin/test_rate_limit.py` around lines 38 - 83, The tests test_info_returns_429_when_rate_limited and test_add_or_update_returns_429_when_rate_limited assume a fresh per-scope counter; instead make them tolerate prior consumption by driving requests until the first 429 is observed (loop calling http_get_with_headers in test_info_returns_429... and http_json_with_headers in test_add_or_update..., asserting success codes (200/202) only while response != 429 and then asserting the first 429 provides a positive Retry-After and that X-RateLimit-Limit in the last success headers equals the parsed limit from _parse_rate_limit); alternatively, at test start clear DRF throttle storage via exec_python running from django.core.cache import cache; cache.clear() so counters are deterministic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@tests/plugin/test_rate_limit.py`:
- Around line 38-83: The tests test_info_returns_429_when_rate_limited and
test_add_or_update_returns_429_when_rate_limited assume a fresh per-scope
counter; instead make them tolerate prior consumption by driving requests until
the first 429 is observed (loop calling http_get_with_headers in
test_info_returns_429... and http_json_with_headers in test_add_or_update...,
asserting success codes (200/202) only while response != 429 and then asserting
the first 429 provides a positive Retry-After and that X-RateLimit-Limit in the
last success headers equals the parsed limit from _parse_rate_limit);
alternatively, at test start clear DRF throttle storage via exec_python running
from django.core.cache import cache; cache.clear() so counters are
deterministic.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 69324dbc-e824-432b-8dbc-9bde3c7fd69a
⛔ Files ignored due to path filters (1)
uv.lockis excluded by!**/*.lock
📒 Files selected for processing (23)
.github/README.md.github/workflows/ci-plugin-auth.yml.github/workflows/ci-plugin-functional.yml.github/workflows/ci-plugin-smoke.yml.github/workflows/ci.ymlREADME.mddocker/README.mddocker/docker-compose.ci.ymlpyproject.tomlscripts/integration-auth.shscripts/integration-functional.shscripts/integration-smoke.shtests/plugin/__init__.pytests/plugin/conftest.pytests/plugin/lib/__init__.pytests/plugin/lib/docker_exec.pytests/plugin/lib/gh_repo.pytests/plugin/lib/http.pytests/plugin/lib/weblate_api.pytests/plugin/test_auth.pytests/plugin/test_functional.pytests/plugin/test_rate_limit.pytests/plugin/test_smoke.py
💤 Files with no reviewable changes (1)
- tests/plugin/lib/http.py
✅ Files skipped from review due to trivial changes (5)
- tests/plugin/conftest.py
- .github/README.md
- tests/plugin/lib/init.py
- tests/plugin/test_functional.py
- docker/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
- docker/docker-compose.ci.yml
There was a problem hiding this comment.
🧹 Nitpick comments (2)
scripts/plugin-smoke.sh (1)
45-47: 💤 Low valueConsider adding throttle environment variables for consistency.
Similar to
plugin-functional.sh, this smoke test script does not export throttle rate environment variables, whileplugin-auth.shdoes (lines 41-42 in that file). For consistency across the test suite, consider adding the same throttle configuration before running tests, even if smoke tests are unlikely to trigger rate limits.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/plugin-smoke.sh` around lines 45 - 47, Add the same throttle environment variable exports used in plugin-auth.sh into scripts/plugin-smoke.sh (place them before the python -m pytest invocation) so the smoke test uses the same rate-limit configuration; copy the exact export lines from plugin-auth.sh and insert them right after the uv pip install --quiet --system --group plugin line and before the pytest command to ensure consistent throttle behavior.scripts/plugin-functional.sh (1)
61-63: ⚡ Quick winConsider adding throttle environment variables for consistency.
The
plugin-auth.shscript exportsBOOST_ENDPOINT_THROTTLE_INFOandBOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATEenvironment variables (lines 41-42 in that file), but this functional test script does not. If the functional tests make requests to the Boost endpoints, they may encounter default throttle limits that differ from the test expectations.For consistency and predictability across test suites, consider adding:
export BOOST_ENDPOINT_THROTTLE_INFO="${BOOST_ENDPOINT_THROTTLE_INFO:-3/minute}" export BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE="${BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE:-3/hour}"before line 60.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/plugin-functional.sh` around lines 61 - 63, Add exported throttle env vars to the plugin functional test script: in scripts/plugin-functional.sh export BOOST_ENDPOINT_THROTTLE_INFO and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE with sensible defaults (e.g., defaulting to "3/minute" and "3/hour") before the pytest invocation so the tests use the same throttle settings as plugin-auth.sh; update the script near where the test runner (python -m pytest) is invoked so BOOST_ENDPOINT_THROTTLE_INFO and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE are set for the test process.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@scripts/plugin-functional.sh`:
- Around line 61-63: Add exported throttle env vars to the plugin functional
test script: in scripts/plugin-functional.sh export BOOST_ENDPOINT_THROTTLE_INFO
and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE with sensible defaults (e.g.,
defaulting to "3/minute" and "3/hour") before the pytest invocation so the tests
use the same throttle settings as plugin-auth.sh; update the script near where
the test runner (python -m pytest) is invoked so BOOST_ENDPOINT_THROTTLE_INFO
and BOOST_ENDPOINT_THROTTLE_ADD_OR_UPDATE are set for the test process.
In `@scripts/plugin-smoke.sh`:
- Around line 45-47: Add the same throttle environment variable exports used in
plugin-auth.sh into scripts/plugin-smoke.sh (place them before the python -m
pytest invocation) so the smoke test uses the same rate-limit configuration;
copy the exact export lines from plugin-auth.sh and insert them right after the
uv pip install --quiet --system --group plugin line and before the pytest
command to ensure consistent throttle behavior.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 483db73d-9ef1-4892-a03b-d6baa43c31ff
📒 Files selected for processing (18)
.github/README.md.github/workflows/ci-plugin-auth.yml.github/workflows/ci-plugin-functional.yml.github/workflows/ci-plugin-smoke.ymlREADME.mddocs/boost-weblate-plugin-refactor-plan.mdscripts/README.mdscripts/plugin-auth.shscripts/plugin-functional.shscripts/plugin-smoke.shsrc/boost_weblate/endpoint/apps.pytests/plugin/lib/gh_repo.pytests/plugin/lib/http.pytests/plugin/lib/weblate_api.pytests/plugin/test_auth.pytests/plugin/test_functional.pytests/plugin/test_rate_limit.pytests/plugin/test_smoke.py
✅ Files skipped from review due to trivial changes (5)
- tests/plugin/lib/gh_repo.py
- docs/boost-weblate-plugin-refactor-plan.md
- .github/README.md
- scripts/README.md
- src/boost_weblate/endpoint/apps.py
🚧 Files skipped from review as they are similar to previous changes (9)
- tests/plugin/lib/weblate_api.py
- tests/plugin/test_auth.py
- tests/plugin/test_functional.py
- .github/workflows/ci-plugin-auth.yml
- .github/workflows/ci-plugin-smoke.yml
- tests/plugin/test_smoke.py
- .github/workflows/ci-plugin-functional.yml
- tests/plugin/test_rate_limit.py
- tests/plugin/lib/http.py
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/plugin/lib/http.py (1)
76-109: ⚡ Quick winEliminate code duplication by delegating to
http_json_with_headers.The
http_jsonfunction duplicates ~33 lines of logic fromhttp_json_with_headers. Refactorhttp_jsonto callhttp_json_with_headersand discard the headers from the returned tuple, establishing a single source of truth for the HTTP request/response handling logic.♻️ Refactor to eliminate duplication
def http_json( method: str, path: str, *, token: str | None = None, body: dict[str, Any] | None = None, timeout: float = 30.0, ) -> tuple[int, Any]: """Perform an HTTP request and return ``(status_code, parsed_json_or_text)``.""" - url = f"{base_url()}{path}" - headers: dict[str, str] = {"Accept": "application/json"} - if token is not None: - headers["Authorization"] = auth_header(token) - - data: bytes | None = None - if body is not None: - data = json.dumps(body).encode() - headers["Content-Type"] = "application/json" - - req = urllib.request.Request(url, data=data, headers=headers, method=method) - try: - with urllib.request.urlopen(req, timeout=timeout) as resp: - raw = resp.read() - code: int = resp.getcode() - except urllib.error.HTTPError as e: - raw = e.read() - code = e.code - - if not raw: - return code, None - try: - return code, json.loads(raw.decode()) - except (json.JSONDecodeError, UnicodeDecodeError): - return code, raw.decode(errors="replace") + status_code, parsed_body, _ = http_json_with_headers( + method, path, token=token, body=body, timeout=timeout + ) + return status_code, parsed_body🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/plugin/lib/http.py` around lines 76 - 109, The http_json function duplicates the request/response logic; refactor it to call http_json_with_headers(method, path, token=token, body=body, timeout=timeout) and return only the (status_code, parsed_or_text) part of the tuple, discarding the headers element; preserve the existing signature and behavior (including timeout and token handling) so all HTTP logic lives in http_json_with_headers and http_json becomes a thin wrapper that extracts the first two return values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/plugin/lib/http.py`:
- Around line 76-109: The http_json function duplicates the request/response
logic; refactor it to call http_json_with_headers(method, path, token=token,
body=body, timeout=timeout) and return only the (status_code, parsed_or_text)
part of the tuple, discarding the headers element; preserve the existing
signature and behavior (including timeout and token handling) so all HTTP logic
lives in http_json_with_headers and http_json becomes a thin wrapper that
extracts the first two return values.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 0fdbd98d-4be4-49d2-8fc5-6aacd531cd31
📒 Files selected for processing (2)
tests/plugin/lib/http.pytests/plugin/test_rate_limit.py
Close #82, close #83.
Summary by CodeRabbit
New Features
Tests
Chores
Documentation