Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: update exception logging in CustomErrorHandler #246

Merged
merged 3 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions components/renku_data_services/base_api/error_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from sanic import HTTPResponse, Request, SanicException, json
from sanic.errorpages import BaseRenderer, TextRenderer
from sanic.handlers import ErrorHandler
from sanic.log import logger
from sanic_ext.exceptions import ValidationError
from sqlalchemy.exc import SQLAlchemyError

Expand All @@ -22,6 +21,7 @@ class BaseError(Protocol):
code: int
message: str
detail: Optional[str]
quiet: bool


class BaseErrorResponse(Protocol):
Expand Down Expand Up @@ -62,15 +62,9 @@ def __init__(self, api_spec: ApiSpec, base: type[BaseRenderer] = TextRenderer) -
self.api_spec = api_spec
super().__init__(base)

def _log_unhandled_exception(self, exception: Exception) -> None:
if self.debug:
logger.exception("An unknown or unhandled exception occurred", exc_info=exception)
logger.error("An unknown or unhandled exception of type %s occurred", type(exception).__name__)

def default(self, request: Request, exception: Exception) -> HTTPResponse:
"""Overrides the default error handler."""
formatted_exception = errors.BaseError()
logger.exception("An unknown or unhandled exception occurred", exc_info=exception)
match exception:
case errors.BaseError():
formatted_exception = exception
Expand All @@ -88,14 +82,15 @@ def default(self, request: Request, exception: Exception) -> HTTPResponse:
]
message = f"There are errors in the following fields, {', '.join(parts)}"
formatted_exception = errors.ValidationError(message=message)
case _:
self._log_unhandled_exception(exception)
case SanicException():
message = exception.message
if message == "" or message is None:
message = ", ".join([str(i) for i in exception.args])
formatted_exception = errors.BaseError(
message=message, status_code=exception.status_code, code=1000 + exception.status_code
message=message,
status_code=exception.status_code,
code=1000 + exception.status_code,
quiet=exception.quiet or False,
)
case SqliteError():
formatted_exception = errors.BaseError(
Expand Down Expand Up @@ -131,8 +126,7 @@ def default(self, request: Request, exception: Exception) -> HTTPResponse:
formatted_exception = errors.ValidationError(
message="The provided input is too large to be stored in the database"
)
case _:
self._log_unhandled_exception(exception)
self.log(request, formatted_exception)
return json(
self.api_spec.ErrorResponse(
error=self.api_spec.Error(
Expand Down
90 changes: 47 additions & 43 deletions components/renku_data_services/errors/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class BaseError(Exception):
status_code: int = 500
message: str = "An unexpected error occurred"
detail: Optional[str] = None
quiet: bool = False

def __repr__(self) -> str:
"""String representation of the error."""
Expand All @@ -22,29 +23,25 @@ def __str__(self) -> str:
return f"{self.__class__.__qualname__}: {self.message}"


@dataclass
class MissingResourceError(BaseError):
"""Raised when a resource is not found."""

code: int = 1404
status_code: int = 404
message: str = "The requested resource does not exist or cannot be found"
# ! IMPORTANT: keep this list ordered by HTTP status code.


@dataclass
class ConfigurationError(BaseError):
"""Raised when the server is not properly configured."""
class GeneralBadRequest(BaseError):
"""Raised for a 400 status code - when the server cannot or will not process the request."""

message: str = "The server is not properly configured and cannot run"
code: int = 1400
message: str = "The request is invalid, malformed or non-sensical and cannot be fulfilled."
status_code: int = 400


@dataclass
class ValidationError(BaseError):
"""Raised when the inputs or outputs are invalid."""
class NoDefaultPoolAccessError(BaseError):
"""Raised when the user does not have the right to access the default resource pool."""

code: int = 1422
message: str = "The provided input is invalid"
status_code: int = 422
code: int = 1400
message: str = "The user cannot access the default resource pool."
status_code: int = 400


@dataclass
Expand All @@ -57,21 +54,21 @@ class Unauthorized(BaseError):


@dataclass
class NoDefaultPoolAccessError(BaseError):
"""Raised when the user does not have the right to access the default resource pool."""
class MissingResourceError(BaseError):
"""Raised when a resource is not found."""

code: int = 1400
message: str = "The user cannot access the default resource pool."
status_code: int = 400
code: int = 1404
status_code: int = 404
message: str = "The requested resource does not exist or cannot be found"


@dataclass
class GeneralBadRequest(BaseError):
"""Raised for a 400 status code - when the server cannot or will not process the request."""
class ConflictError(BaseError):
"""Raised when a conflicting update occurs."""

code: int = 1400
message: str = "The request is invalid, malformed or non-sensical and cannot be fulfilled."
status_code: int = 400
code: int = 1409
message: str = "Conflicting update detected."
status_code: int = 409


@dataclass
Expand All @@ -89,6 +86,31 @@ class UpdatingWithStaleContentError(BaseError):
status_code: int = 409


@dataclass
class ValidationError(BaseError):
"""Raised when the inputs or outputs are invalid."""

code: int = 1422
message: str = "The provided input is invalid"
status_code: int = 422


@dataclass
class PreconditionRequiredError(BaseError):
"""Raised when a precondition is not met."""

code: int = 1428
message: str = "Conflicting update detected."
status_code: int = 428


@dataclass
class ConfigurationError(BaseError):
"""Raised when the server is not properly configured."""

message: str = "The server is not properly configured and cannot run"


@dataclass
class ProgrammingError(BaseError):
"""Raised an irrecoverable programming error or bug occurs."""
Expand All @@ -107,24 +129,6 @@ class EventError(BaseError):
status_code: int = 500


@dataclass
class ConflictError(BaseError):
"""Raised when a conflicting update occurs."""

code: int = 1409
message: str = "Conflicting update detected."
status_code: int = 409


@dataclass
class PreconditionRequiredError(BaseError):
"""Raised when a precondition is not met."""

code: int = 1428
message: str = "Conflicting update detected."
status_code: int = 428


@dataclass
class SecretDecryptionError(BaseError):
"""Raised when an error occurs decrypting secrets."""
Expand Down
4 changes: 2 additions & 2 deletions components/renku_data_services/user_preferences/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async def get_user_preferences(
user_preferences = res.one_or_none()

if user_preferences is None:
raise errors.MissingResourceError(message="Preferences not found for user.")
raise errors.MissingResourceError(message="Preferences not found for user.", quiet=True)
return user_preferences.dump()

async def delete_user_preferences(self, user: base_models.APIUser) -> None:
Expand Down Expand Up @@ -117,7 +117,7 @@ async def remove_pinned_project(self, user: base_models.APIUser, project_slug: s
user_preferences = res.one_or_none()

if user_preferences is None:
raise errors.MissingResourceError(message="Preferences not found for user.")
raise errors.MissingResourceError(message="Preferences not found for user.", quiet=True)

project_slugs: list[str]
project_slugs = user_preferences.pinned_projects.get("project_slugs", [])
Expand Down
Loading