feat: Add Retry-After HTTP header to lockout responses#1401
feat: Add Retry-After HTTP header to lockout responses#1401rodrigobnogueira wants to merge 8 commits intojazzband:masterfrom
Retry-After HTTP header to lockout responses#1401Conversation
…OOLOFF_TIME` is configured, along with documentation and tests.
…er-header # Conflicts: # axes/middleware.py
| @staticmethod | ||
| def _set_retry_after_header( | ||
| response: HttpResponse, request: HttpRequest | ||
| ) -> HttpResponse: | ||
| if not settings.AXES_ENABLE_RETRY_AFTER_HEADER: | ||
| return response | ||
|
|
||
| if settings.AXES_LOCKOUT_CALLABLE or settings.AXES_LOCKOUT_URL: | ||
| return response | ||
|
|
||
| cool_off = get_cool_off(request) | ||
| if cool_off is not None: | ||
| response["Retry-After"] = str(int(cool_off.total_seconds())) | ||
|
|
||
| return response | ||
|
|
There was a problem hiding this comment.
Wouldl it make sense to just write this as a simpler single if-then method? Why do we need to skip setting the header on callable and lockout URLs?
I think a better location for this function could be the axes.helpers module since it contains other similar implementations for getters and this can be just a generic function (although it is quite middleware-ish since it uses both response and request objects).
Since this method actually mutates the response object without copying it it should probably just set the header, no need to return a new object really:
| @staticmethod | |
| def _set_retry_after_header( | |
| response: HttpResponse, request: HttpRequest | |
| ) -> HttpResponse: | |
| if not settings.AXES_ENABLE_RETRY_AFTER_HEADER: | |
| return response | |
| if settings.AXES_LOCKOUT_CALLABLE or settings.AXES_LOCKOUT_URL: | |
| return response | |
| cool_off = get_cool_off(request) | |
| if cool_off is not None: | |
| response["Retry-After"] = str(int(cool_off.total_seconds())) | |
| return response | |
| def set_retry_after_header( | |
| request: HttpRequest, response: HttpResponse | |
| ): | |
| if settings.AXES_ENABLE_RETRY_AFTER_HEADER: | |
| response["Retry-After"] = str(int(get_cool_off(request).total_seconds())) |
Then it can just be simply invoked here or maybe even preferably in axes.helpers.get_lockout_response where it could live in one place instead of multiple:
| @staticmethod | |
| def _set_retry_after_header( | |
| response: HttpResponse, request: HttpRequest | |
| ) -> HttpResponse: | |
| if not settings.AXES_ENABLE_RETRY_AFTER_HEADER: | |
| return response | |
| if settings.AXES_LOCKOUT_CALLABLE or settings.AXES_LOCKOUT_URL: | |
| return response | |
| cool_off = get_cool_off(request) | |
| if cool_off is not None: | |
| response["Retry-After"] = str(int(cool_off.total_seconds())) | |
| return response | |
| set_retry_after_header(request, response) |
This is easier to use and simple to test.
There was a problem hiding this comment.
done. It is simpler now. thanks
There was a problem hiding this comment.
Let me know if the implementation is better now.
…er-header # Conflicts: # docs/4_configuration.rst
349be0b to
a4b34ac
Compare
a4b34ac to
fd78bdf
Compare
What does this PR do?
When
AXES_COOLOFF_TIMEis configured, lockout responses now automatically include theRetry-AfterHTTP header (RFC 7231 §7.1.3) with the cool-off duration in seconds. This helps clients (browsers, API consumers, bots)understand how long to wait before retrying.
Changes
axes/helpers.py_set_retry_after_header()helper that convertsAXES_COOLOFF_TIMEto total seconds and sets the
Retry-Afterheader on the response.get_lockout_response():JSON (XHR), template-rendered, and plain
HttpResponse.tests/test_helpers.pyRetry-Afterpresent with a valid cool-off time (plain response)Retry-Afterabsent whenAXES_COOLOFF_TIMEisNoneRetry-Afterpresent on JSON (XHR) responsesRetry-Afterpresent on template-rendered responsesRetry-Afterabsent on redirect responses (AXES_LOCKOUT_URL)docs/4_configuration.rst.. note::block after the settings table documenting theRetry-Afterheader behavior and which response types include it.Design Decisions
HttpResponseJsonResponse(XHR)AXES_LOCKOUT_URLAXES_LOCKOUT_CALLABLEAXES_COOLOFF_TIME=NoneTesting
All 351 existing + new tests pass with zero failures.
Before submitting