|
| 1 | +import asyncio |
1 | 2 | import logging |
2 | 3 | from collections.abc import Callable, Coroutine, Mapping |
3 | 4 | from contextlib import contextmanager |
|
8 | 9 | import httpx |
9 | 10 | from fastapi import HTTPException, status |
10 | 11 | from pydantic import ValidationError |
| 12 | +from servicelib.rabbitmq._errors import RemoteMethodNotRegisteredError |
11 | 13 | from simcore_service_api_server.exceptions.backend_errors import BaseBackEndError |
12 | 14 |
|
13 | 15 | from ..models.schemas.errors import ErrorGet |
@@ -50,8 +52,12 @@ class ToApiTuple(NamedTuple): |
50 | 52 |
|
51 | 53 |
|
52 | 54 | # service to public-api status maps |
53 | | -E = TypeVar("E", bound=BaseBackEndError) |
54 | | -HttpStatusMap: TypeAlias = Mapping[ServiceHTTPStatus, E] |
| 55 | +BackEndErrorType = TypeVar("BackEndErrorType", bound=BaseBackEndError) |
| 56 | +RpcExceptionType = TypeVar( |
| 57 | + "RpcExceptionType", bound=Exception |
| 58 | +) # need more specific rpc exception base class |
| 59 | +HttpStatusMap: TypeAlias = Mapping[ServiceHTTPStatus, BackEndErrorType] |
| 60 | +RabbitMqRpcExceptionMap: TypeAlias = Mapping[RpcExceptionType, BackEndErrorType] |
55 | 61 |
|
56 | 62 |
|
57 | 63 | def _get_http_exception_kwargs( |
@@ -98,6 +104,7 @@ def _get_http_exception_kwargs( |
98 | 104 | def service_exception_handler( |
99 | 105 | service_name: str, |
100 | 106 | http_status_map: HttpStatusMap, |
| 107 | + rpc_exception_map: RabbitMqRpcExceptionMap, |
101 | 108 | **context, |
102 | 109 | ): |
103 | 110 | status_code: int |
@@ -126,35 +133,64 @@ def service_exception_handler( |
126 | 133 | status_code=status_code, detail=detail, headers=headers |
127 | 134 | ) from exc |
128 | 135 |
|
| 136 | + except BaseException as exc: # currently no baseclass for rpc errors |
| 137 | + if ( |
| 138 | + type(exc) == asyncio.TimeoutError |
| 139 | + ): # https://github.com/ITISFoundation/osparc-simcore/blob/master/packages/service-library/src/servicelib/rabbitmq/_client_rpc.py#L76 |
| 140 | + raise HTTPException( |
| 141 | + status_code=status.HTTP_504_GATEWAY_TIMEOUT, |
| 142 | + detail="Request to backend timed out", |
| 143 | + ) from exc |
| 144 | + if type(exc) in { |
| 145 | + asyncio.exceptions.CancelledError, |
| 146 | + RuntimeError, |
| 147 | + RemoteMethodNotRegisteredError, |
| 148 | + }: # https://github.com/ITISFoundation/osparc-simcore/blob/master/packages/service-library/src/servicelib/rabbitmq/_client_rpc.py#L76 |
| 149 | + raise HTTPException( |
| 150 | + status_code=status.HTTP_502_BAD_GATEWAY, detail="Request to failed" |
| 151 | + ) from exc |
| 152 | + if backend_error_type := rpc_exception_map.get(type(exc)): |
| 153 | + raise backend_error_type(**context) from exc |
| 154 | + raise |
| 155 | + |
129 | 156 |
|
130 | 157 | def service_exception_mapper( |
131 | 158 | *, |
132 | 159 | service_name: str, |
133 | | - http_status_map: HttpStatusMap, |
| 160 | + http_status_map: HttpStatusMap = {}, |
| 161 | + rpc_exception_map: RabbitMqRpcExceptionMap = {}, |
134 | 162 | ) -> Callable[ |
135 | 163 | [Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]], |
136 | 164 | Callable[Concatenate[Self, P], Coroutine[Any, Any, R]], |
137 | 165 | ]: |
138 | 166 | def _decorator(member_func: Callable[Concatenate[Self, P], Coroutine[Any, Any, R]]): |
139 | | - _assert_correct_kwargs(func=member_func, status_map=http_status_map) |
| 167 | + _assert_correct_kwargs( |
| 168 | + func=member_func, |
| 169 | + exception_types=set(http_status_map.values()).union( |
| 170 | + set(rpc_exception_map.values()) |
| 171 | + ), |
| 172 | + ) |
140 | 173 |
|
141 | 174 | @wraps(member_func) |
142 | 175 | async def _wrapper(self: Self, *args: P.args, **kwargs: P.kwargs) -> R: |
143 | | - with service_exception_handler(service_name, http_status_map, **kwargs): |
| 176 | + with service_exception_handler( |
| 177 | + service_name, http_status_map, rpc_exception_map, **kwargs |
| 178 | + ): |
144 | 179 | return await member_func(self, *args, **kwargs) |
145 | 180 |
|
146 | 181 | return _wrapper |
147 | 182 |
|
148 | 183 | return _decorator |
149 | 184 |
|
150 | 185 |
|
151 | | -def _assert_correct_kwargs(func: Callable, status_map: HttpStatusMap): |
| 186 | +def _assert_correct_kwargs(func: Callable, exception_types: set[BackEndErrorType]): |
152 | 187 | _required_kwargs = { |
153 | 188 | name |
154 | 189 | for name, param in signature(func).parameters.items() |
155 | 190 | if param.kind == param.KEYWORD_ONLY |
156 | 191 | } |
157 | | - for exc_type in status_map.values(): |
| 192 | + for exc_type in exception_types: |
| 193 | + assert isinstance(exc_type, type) # nosec |
158 | 194 | _exception_inputs = exc_type.named_fields() |
159 | 195 | assert _exception_inputs.issubset( |
160 | 196 | _required_kwargs |
|
0 commit comments