Skip to content

feat: Add Retry-After HTTP header to lockout responses#1401

Open
rodrigobnogueira wants to merge 5 commits intojazzband:masterfrom
rodrigobnogueira:feature/retry-after-header
Open

feat: Add Retry-After HTTP header to lockout responses#1401
rodrigobnogueira wants to merge 5 commits intojazzband:masterfrom
rodrigobnogueira:feature/retry-after-header

Conversation

@rodrigobnogueira
Copy link

@rodrigobnogueira rodrigobnogueira commented Feb 21, 2026

What does this PR do?

When AXES_COOLOFF_TIME is configured, lockout responses now automatically include the Retry-After HTTP 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

  • Added _set_retry_after_header() helper that converts AXES_COOLOFF_TIME
    to total seconds and sets the Retry-After header on the response.
  • Called from the three Axes-controlled branches of get_lockout_response():
    JSON (XHR), template-rendered, and plain HttpResponse.

tests/test_helpers.py

  • Added 5 tests covering:
    • Retry-After present with a valid cool-off time (plain response)
    • Retry-After absent when AXES_COOLOFF_TIME is None
    • Retry-After present on JSON (XHR) responses
    • Retry-After present on template-rendered responses
    • Retry-After absent on redirect responses (AXES_LOCKOUT_URL)

docs/4_configuration.rst

  • Added a .. note:: block after the settings table documenting the
    Retry-After header behavior and which response types include it.

Design Decisions

Scenario Header set? Rationale
Plain HttpResponse Standard lockout response
JsonResponse (XHR) API consumers benefit most
Template render Browser can still use it
AXES_LOCKOUT_URL Redirect — destination controls headers
AXES_LOCKOUT_CALLABLE User owns the response entirely
AXES_COOLOFF_TIME=None Permanent ban — no retry window to advertise

Testing

All 351 existing + new tests pass with zero failures.

Before submitting

  • This PR fixes a typo or improves the docs (you can dismiss the other checks if that's the case).
  • Did you make sure to update the documentation with your changes?
  • Did you write any new necessary tests?

rodrigo.nogueira added 2 commits February 21, 2026 18:44
…OOLOFF_TIME` is configured, along with documentation and tests.
axes/helpers.py Outdated
return settings.AXES_PERMALOCK_MESSAGE


def _set_retry_after_header(response: HttpResponse, request: HttpRequest) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @rodrigobnogueira, would the Axes middleware be a good place for this?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @aleksihakli ,

moved Retry-After handling into AxesMiddleware so lockout response header logic is centralized there

axes/helpers.py Outdated
def _set_retry_after_header(response: HttpResponse, request: HttpRequest) -> None:
cool_off = get_cool_off(request)
if cool_off is not None:
response["Retry-After"] = str(int(cool_off.total_seconds()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could settings this header be toggled with a flag?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added AXES_ENABLE_RETRY_AFTER_HEADER (default False to not change current user's expectations) so projects can toggle this behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants