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

SwissDataScienceCenter / renku-data-services / 18675974705

21 Oct 2025 07:16AM UTC coverage: 83.359% (-0.002%) from 83.361%
18675974705

push

github

web-flow
chore: do not log all asyncio.CancelledError (#1069)

This occurs in almost all cases when the client making the request (i.e.
the UI) interrupts the request before it can complete. Usually because
the user closed the browser or nagivated to some other page.

---------

Co-authored-by: Samuel Gaist <samuel.gaist@idiap.ch>

6 of 8 new or added lines in 2 files covered. (75.0%)

54 existing lines in 5 files now uncovered.

21791 of 26141 relevant lines covered (83.36%)

1.49 hits per line

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

87.5
/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 jwt
2✔
12
from asyncpg import exceptions as postgres_exceptions
2✔
13
from pydantic import ValidationError as PydanticValidationError
2✔
14
from sanic import HTTPResponse, Request, SanicException, json
2✔
15
from sanic.errorpages import BaseRenderer, TextRenderer
2✔
16
from sanic.handlers import ErrorHandler
2✔
17
from sanic_ext.exceptions import ValidationError
2✔
18
from sqlalchemy.exc import SQLAlchemyError
2✔
19

20
from renku_data_services import errors
2✔
21

22

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

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

31

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

35
    error: BaseError
2✔
36

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

51

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

55

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

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

62

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

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

70
    def default(self, request: Request, exception: Exception) -> HTTPResponse:
2✔
71
        """Overrides the default error handler."""
72
        formatted_exception = errors.BaseError()
2✔
73
        match exception:
2✔
74
            case errors.BaseError():
2✔
75
                formatted_exception = exception
2✔
76
            case ValidationError():
2✔
77
                extra_exception = None if exception.extra is None else exception.extra["exception"]
2✔
78
                match extra_exception:
2✔
79
                    case TypeError():
2✔
80
                        formatted_exception = errors.ValidationError(
2✔
81
                            message="The validation failed because the provided input has the wrong type"
82
                        )
83
                    case PydanticValidationError():
2✔
84
                        parts = [
2✔
85
                            ".".join(str(i) for i in field["loc"]) + ": " + field["msg"]
86
                            for field in extra_exception.errors()
87
                        ]
88
                        message = f"There are errors in the following fields, {', '.join(parts)}"
2✔
89
                        formatted_exception = errors.ValidationError(message=message)
2✔
90
            case SanicException():
2✔
91
                message = exception.message
2✔
92
                if message == "" or message is None:
2✔
93
                    message = ", ".join([str(i) for i in exception.args])
×
94
                formatted_exception = errors.BaseError(
2✔
95
                    message=message,
96
                    status_code=exception.status_code,
97
                    code=1000 + exception.status_code,
98
                    quiet=exception.quiet or False,
99
                )
100
            case SqliteError():
2✔
101
                formatted_exception = errors.BaseError(
×
102
                    message=f"Database error occurred: {exception.sqlite_errorname}",
103
                    detail=f"Error code: {exception.sqlite_errorcode}",
104
                )
105
            case postgres_exceptions.PostgresError():
2✔
106
                formatted_exception = errors.BaseError(
×
107
                    message=f"Database error occurred: {exception.msg}", detail=f"Error code: {exception.pgcode}"
108
                )
109
            case SQLAlchemyError():
2✔
110
                message = ", ".join([str(i) for i in exception.args])
1✔
111
                if "CharacterNotInRepertoireError" in message:
1✔
112
                    # NOTE: This message is usually triggered if a string field for the database contains
113
                    # NULL - i.e \u0000 or other invalid characters that are not UTF-8 compatible
114
                    formatted_exception = errors.ValidationError(
×
115
                        message="The payload contains characters that are incompatible with the database",
116
                        detail=message,
117
                    )
118
                elif "value out of int32 range" in message:
1✔
UNCOV
119
                    formatted_exception = errors.ValidationError(
×
120
                        message="The payload contains integers with values that are "
121
                        "too large or small for the database",
122
                        detail=message,
123
                    )
124
                else:
125
                    formatted_exception = errors.BaseError(message=f"Database error occurred: {message}")
1✔
126
            case PydanticValidationError():
2✔
127
                parts = [".".join(str(i) for i in field["loc"]) + ": " + field["msg"] for field in exception.errors()]
2✔
128
                message = f"There are errors in the following fields, {', '.join(parts)}"
2✔
129
                formatted_exception = errors.ValidationError(message=message)
2✔
130
            case OverflowError():
1✔
131
                formatted_exception = errors.ValidationError(
×
132
                    message="The provided input is too large to be stored in the database"
133
                )
134
            case jwt.exceptions.InvalidTokenError():
1✔
135
                formatted_exception = errors.InvalidTokenError()
×
136
            case CancelledError():
1✔
NEW
137
                quiet = request.transport.is_closing()
×
NEW
138
                formatted_exception = errors.RequestCancelledError(quiet=quiet)
×
139
        self.log(request, formatted_exception)
2✔
140
        if formatted_exception.status_code == 500 and "PYTEST_CURRENT_TEST" in os.environ:
2✔
141
            # TODO: Figure out how to do logging properly in here, I could not get the sanic logs to show up from here
142
            # at all when running schemathesis. So 500 errors are hard to debug but print statements do show up.
143
            # The above log statement does not show up in the logs that pytest shows after a test is done.
144
            sys.stderr.write(f"A 500 error was raised because of {type(exception)} on request {request}\n")
1✔
145
            traceback.print_exception(exception)
1✔
146
        return json(
2✔
147
            self.api_spec.ErrorResponse(
148
                error=self.api_spec.Error(
149
                    code=formatted_exception.code,
150
                    message=formatted_exception.message,
151
                    detail=formatted_exception.detail,
152
                )
153
            ).model_dump(exclude_none=True),
154
            status=formatted_exception.status_code,
155
        )
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