8673021ad4
* Allow non-conforming ErrorHandlers (#2259) * Allow non-conforming ErrorHandlers * Rename to legacy lookup * Updated depnotice * Bump version * Fix formatting * Remove unused import * Fix error messages * Add error format commit and merge conflicts * Make HTTP connections start in IDLE stage, avoiding delays and error messages (#2268) * Make all new connections start in IDLE stage, and switch to REQUEST stage only once any bytes are received from client. This makes new connections without any request obey keepalive timeout rather than request timeout like they currently do. * Revert typo * Remove request timeout endpoint test which is no longer working (still tested by mocking). Fix mock timeout test setup. Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com> * Bump version * Add error format from config replacement objects * Cleanup mistaken print statement * Cleanup reversions * Bump version Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com> Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com>
275 lines
8.2 KiB
Python
275 lines
8.2 KiB
Python
import logging
|
|
import warnings
|
|
|
|
import pytest
|
|
|
|
from bs4 import BeautifulSoup
|
|
from websockets.version import version as websockets_version
|
|
|
|
from sanic import Sanic
|
|
from sanic.exceptions import (
|
|
Forbidden,
|
|
InvalidUsage,
|
|
NotFound,
|
|
SanicException,
|
|
ServerError,
|
|
Unauthorized,
|
|
abort,
|
|
)
|
|
from sanic.response import text
|
|
|
|
|
|
class SanicExceptionTestException(Exception):
|
|
pass
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def exception_app():
|
|
app = Sanic("test_exceptions")
|
|
|
|
@app.route("/")
|
|
def handler(request):
|
|
return text("OK")
|
|
|
|
@app.route("/error")
|
|
def handler_error(request):
|
|
raise ServerError("OK")
|
|
|
|
@app.route("/404")
|
|
def handler_404(request):
|
|
raise NotFound("OK")
|
|
|
|
@app.route("/403")
|
|
def handler_403(request):
|
|
raise Forbidden("Forbidden")
|
|
|
|
@app.route("/401")
|
|
def handler_401(request):
|
|
raise Unauthorized("Unauthorized")
|
|
|
|
@app.route("/401/basic")
|
|
def handler_401_basic(request):
|
|
raise Unauthorized("Unauthorized", scheme="Basic", realm="Sanic")
|
|
|
|
@app.route("/401/digest")
|
|
def handler_401_digest(request):
|
|
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")
|
|
|
|
@app.route("/invalid")
|
|
def handler_invalid(request):
|
|
raise InvalidUsage("OK")
|
|
|
|
@app.route("/abort/401")
|
|
def handler_401_error(request):
|
|
raise SanicException(status_code=401)
|
|
|
|
@app.route("/abort")
|
|
def handler_500_error(request):
|
|
raise SanicException(status_code=500)
|
|
|
|
@app.route("/old_abort")
|
|
def handler_old_abort_error(request):
|
|
abort(500)
|
|
|
|
@app.route("/abort/message")
|
|
def handler_abort_message(request):
|
|
raise SanicException(message="Custom Message", status_code=500)
|
|
|
|
@app.route("/divide_by_zero")
|
|
def handle_unhandled_exception(request):
|
|
_ = 1 / 0
|
|
|
|
@app.route("/error_in_error_handler_handler")
|
|
def custom_error_handler(request):
|
|
raise SanicExceptionTestException("Dummy message!")
|
|
|
|
@app.exception(SanicExceptionTestException)
|
|
def error_in_error_handler_handler(request, exception):
|
|
_ = 1 / 0
|
|
|
|
return app
|
|
|
|
|
|
def test_catch_exception_list(app):
|
|
@app.exception([SanicExceptionTestException, NotFound])
|
|
def exception_list(request, exception):
|
|
return text("ok")
|
|
|
|
@app.route("/")
|
|
def exception(request):
|
|
raise SanicExceptionTestException("You won't see me")
|
|
|
|
request, response = app.test_client.get("/random")
|
|
assert response.text == "ok"
|
|
|
|
request, response = app.test_client.get("/")
|
|
assert response.text == "ok"
|
|
|
|
|
|
def test_no_exception(exception_app):
|
|
"""Test that a route works without an exception"""
|
|
request, response = exception_app.test_client.get("/")
|
|
assert response.status == 200
|
|
assert response.text == "OK"
|
|
|
|
|
|
def test_server_error_exception(exception_app):
|
|
"""Test the built-in ServerError exception works"""
|
|
request, response = exception_app.test_client.get("/error")
|
|
assert response.status == 500
|
|
|
|
|
|
def test_invalid_usage_exception(exception_app):
|
|
"""Test the built-in InvalidUsage exception works"""
|
|
request, response = exception_app.test_client.get("/invalid")
|
|
assert response.status == 400
|
|
|
|
|
|
def test_not_found_exception(exception_app):
|
|
"""Test the built-in NotFound exception works"""
|
|
request, response = exception_app.test_client.get("/404")
|
|
assert response.status == 404
|
|
|
|
|
|
def test_forbidden_exception(exception_app):
|
|
"""Test the built-in Forbidden exception"""
|
|
request, response = exception_app.test_client.get("/403")
|
|
assert response.status == 403
|
|
|
|
|
|
def test_unauthorized_exception(exception_app):
|
|
"""Test the built-in Unauthorized exception"""
|
|
request, response = exception_app.test_client.get("/401")
|
|
assert response.status == 401
|
|
|
|
request, response = exception_app.test_client.get("/401/basic")
|
|
assert response.status == 401
|
|
assert response.headers.get("WWW-Authenticate") is not None
|
|
assert response.headers.get("WWW-Authenticate") == 'Basic realm="Sanic"'
|
|
|
|
request, response = exception_app.test_client.get("/401/digest")
|
|
assert response.status == 401
|
|
|
|
auth_header = response.headers.get("WWW-Authenticate")
|
|
assert auth_header is not None
|
|
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
|
|
|
|
request, response = exception_app.test_client.get("/401/bearer")
|
|
assert response.status == 401
|
|
assert response.headers.get("WWW-Authenticate") == "Bearer"
|
|
|
|
|
|
def test_handled_unhandled_exception(exception_app):
|
|
"""Test that an exception not built into sanic is handled"""
|
|
request, response = exception_app.test_client.get("/divide_by_zero")
|
|
assert response.status == 500
|
|
soup = BeautifulSoup(response.body, "html.parser")
|
|
assert "Internal Server Error" in soup.h1.text
|
|
|
|
message = " ".join(soup.p.text.split())
|
|
assert message == (
|
|
"The server encountered an internal error and "
|
|
"cannot complete your request."
|
|
)
|
|
|
|
|
|
def test_exception_in_exception_handler(exception_app):
|
|
"""Test that an exception thrown in an error handler is handled"""
|
|
request, response = exception_app.test_client.get(
|
|
"/error_in_error_handler_handler"
|
|
)
|
|
assert response.status == 500
|
|
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"""
|
|
request, response = exception_app.test_client.get(
|
|
"/error_in_error_handler_handler", debug=False
|
|
)
|
|
assert response.status == 500
|
|
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"""
|
|
request, response = exception_app.test_client.get(
|
|
"/error_in_error_handler_handler", debug=True
|
|
)
|
|
assert response.status == 500
|
|
assert response.body.startswith(b"Exception raised in exception ")
|
|
|
|
|
|
def test_sanic_exception(exception_app):
|
|
"""Test sanic exceptions are handled"""
|
|
request, response = exception_app.test_client.get("/abort/401")
|
|
assert response.status == 401
|
|
|
|
request, response = exception_app.test_client.get("/abort")
|
|
assert response.status == 500
|
|
# check fallback message
|
|
assert "Internal Server Error" in response.text
|
|
|
|
request, response = exception_app.test_client.get("/abort/message")
|
|
assert response.status == 500
|
|
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")
|
|
# 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 (
|
|
"Exception occurred while handling uri:"
|
|
in caplog.record_tuples[record_index][2]
|
|
)
|