• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SwissDataScienceCenter / renku-data-services / 20377691277

19 Dec 2025 05:33PM UTC coverage: 86.065% (-0.03%) from 86.099%
20377691277

Pull #1151

github

web-flow
Merge fc0de8e91 into 59700fa49
Pull Request #1151: fix: updating group slug did not update search

23 of 23 new or added lines in 3 files covered. (100.0%)

15 existing lines in 6 files now uncovered.

24328 of 28267 relevant lines covered (86.07%)

1.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.39
/components/renku_data_services/base_api/error_handler.py
1
"""The error handler for the application."""
2

3
import os
2✔
4
import sys
2✔
5
import traceback
2✔
6
from asyncio import CancelledError
2✔
7
from collections.abc import Mapping, Set
2✔
8
from sqlite3 import Error as SqliteError
2✔
9
from typing import Any, Optional, Protocol, TypeVar, Union
2✔
10

11
import httpx
2✔
12
import jwt
2✔
13
from asyncpg import exceptions as postgres_exceptions
2✔
14
from pydantic import ValidationError as PydanticValidationError
2✔
15
from sanic import HTTPResponse, Request, SanicException, json
2✔
16
from sanic.errorpages import BaseRenderer, TextRenderer
2✔
17
from sanic.handlers import ErrorHandler
2✔
18
from sanic_ext.exceptions import ValidationError
2✔
19
from sqlalchemy.exc import SQLAlchemyError
2✔
20

21
from renku_data_services import errors
2✔
22

23

24
class BaseError(Protocol):
2✔
25
    """Protocol for the error type of an apispec module."""
26

27
    code: int
2✔
28
    message: str
2✔
29
    detail: Optional[str]
2✔
30
    quiet: bool
2✔
31

32

33
class BaseErrorResponse(Protocol):
2✔
34
    """Protocol for the error response class of an apispec module."""
35

36
    error: BaseError
2✔
37

38
    def dict(
2✔
39
        self,
40
        *,
41
        include: Optional[Union[Set[Union[int, str]], Mapping[Union[int, str], Any]]] = None,
42
        exclude: Optional[Union[Set[Union[int, str]], Mapping[Union[int, str], Any]]] = None,
43
        by_alias: bool = False,
44
        skip_defaults: Optional[bool] = None,
45
        exclude_unset: bool = False,
46
        exclude_defaults: bool = False,
47
        exclude_none: bool = False,
48
    ) -> dict[str, Any]:
49
        """Turn the response to dict."""
50
        ...
×
51

52

53
BError = TypeVar("BError", bound=BaseError)
2✔
54
BErrorResponse = TypeVar("BErrorResponse", bound=BaseErrorResponse)
2✔
55

56

57
class ApiSpec(Protocol[BErrorResponse, BError]):
2✔
58
    """Protocol for an apispec with error data."""
59

60
    ErrorResponse: BErrorResponse
2✔
61
    Error: BError
2✔
62

63

64
class CustomErrorHandler(ErrorHandler):
2✔
65
    """Central error handling."""
66

67
    def __init__(self, api_spec: ApiSpec, base: type[BaseRenderer] = TextRenderer) -> None:
2✔
68
        self.api_spec = api_spec
2✔
69
        super().__init__(base)
2✔
70

71
    @classmethod
2✔
72
    def _get_formatted_exception(cls, request: Request, exception: Exception) -> errors.BaseError | None:
2✔
73
        formatted_exception: errors.BaseError | None = None
2✔
74
        match exception:
2✔
75
            case errors.BaseError():
2✔
76
                formatted_exception = exception
2✔
77
            case ValidationError():
2✔
78
                extra_exception = None if exception.extra is None else exception.extra["exception"]
2✔
79
                match extra_exception:
2✔
80
                    case TypeError():
2✔
81
                        formatted_exception = errors.ValidationError(
2✔
82
                            message="The validation failed because the provided input has the wrong type"
83
                        )
84
                    case PydanticValidationError():
2✔
85
                        parts = [
2✔
86
                            ".".join(str(i) for i in field["loc"]) + ": " + field["msg"]
87
                            for field in extra_exception.errors()
88
                        ]
89
                        message = f"There are errors in the following fields, {', '.join(parts)}"
2✔
90
                        formatted_exception = errors.ValidationError(message=message)
