sanic/tests/test_exceptions.py

275 lines
8.2 KiB
Python
Raw Normal View History

import logging
2021-04-05 16:01:48 +01:00
import warnings
import pytest
2017-01-13 00:54:34 +00:00
from bs4 import BeautifulSoup
from sanic import Sanic
from sanic.exceptions import (
Forbidden,
InvalidUsage,
NotFound,
SanicException,
ServerError,
Unauthorized,
abort,
)
2016-10-14 23:36:58 +01:00
from sanic.response import text
New websockets (#2158) * First attempt at new Websockets implementation based on websockets >= 9.0, with sans-i/o features. Requires more work. * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * wip, update websockets code to new Sans/IO API * Refactored new websockets impl into own modules Incorporated other suggestions made by team * Another round of work on the new websockets impl * Added websocket_timeout support (matching previous/legacy support) * Lots more comments * Incorporated suggested changes from previous round of review * Changed RuntimeError usage to ServerError * Changed SanicException usage to ServerError * Removed some redundant asserts * Change remaining asserts to ServerErrors * Fixed some timeout handling issues * Fixed websocket.close() handling, and made it more robust * Made auto_close task smarter and more error-resilient * Made fail_connection routine smarter and more error-resilient * Further new websockets impl fixes * Update compatibility with Websockets v10 * Track server connection state in a more precise way * Try to handle the shutdown process more gracefully * Add a new end_connection() helper, to use as an alterative to close() or fail_connection() * Kill the auto-close task and keepalive-timeout task when sanic is shutdown * Deprecate WEBSOCKET_READ_LIMIT and WEBSOCKET_WRITE_LIMIT configs, they are not used in this implementation. * Change a warning message to debug level Remove default values for deprecated websocket parameters * Fix flake8 errors * Fix a couple of missed failing tests * remove websocket bench from examples * Integrate suggestions from code reviews Use Optional[T] instead of union[T,None] Fix mypy type logic errors change "is not None" to truthy checks where appropriate change "is None" to falsy checks were appropriate Add more debug logging when debug mode is on Change to using sanic.logger for debug logging rather than error_logger. * Fix long line lengths of debug messages Add some new debug messages when websocket IO is paused and unpaused for flow control Fix websocket example to use app.static() * remove unused import in websocket example app * re-run isort after Flake8 fixes Co-authored-by: Adam Hopkins <adam@amhopkins.com> Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-09-29 11:09:23 +01:00
from websockets.version import version as websockets_version
class SanicExceptionTestException(Exception):
pass
2018-12-30 11:18:06 +00:00
@pytest.fixture(scope="module")
def exception_app():
2018-12-30 11:18:06 +00:00
app = Sanic("test_exceptions")
2018-12-30 11:18:06 +00:00
@app.route("/")
def handler(request):
2018-12-30 11:18:06 +00:00
return text("OK")
2018-12-30 11:18:06 +00:00
@app.route("/error")
def handler_error(request):
raise ServerError("OK")
2018-12-30 11:18:06 +00:00
@app.route("/404")
def handler_404(request):
raise NotFound("OK")
2018-12-30 11:18:06 +00:00
@app.route("/403")
def handler_403(request):
raise Forbidden("Forbidden")
2018-12-30 11:18:06 +00:00
@app.route("/401")
def handler_401(request):
raise Unauthorized("Unauthorized")
2018-12-30 11:18:06 +00:00
@app.route("/401/basic")
def handler_401_basic(request):
raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic")
2018-12-30 11:18:06 +00:00
@app.route("/401/digest")
def handler_401_digest(request):
2018-12-30 11:18:06 +00:00
raise Unauthorized(
"Unauthorized",
scheme="Digest",
realm="Sanic",
qop="auth, auth-int",
algorithm="MD5",
nonce="abcdef",
opaque="zyxwvu",
)
@app.route("/401/bearer")
def handler_401_bearer(request):
raise Unauthorized("Unauthorized", scheme="Bearer")
2018-12-30 11:18:06 +00:00
@app.route("/invalid")
def handler_invalid(request):
raise InvalidUsage("OK")
2018-12-30 11:18:06 +00:00
@app.route("/abort/401")
def handler_401_error(request):
2021-04-05 16:01:48 +01:00
raise SanicException(status_code=401)
2018-12-30 11:18:06 +00:00
@app.route("/abort")
def handler_500_error(request):
2021-04-05 16:01:48 +01:00
raise SanicException(status_code=500)
@app.route("/old_abort")
def handler_old_abort_error(request):
2017-05-20 22:43:57 +01:00
abort(500)
2018-12-30 11:18:06 +00:00
@app.route("/abort/message")
2018-12-13 17:50:50 +00:00
def handler_abort_message(request):
2021-04-05 16:01:48 +01:00
raise SanicException(message="Custom Message", status_code=500)
2018-12-13 17:50:50 +00:00
2018-12-30 11:18:06 +00:00
@app.route("/divide_by_zero")
def handle_unhandled_exception(request):
_ = 1 / 0
2018-12-30 11:18:06 +00:00
@app.route("/error_in_error_handler_handler")
def custom_error_handler(request):
2018-12-30 11:18:06 +00:00
raise SanicExceptionTestException("Dummy message!")
@app.exception(SanicExceptionTestException)
def error_in_error_handler_handler(request, exception):
_ = 1 / 0
return app
2018-08-26 15:43:14 +01:00
def test_catch_exception_list(app):
2017-03-08 00:22:23 +00:00
@app.exception([SanicExceptionTestException, NotFound])
def exception_list(request, exception):
return text("ok")
2018-12-30 11:18:06 +00:00
@app.route("/")
2017-03-08 00:22:23 +00:00
def exception(request):
raise SanicExceptionTestException("You won't see me")
2018-12-30 11:18:06 +00:00
request, response = app.test_client.get("/random")
assert response.text == "ok"
2017-03-08 00:22:23 +00:00
2018-12-30 11:18:06 +00:00
request, response = app.test_client.get("/")
assert response.text == "ok"
2017-05-21 03:30:08 +01:00
def test_no_exception(exception_app):
"""Test that a route works without an exception"""
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/")
2016-10-14 23:36:58 +01:00
assert response.status == 200
2018-12-30 11:18:06 +00:00
assert response.text == "OK"
def test_server_error_exception(exception_app):
"""Test the built-in ServerError exception works"""
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/error")
2016-10-14 23:36:58 +01:00
assert response.status == 500
2017-05-21 03:30:08 +01:00
def test_invalid_usage_exception(exception_app):
"""Test the built-in InvalidUsage exception works"""
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/invalid")
2017-05-21 03:30:08 +01:00
assert response.status == 400
2016-10-14 23:36:58 +01:00
def test_not_found_exception(exception_app):
"""Test the built-in NotFound exception works"""
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/404")
2016-10-14 23:36:58 +01:00
assert response.status == 404
def test_forbidden_exception(exception_app):
"""Test the built-in Forbidden exception"""
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/403")
assert response.status == 403
def test_unauthorized_exception(exception_app):
"""Test the built-in Unauthorized exception"""
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/401")
assert response.status == 401
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/401/basic")
assert response.status == 401
2018-12-30 11:18:06 +00:00
assert response.headers.get("WWW-Authenticate") is not None
assert response.headers.get("WWW-Authenticate") == 'Basic realm="Sanic"'
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/401/digest")
assert response.status == 401
2018-12-30 11:18:06 +00:00
auth_header = response.headers.get("WWW-Authenticate")
assert auth_header is not None
2018-12-30 11:18:06 +00:00
assert auth_header.startswith("Digest")
assert 'qop="auth, auth-int"' in auth_header
assert 'algorithm="MD5"' in auth_header
assert 'nonce="abcdef"' in auth_header
assert 'opaque="zyxwvu"' in auth_header
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/401/bearer")
assert response.status == 401
2018-12-30 11:18:06 +00:00
assert response.headers.get("WWW-Authenticate") == "Bearer"
def test_handled_unhandled_exception(exception_app):
"""Test that an exception not built into sanic is handled"""
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/divide_by_zero")
assert response.status == 500
2018-12-30 11:18:06 +00:00
soup = BeautifulSoup(response.body, "html.parser")
assert "Internal Server Error" in soup.h1.text
2017-01-13 00:54:34 +00:00
message = " ".join(soup.p.text.split())
assert message == (
"The server encountered an internal error and "
2018-12-30 11:18:06 +00:00
"cannot complete your request."
)
2017-05-21 03:30:08 +01:00
def test_exception_in_exception_handler(exception_app):
"""Test that an exception thrown in an error handler is handled"""
2017-02-14 19:51:20 +00:00
request, response = exception_app.test_client.get(
2018-12-30 11:18:06 +00:00
"/error_in_error_handler_handler"
)
assert response.status == 500
2018-12-30 11:18:06 +00:00
assert response.body == b"An error occurred while handling an error"
def test_exception_in_exception_handler_debug_off(exception_app):
"""Test that an exception thrown in an error handler is handled"""
2017-02-14 19:51:20 +00:00
request, response = exception_app.test_client.get(
2018-12-30 11:18:06 +00:00
"/error_in_error_handler_handler", debug=False
)
assert response.status == 500
2018-12-30 11:18:06 +00:00
assert response.body == b"An error occurred while handling an error"
def test_exception_in_exception_handler_debug_on(exception_app):
"""Test that an exception thrown in an error handler is handled"""
2017-02-14 19:51:20 +00:00
request, response = exception_app.test_client.get(
2018-12-30 11:18:06 +00:00
"/error_in_error_handler_handler", debug=True
)
assert response.status == 500
2018-12-30 11:18:06 +00:00
assert response.body.startswith(b"Exception raised in exception ")
2017-05-21 03:30:08 +01:00
2021-04-05 16:01:48 +01:00
def test_sanic_exception(exception_app):
"""Test sanic exceptions are handled"""
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/abort/401")
assert response.status == 401
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/abort")
2017-05-21 03:30:08 +01:00
assert response.status == 500
2021-04-05 16:01:48 +01:00
# check fallback message
assert "Internal Server Error" in response.text
2018-12-13 17:50:50 +00:00
2018-12-30 11:18:06 +00:00
request, response = exception_app.test_client.get("/abort/message")
2018-12-13 17:50:50 +00:00
assert response.status == 500
2021-04-05 16:01:48 +01:00
assert "Custom Message" in response.text
with warnings.catch_warnings(record=True) as w:
request, response = exception_app.test_client.get("/old_abort")
assert response.status == 500
assert len(w) == 1 and "deprecated" in w[0].message.args[0]
def test_custom_exception_default_message(exception_app):
class TeaError(SanicException):
message = "Tempest in a teapot"
status_code = 418
exception_app.router.reset()
@exception_app.get("/tempest")
def tempest(_):
raise TeaError
_, response = exception_app.test_client.get("/tempest", debug=True)
assert response.status == 418
assert b"Tempest in a teapot" in response.body
def test_exception_in_ws_logged(caplog):
app = Sanic(__file__)
@app.websocket("/feed")
async def feed(request, ws):
raise Exception("...")
with caplog.at_level(logging.INFO):
app.test_client.websocket("/feed")
New websockets (#2158) * First attempt at new Websockets implementation based on websockets >= 9.0, with sans-i/o features. Requires more work. * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * wip, update websockets code to new Sans/IO API * Refactored new websockets impl into own modules Incorporated other suggestions made by team * Another round of work on the new websockets impl * Added websocket_timeout support (matching previous/legacy support) * Lots more comments * Incorporated suggested changes from previous round of review * Changed RuntimeError usage to ServerError * Changed SanicException usage to ServerError * Removed some redundant asserts * Change remaining asserts to ServerErrors * Fixed some timeout handling issues * Fixed websocket.close() handling, and made it more robust * Made auto_close task smarter and more error-resilient * Made fail_connection routine smarter and more error-resilient * Further new websockets impl fixes * Update compatibility with Websockets v10 * Track server connection state in a more precise way * Try to handle the shutdown process more gracefully * Add a new end_connection() helper, to use as an alterative to close() or fail_connection() * Kill the auto-close task and keepalive-timeout task when sanic is shutdown * Deprecate WEBSOCKET_READ_LIMIT and WEBSOCKET_WRITE_LIMIT configs, they are not used in this implementation. * Change a warning message to debug level Remove default values for deprecated websocket parameters * Fix flake8 errors * Fix a couple of missed failing tests * remove websocket bench from examples * Integrate suggestions from code reviews Use Optional[T] instead of union[T,None] Fix mypy type logic errors change "is not None" to truthy checks where appropriate change "is None" to falsy checks were appropriate Add more debug logging when debug mode is on Change to using sanic.logger for debug logging rather than error_logger. * Fix long line lengths of debug messages Add some new debug messages when websocket IO is paused and unpaused for flow control Fix websocket example to use app.static() * remove unused import in websocket example app * re-run isort after Flake8 fixes Co-authored-by: Adam Hopkins <adam@amhopkins.com> Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-09-29 11:09:23 +01:00
# Websockets v10.0 and above output an additional
# INFO message when a ws connection is accepted
ws_version_parts = websockets_version.split(".")
ws_major = int(ws_version_parts[0])
record_index = 2 if ws_major >= 10 else 1
assert caplog.record_tuples[record_index][0] == "sanic.error"
assert caplog.record_tuples[record_index][1] == logging.ERROR
assert (
New websockets (#2158) * First attempt at new Websockets implementation based on websockets >= 9.0, with sans-i/o features. Requires more work. * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * Update sanic/websocket.py Co-authored-by: Adam Hopkins <adam@amhopkins.com> * wip, update websockets code to new Sans/IO API * Refactored new websockets impl into own modules Incorporated other suggestions made by team * Another round of work on the new websockets impl * Added websocket_timeout support (matching previous/legacy support) * Lots more comments * Incorporated suggested changes from previous round of review * Changed RuntimeError usage to ServerError * Changed SanicException usage to ServerError * Removed some redundant asserts * Change remaining asserts to ServerErrors * Fixed some timeout handling issues * Fixed websocket.close() handling, and made it more robust * Made auto_close task smarter and more error-resilient * Made fail_connection routine smarter and more error-resilient * Further new websockets impl fixes * Update compatibility with Websockets v10 * Track server connection state in a more precise way * Try to handle the shutdown process more gracefully * Add a new end_connection() helper, to use as an alterative to close() or fail_connection() * Kill the auto-close task and keepalive-timeout task when sanic is shutdown * Deprecate WEBSOCKET_READ_LIMIT and WEBSOCKET_WRITE_LIMIT configs, they are not used in this implementation. * Change a warning message to debug level Remove default values for deprecated websocket parameters * Fix flake8 errors * Fix a couple of missed failing tests * remove websocket bench from examples * Integrate suggestions from code reviews Use Optional[T] instead of union[T,None] Fix mypy type logic errors change "is not None" to truthy checks where appropriate change "is None" to falsy checks were appropriate Add more debug logging when debug mode is on Change to using sanic.logger for debug logging rather than error_logger. * Fix long line lengths of debug messages Add some new debug messages when websocket IO is paused and unpaused for flow control Fix websocket example to use app.static() * remove unused import in websocket example app * re-run isort after Flake8 fixes Co-authored-by: Adam Hopkins <adam@amhopkins.com> Co-authored-by: Adam Hopkins <admhpkns@gmail.com>
2021-09-29 11:09:23 +01:00
"Exception occurred while handling uri:"
in caplog.record_tuples[record_index][2]
)