Repository navigation
Fix argument order in the async middleware lockout response - #1494
Merged
camilonova merged 1 commit intoOct 30, 2026
Merged
camilonova merged 1 commit into
camilonova merged 1 commit into
Conversation
AxesMiddleware.__call__ passes three arguments to axes.helpers.get_lockout_response, but AxesMiddleware.__acall__ still passes two. get_lockout_response is declared in axes/helpers.py as get_lockout_response(request, original_response=None, credentials=None), so the async path binds the credentials dict to original_response and leaves credentials as None. Under ASGI that breaks two documented behaviours. AXES_LOCKOUT_CALLABLE receives the credentials dict in the slot where docs/4_configuration.rst says it receives the original HttpResponse, and it never receives the credentials at all. With no lockout callable configured, get_client_username and get_failure_limit are called with credentials set to None, so an AXES_USERNAME_CALLABLE that reads the credentials dict raises AttributeError instead of rendering the lockout page. The async branch now passes (request, response, credentials) like the sync branch does. AxesMiddleware.__acall__ had no test coverage, so this adds three tests for it.
camilonova
approved these changes
Oct 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
AxesMiddleware.__acall__passes the wrong arguments toaxes.helpers.get_lockout_response, so under ASGI every lockout response is built from the credentials dict in the slot meant for the original response, with no credentials at all.get_lockout_responseis declared asget_lockout_response(request, original_response=None, credentials=None)ataxes/helpers.py:515. The sync branch calls it correctly ataxes/middleware.py:51. The async branch ataxes/middleware.py:61-65still calls(request, credentials), which was right until 04fd39f insertedoriginal_responseas the second parameter; that commit updated__call__and only reformatted__acall__.Two documented behaviours break as a result. A custom
AXES_LOCKOUT_CALLABLEreceives the credentials dict asoriginal_responseandNoneascredentials, whiledocs/4_configuration.rstdocumentsdef generate_lockout_response(request: HttpRequest, original_response: HttpResponse, credentials: dict). With no lockout callable configured,get_lockout_responsethen runsget_client_username(request, None)andget_failure_limit(request, None), so anAXES_USERNAME_CALLABLEthat reads the credentials dict raisesAttributeError: 'NoneType' object has no attribute 'get'and the client gets a 500 instead of the lockout page.The fix is one line: the async branch now passes
(request, response, credentials)like the sync branch.__acall__had no test coverage, so this adds three tests totests/test_middleware.py. They drive the middleware throughasync_to_sync(AxesMiddleware(get_response))with an asyncget_response, so the real coroutine dispatch in__call__runs rather than being bypassed.Verified on Python 3.11 with Django 5.2 against sqlite, the configuration
test.ymlruns.pytestgives 380 passed, 19 subtests passed. On an unmodifiedmastercheckout it gives 377 passed, 19 subtests passed.With
axes/middleware.pyreverted tomasterand the new tests in place, both regression tests fail:test_async_lockout_response_with_axes_callable_usernamewithAttributeError: 'NoneType' object has no attribute 'get'raised fromaxes/helpers.py:546, andtest_async_lockout_callable_gets_original_response_and_credentialswithAssertionError: Lists differ: [({...: 'username'}, None)] != [(<HttpResponse status_code=200, "text/html; charset=utf-8">, {...: 'username'})], which shows the credentials landing in theoriginal_responseposition.black --check --diff axesleaves 41 files unchanged,prospector axesfinds 0 messages, andmypy axesfinds no issues in 41 source files.mypy .reports one error on the pre-existingget_usernamehelper intests/test_middleware.py; it sits at line 10 on an unmodified checkout and at line 11 here only because this adds an import above it.No documentation change is needed:
docs/4_configuration.rstanddocs/5_customization.rstalready describe the three argument contract that this restores. I have leftCHANGES.rstalone because every entry there landed in aVersion x.y.zcommit, but I am glad to add one if you would rather contributors did that.Before submitting