2✔
91
            case SanicException():
2✔
92
                message = exception.message
2✔
93
                if message == "" or message is None:
2✔
94
                    message = ", ".join([str(i) for i in exception.args])
×
95
                formatted_exception = errors.BaseError(
2✔
96
                    message=message,
97
                    status_code=exception.status_code,
98
                    code=1000 + exception.status_code,
99
                    quiet=exception.quiet or False,
100
                )
101
            case SqliteError():
2✔
102
                formatted_exception = errors.BaseError(
1✔
103
                    message=f"Database error occurred: {exception.sqlite_errorname}",
104
                    detail=f"Error code: {exception.sqlite_errorcode}",
105
                )
106
            case postgres_exceptions.PostgresError():
2✔
107
                formatted_exception = errors.BaseError(
1✔
108
                    message=f"Database error occurred: {exception.msg}", detail=f"Error code: {exception.pgcode}"
109
                )
110
            case SQLAlchemyError():
2✔
111
                message = ", ".join([str(i) for i in exception.args])
2✔
112
                if "CharacterNotInRepertoireError" in message:
2✔
113
                    # NOTE: This message is usually triggered if a string field for the database contains
114
                    # NULL - i.e \u0000 or other invalid characters that are not UTF-8 compatible
115
                    formatted_exception = errors.ValidationError(
1✔
116
                        message="The payload contains characters that are incompatible with the database",
117
                        detail=message,
118
                    )
119
                elif "value out of int32 range" in message:
1✔
UNCOV
120
                    formatted_exception = errors.ValidationError(
×
121
                        message="The payload contains integers with values that are "
122
                        "too large or small for the database",
123
                        detail=message,
124
                    )
125
                else:
126
                    formatted_exception = errors.BaseError(message=f"Database error occurred: {message}")
1✔
127
            case PydanticValidationError():
2✔
128
                parts = [".".join(str(i) for i in field["loc"]) + ": " + field["msg"] for field in exception.errors()]
2✔
129
                message = f"There are errors in the following fields, {', '.join(parts)}"
2✔
130
                formatted_exception = errors.ValidationError(message=message)
2✔
131
            case OverflowError():
1✔
132
                formatted_exception = errors.ValidationError(
1✔
133
                    message="The provided input is too large to be stored in the database"
134
                )
135
            case jwt.exceptions.InvalidTokenError():
1✔
136
                formatted_exception = errors.InvalidTokenError()
1✔
137
            case CancelledError():
1✔
138
                quiet = request.transport.is_closing()
×
139
                formatted_exception = errors.RequestCancelledError(quiet=quiet)
×
140

141
            case httpx.RequestError():
1✔
142
                req_uri = "<unknown-uri>"
1✔
143
                req_method = "<unknown-method>"
1✔
144
                if exception._request:
1✔
145
                    req_uri = str(exception.request.url)
×
146
                    req_method = exception.request.method
×
147

148
                formatted_exception = errors.BaseError(
1✔
149
                    message=f"Error on remote connection {req_method} {req_uri}: {exception}"
150
                )
151

152
        return formatted_exception
2✔
153

154
    def default(self, request: Request, exception: Exception) -> HTTPResponse:
2✔
155
        """Overrides the default error handler."""
156
        formatted_exception = self._get_formatted_exception(request, exception) or errors.BaseError()
2✔
157

158
        self.log(request, formatted_exception)
2✔
159
        if formatted_exception.status_code == 500 and "PYTEST_CURRENT_TEST" in os.environ:
2✔
160
            # TODO: Figure out how to do logging properly in here, I could not get the sanic logs to show up from here
161
            # at all when running schemathesis. So 500 errors are hard to debug but print statements do show up.
162
            # The above log statement does not show up in the logs that pytest shows after a test is done.
163
            sys.stderr.write(f"A 500 error was raised because of {type(exception)} on request {request}\n")
1✔
164
            traceback.print_exception(exception)
1✔
165
        return json(
2✔
166
            self.api_spec.ErrorResponse(
167
                error=self.api_spec.Error(
168
                    code=formatted_exception.code,
169
                    message=formatted_exception.message,
170
                    detail=formatted_exception.detail,
171
                )
172
            ).model_dump(exclude_none=True),
173
            status=formatted_exception.status_code,
174
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc