🐛 improve exception handling

This commit is contained in:
Joe Kaufeld 2024-08-12 11:54:16 -04:00
parent 8e37e58a99
commit 23360a3d91
4 changed files with 66 additions and 34 deletions

View File

@ -1,4 +1,5 @@
from spiderweb import WebServer from spiderweb import WebServer
from spiderweb.exceptions import ServerError
from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse
@ -30,7 +31,7 @@ def json(request):
@app.route("/error") @app.route("/error")
def error(request): def error(request):
return HttpResponse(status_code=500, body="Internal Server Error") raise ServerError
@app.route("/middleware") @app.route("/middleware")

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "spiderweb" name = "spiderweb"
version = "0.4.0" version = "0.5.0"
description = "A small web framework, just big enough to hold your average spider." description = "A small web framework, just big enough to hold your average spider."
authors = ["Joe Kaufeld <opensource@joekaufeld.com>"] authors = ["Joe Kaufeld <opensource@joekaufeld.com>"]
readme = "README.md" readme = "README.md"

View File

@ -1,6 +1,7 @@
class SpiderwebException(Exception): class SpiderwebException(Exception):
# parent error class; all child exceptions should inherit from this # parent error class; all child exceptions should inherit from this
pass def __str__(self):
return f"{self.__class__.__name__}({self.code}, {self.msg})"
class SpiderwebNetworkException(SpiderwebException): class SpiderwebNetworkException(SpiderwebException):
@ -11,14 +12,46 @@ class SpiderwebNetworkException(SpiderwebException):
self.msg = msg self.msg = msg
self.desc = desc self.desc = desc
def __str__(self):
return f"{self.__class__.__name__}({self.code}, {self.msg})"
class APIError(SpiderwebNetworkException): class APIError(SpiderwebNetworkException):
pass pass
class NotFound(SpiderwebNetworkException):
def __init__(self):
self.code = 404
self.msg = "Not Found"
self.desc = "The requested resource could not be found"
class BadRequest(SpiderwebNetworkException):
def __init__(self, desc=None):
self.code = 400
self.msg = "Bad Request"
self.desc = desc if desc else "The request could not be understood by the server"
class Unauthorized(SpiderwebNetworkException):
def __init__(self, desc=None):
self.code = 401
self.msg = "Unauthorized"
self.desc = desc if desc else "The request requires user authentication"
class Forbidden(SpiderwebNetworkException):
def __init__(self, desc=None):
self.code = 403
self.msg = "Forbidden"
self.desc = desc if desc else "You are not allowed to access this resource"
class ServerError(SpiderwebNetworkException):
def __init__(self, desc=None):
self.code = 500
self.msg = "Internal Server Error"
self.desc = desc if desc else "The server has encountered an error"
class ConfigError(SpiderwebException): class ConfigError(SpiderwebException):
pass pass

View File

@ -5,7 +5,6 @@
import json import json
import re import re
import signal import signal
import sys
import time import time
import traceback import traceback
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
@ -23,7 +22,7 @@ from spiderweb.exceptions import (
ConfigError, ConfigError,
ParseError, ParseError,
GeneralException, GeneralException,
NoResponseError, UnusedMiddleware, NoResponseError, UnusedMiddleware, SpiderwebNetworkException, NotFound,
) )
from spiderweb.request import Request from spiderweb.request import Request
from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse from spiderweb.response import HttpResponse, JsonResponse, TemplateResponse, RedirectResponse
@ -142,11 +141,8 @@ class WebServer(HTTPServer):
raise ParseError(f"Unknown converter {converter}") raise ParseError(f"Unknown converter {converter}")
else: else:
converter = StrConverter # noqa: F405 converter = StrConverter # noqa: F405
parts[i] = r"(?P<%s>%s)" % ( parts[i] = rf"(?P<{name}__{str(converter.__name__)}>{converter.regex})"
f"{name}__{str(converter.__name__)}", return re.compile(rf"^{'/'.join(parts)}$")
converter.regex,
)
return re.compile(r"^%s$" % "/".join(parts))
def check_for_route_duplicates(self, path: str): def check_for_route_duplicates(self, path: str):
if self.convert_path(path) in self.handler_class._routes: if self.convert_path(path) in self.handler_class._routes:
@ -233,6 +229,9 @@ class WebServer(HTTPServer):
class RequestHandler(BaseHTTPRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
# I can't help the naming convention of these because that's what # I can't help the naming convention of these because that's what
# BaseHTTPRequestHandler uses for some weird reason # BaseHTTPRequestHandler uses for some weird reason
# These stop pycharm from complaining about these not existing. They're
# injected by the WebServer class at runtime
_routes = {} _routes = {}
middleware = [] middleware = []
@ -269,7 +268,7 @@ class RequestHandler(BaseHTTPRequestHandler):
return self._routes[option], convert_match_to_dict( return self._routes[option], convert_match_to_dict(
match_data.groupdict() match_data.groupdict()
) )
raise APIError(404, "No route found") raise NotFound()
def get_error_route(self, code: int) -> Callable: def get_error_route(self, code: int) -> Callable:
try: try:
@ -319,10 +318,10 @@ class RequestHandler(BaseHTTPRequestHandler):
self.middleware.remove(middleware) self.middleware.remove(middleware)
continue continue
def prepare_response(self, request, resp) -> HttpResponse: def prepare_and_fire_response(self, request, resp) -> None:
try: try:
if isinstance(resp, dict): if isinstance(resp, dict):
self.fire_response(JsonResponse(data=resp)) self.fire_response(request, JsonResponse(data=resp))
if isinstance(resp, TemplateResponse): if isinstance(resp, TemplateResponse):
if hasattr(self, "env"): # injected from above if hasattr(self, "env"): # injected from above
resp.set_template_loader(self.env) resp.set_template_loader(self.env)
@ -335,17 +334,25 @@ class RequestHandler(BaseHTTPRequestHandler):
except APIError: except APIError:
raise raise
except ConnectionAbortedError as e:
log.error(f"GET {self.path} : {e}")
except Exception: except Exception:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
self.fire_response(request, self.get_error_route(500)(request)) self.fire_response(request, self.get_error_route(500)(request))
def send_error_response(self, request: Request, e: SpiderwebNetworkException):
try:
self.send_error(e.code, e.msg, e.desc)
except ConnectionAbortedError as e:
log.error(f"{request.method} {self.path} : {e}")
def handle_request(self, request): def handle_request(self, request):
request.url = urlparse.urlparse(request.path) request.url = urlparse.urlparse(request.path)
try:
handler, additional_args = self.get_route(request.url.path) handler, additional_args = self.get_route(request.url.path)
except NotFound:
handler = self.get_error_route(404)
additional_args = {}
if request.url.query: if request.url.query:
params = urlparse.parse_qs(request.url.query) params = urlparse.parse_qs(request.url.query)
@ -363,18 +370,9 @@ class RequestHandler(BaseHTTPRequestHandler):
resp = handler(request, **additional_args) resp = handler(request, **additional_args)
if resp is None: if resp is None:
raise NoResponseError(f"View {handler} returned None.") raise NoResponseError(f"View {handler} returned None.")
if isinstance(resp, dict): # run the response through the middleware and send it
self.fire_response(request, JsonResponse(data=resp)) self.prepare_and_fire_response(request, resp)
if isinstance(resp, TemplateResponse):
if hasattr(self, "env"): # injected from above
resp.set_template_loader(self.env)
self.process_response_middleware(request, resp)
self.fire_response(request, resp)
else: else:
raise APIError(404) raise SpiderwebNetworkException(404)
except APIError as e: except SpiderwebNetworkException as e:
try: self.send_error_response(request, e)
self.send_error(e.code, e.msg, e.desc)
except ConnectionAbortedError as e:
log.error(f"GET {self.path} : {e}")