Conversation
Fixes cherrypy#797. The except-block that's meant to log an unexpected failure while sending a 503 response called self.server.error_log(...), but self is the HTTPServer instance itself here, and HTTPServer has no server attribute -- that belongs to a different class, HTTPConnection (whose self.server does point back to its owning HTTPServer). So instead of logging the original exception and keeping the background thread alive (which is exactly what the comment right above says this code is for), it raised a fresh AttributeError from inside the except-block. Call self.error_log(...) directly, matching HTTPServer's own method.
Documentation build overview
12 files changed ·
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #840 +/- ##
==========================================
- Coverage 78.37% 78.36% -0.02%
==========================================
Files 41 41
Lines 4791 4811 +20
Branches 548 549 +1
==========================================
+ Hits 3755 3770 +15
- Misses 899 902 +3
- Partials 137 139 +2 |
Cheroot requires a Towncrier news fragment for every user-visible change. This adds the bugfix entry for the AttributeError that HTTPServer._serve_unservicable() raised from its own error handler, so the fix shows up in the release notes.
for more information, see https://pre-commit.ci
3e98d66 to
878e3aa
Compare
|
@Tatamis Thanks for the submission. This is actually the third time a fix for this problem has been submitted: see #825 and #830, both still open. These were incomplete submissions for which I left comments on how to complete. Your submission is also incomplete in that it does not use our template for new PRs - |
| ``AttributeError`` from within the except-block meant to keep this | ||
| background thread alive. See issue #797. | ||
| """ | ||
| from .. import server as server_module |
There was a problem hiding this comment.
This kind of local, whole-module import shouldn't be needed. _STOPPING_FOR_INTERRUPT can just move into the top-level from ..server import ... block. For HTTPRequest, rather than importing the module just to patch an attribute on it, use mocker.patch('cheroot.server.HTTPRequest', ...) instead — no import needed at all.
| @@ -0,0 +1,5 @@ | |||
| Fixed :py:meth:`HTTPServer._serve_unservicable() <cheroot.server.HTTPServer._serve_unservicable>` | |||
| raising an unrelated :py:exc:`AttributeError` instead of logging the original failure | |||
There was a problem hiding this comment.
| raising an unrelated :py:exc:`AttributeError` instead of logging the original failure | |
| raising an unrelated :py:exc:`AttributeError` instead of logging the original failure, |
julianz- pointed out the regression test imported the whole `server`
module locally just to reach two names. _STOPPING_FOR_INTERRUPT moves
into the top-level `from ..server import (...)` block, and the
HTTPRequest patch now goes through mocker.patch('cheroot.server.HTTPRequest',
...) directly instead of patching an attribute on an imported module
object.
Also picked up the suggested comma in the changelog fragment.
|
Pushed 9e7e3c0 addressing this: |
|
Thanks for update. @webknjaz please take a look. |
| assert response.status_code == HTTPStatus.SERVICE_UNAVAILABLE | ||
|
|
||
|
|
||
| def test_serve_unservicable_logs_errors_without_crashing(mocker, capsys): |
There was a problem hiding this comment.
Add type annotations to all newly added test functions.
| attribute -- that belongs to ``HTTPConnection``, which is a different | ||
| class. So instead of logging the original failure, it raised a fresh | ||
| ``AttributeError`` from within the except-block meant to keep this | ||
| background thread alive. See issue #797. |
There was a problem hiding this comment.
This sounds more like a code comment rather than a docsring, which has a different audience and purpose.
| ``AttributeError`` from within the except-block meant to keep this | ||
| background thread alive. See issue #797. | ||
| """ | ||
| httpserver = HTTPServer.__new__(HTTPServer) |
There was a problem hiding this comment.
This object creation style doesn't seem to exist anywhere. I'd rather not use it.
| httpserver = HTTPServer.__new__(HTTPServer) | ||
| httpserver.ready = True | ||
| httpserver._unservicable_conns = queue.Queue() | ||
| fake_conn = mocker.Mock() |
There was a problem hiding this comment.
Please don't mock everything. We need to use proper e2e tests as much as possible, only mocking external things/side effects where appropriate.
| raising an unrelated :py:exc:`AttributeError` instead of logging the original failure, | ||
| when sending a ``503 Service Unavailable`` response itself raised an unexpected exception. | ||
|
|
||
| -- by :user:`Tatamis` |
There was a problem hiding this comment.
We should probably credit the contributors from #825 and #830 in the test rewrite commit and here: https://hynek.me/til/easier-crediting-contributors-github/
There was a problem hiding this comment.
Release notes are generally supposed to be targeting the end-users. This seems to be focusing on the low-level internals. Could you think of making this text more high-level?
Run the test against a real overloaded server instead of mocking the internals of `HTTPServer._serve_unservicable()`. Only the failure to send the 503 response is simulated. The test checks that the original error is logged and that later connections still get their 503, which is the behavior that broke with the `AttributeError`. Move the overload server setup into a fixture shared with `test_overload_results_in_suitable_http_error`, and add type annotations to the new and touched test functions. The approach draws on the tests from cherrypy#825 and cherrypy#830. Co-authored-by: era <283892076+avinashkamat48@users.noreply.github.com> Co-authored-by: Andrew Dang <112019350+andrewkernel@users.noreply.github.com>
Describe the visible effect of the bug rather than the internals, and credit the authors of the earlier fixes from cherrypy#825 and cherrypy#830. Co-authored-by: era <283892076+avinashkamat48@users.noreply.github.com> Co-authored-by: Andrew Dang <112019350+andrewkernel@users.noreply.github.com>
|
@webknjaz thanks for the detailed review. I pushed 2753fd9 (two commits) that addresses all of it:
I ran the changed tests on Python 3.8 and 3.13, and the repo's pre-commit hooks (flake8 with wemake, ruff, mypy, pylint) pass on the changed files. The PR description is updated to match. Please take another look when you have time. |
| Fixed the server stopping to answer overloaded connections with | ||
| ``503 Service Unavailable`` after a failure to send one of these responses. | ||
| The failure is now logged and the following connections are still answered. | ||
|
|
||
| -- by :user:`Tatamis`, :user:`avinashkamat48` and :user:`andrewkernel` |
There was a problem hiding this comment.
| Fixed the server stopping to answer overloaded connections with | |
| ``503 Service Unavailable`` after a failure to send one of these responses. | |
| The failure is now logged and the following connections are still answered. | |
| -- by :user:`Tatamis`, :user:`avinashkamat48` and :user:`andrewkernel` | |
| Fixed the server's ability to send ``503 Service Unavailable`` whenever the server becomes too busy. Failures to send caused the handler for this message to crash leaving new connections during busy times unanswered. Any failure in sending this message is now logged, and subsequent connections continue to be answered. | |
| -- by :user:`Tatamis`, :user:`avinashkamat48` and :user:`andrewkernel` |
There was a problem hiding this comment.
Think this easier to understand as an end user even if it still hints at the internal message handler.
There was a problem hiding this comment.
Thanks, applied as suggested in b9967da (rewrapped the lines only). That commit also fixes the docs build: the new test annotations refer to pytest.MonkeyPatch and pytest.CaptureFixture, which Sphinx can't resolve in nitpicky mode, so I added them to nitpick_ignore, like the other unresolvable references.
The test modules are documented with `autodoc`, and the new test annotations refer to `pytest.MonkeyPatch` and `pytest.CaptureFixture`. Sphinx resolves these to the private `_pytest` package, which has no entries in any inventory, so the nitpicky docs build fails. Ignore them like the other unresolvable references.
Co-authored-by: era <283892076+avinashkamat48@users.noreply.github.com> Co-authored-by: Andrew Dang <112019350+andrewkernel@users.noreply.github.com>
What kind of change does this PR introduce?
What do these changes do?
HTTPServer._serve_unservicable()catches unexpected failures while sending a503 Service Unavailableresponse and logs them, deliberately, so that thebackground thread stays alive and later connections still get their 503:
The problem is
self.server. Hereselfis theHTTPServeritself, andHTTPServerhas noserverattribute — that belongs toHTTPConnection,whose
self.serverdoes point back at its owningHTTPServer. The two otherself.server.error_log(...)calls in this file live inHTTPConnection.communicate(), where they are correct.So the handler raises a fresh
AttributeErrorfrom inside the except-block,the original error is never logged, and the thread that is supposed to keep
answering overloaded connections dies — the exact opposite of what the comment
above it promises.
The fix is one line: call
self.error_log(...), whichHTTPServerdefines afew dozen lines further down in the same class.
I also added
test_overload_survives_failure_to_send_http_error, anend-to-end test that runs against a real overloaded server. Only the failure
to send the 503 is simulated, by making
HTTPRequest.simple_response()raiseonce. It checks that the original error is logged rather than an
AttributeError, and that the next client still receives its 503. Theoverload server setup now lives in a fixture that
test_overload_results_in_suitable_http_errorshares. The test design drawson #825 and #830, whose authors are credited in the commits and in the change
log entry.
I should mention: you noted this is the third PR for this bug, after #825 and
#830, and I only saw those after your comment. The one-line fix is necessarily
the same in all three. If you would rather land one of the earlier PRs, please
do — I am happy for this one to be closed, and can port the test over there
instead if that helps.
Are there changes in behavior for the user?
Nothing changes on the happy path. When sending a 503 fails, the original
exception is now written to the error log with its traceback, and the
unserviceable-connection thread keeps running instead of dying with an
unrelated
AttributeError. Anyone who wrote code against thatAttributeError(unlikely — it was a crash, not an API) would notice it is gone.
Is it a substantial burden for the maintainers to support this?
No. It swaps one attribute lookup for the method on the same class and adds a
test in the existing style of
test_server.py. There is nothing new to keepworking over the next five years, and the test pins the behaviour so the same
mistake cannot come back unnoticed.
Related issue number
Fixes #797. Same underlying bug as #825 and #830.
Checklist
and description in grammatically correct, complete sentences
runs against a real overloaded server
docs/) and inline docstrings reflect the changesdocs/changelog-fragments.d/797.bugfix.rst,named after the issue since the PR number was not known when I wrote it;
happy to rename it to
840.bugfix.rstif you preferTest run
pytest cheroot/test/test_server.py cheroot/test/test_conn.py cheroot/test/test_core.pypasses on Python 3.8 and 3.13 (95 passed, 11 skipped, 4 xfailed on each).
second client never gets its 503) and passes with it.
pre-commithooks (flake8with its plugins,ruff,mypy,pylint) pass on the changed files.