Skip to content

Commit f98c431

Browse files
authored
🐛 Fixes extra long error/debug message in the front-end (#7761)
1 parent 540f786 commit f98c431

File tree

10 files changed

+102
-41
lines changed

10 files changed

+102
-41
lines changed

api/specs/web-server/_projects_states.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pydantic import ValidationError
1414
from servicelib.aiohttp import status
1515
from simcore_service_webserver._meta import API_VTAG
16-
from simcore_service_webserver.director_v2.exceptions import DirectorServiceError
16+
from simcore_service_webserver.director_v2.exceptions import DirectorV2ServiceError
1717
from simcore_service_webserver.projects._controller.projects_states_rest import (
1818
ProjectPathParams,
1919
_OpenProjectQuery,
@@ -62,7 +62,7 @@ def to_desc(exceptions: list[type[Exception]] | type[Exception]):
6262
"description": to_desc([ValidationError])
6363
},
6464
status.HTTP_503_SERVICE_UNAVAILABLE: {
65-
"description": to_desc([DirectorServiceError])
65+
"description": to_desc([DirectorV2ServiceError])
6666
},
6767
},
6868
)

services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6073,7 +6073,7 @@ paths:
60736073
'422':
60746074
description: ValidationError
60756075
'503':
6076-
description: DirectorServiceError
6076+
description: DirectorV2ServiceError
60776077
/v0/projects/{project_id}:close:
60786078
post:
60796079
tags:

services/web/server/src/simcore_service_webserver/director_v2/_client_base.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from tenacity.wait import wait_random
1111
from yarl import URL
1212

13-
from .exceptions import DirectorServiceError
13+
from .exceptions import DirectorV2ServiceError
1414
from .settings import get_client_session
1515

1616
log = logging.getLogger(__name__)
@@ -30,7 +30,9 @@
3030
DataBody: TypeAlias = DataType | list[DataType] | None
3131

3232

33-
_StatusToExceptionMapping = dict[int, tuple[type[DirectorServiceError], dict[str, Any]]]
33+
_StatusToExceptionMapping = dict[
34+
int, tuple[type[DirectorV2ServiceError], dict[str, Any]]
35+
]
3436

3537

3638
def _get_exception_from(
@@ -40,7 +42,7 @@ def _get_exception_from(
4042
exc, exc_ctx = on_error[status_code]
4143
return exc(**exc_ctx, status=status_code, reason=reason)
4244
# default
43-
return DirectorServiceError(status=status_code, reason=reason, url=url)
45+
return DirectorV2ServiceError(status=status_code, reason=reason, url=url)
4446

4547

4648
@retry(**DEFAULT_RETRY_POLICY)
@@ -95,14 +97,14 @@ async def request_director_v2(
9597
return payload
9698

9799
except TimeoutError as err:
98-
raise DirectorServiceError(
100+
raise DirectorV2ServiceError(
99101
status=status.HTTP_503_SERVICE_UNAVAILABLE,
100102
reason=f"request to director-v2 timed-out: {err}",
101103
url=url,
102104
) from err
103105

104106
except aiohttp.ClientError as err:
105-
raise DirectorServiceError(
107+
raise DirectorV2ServiceError(
106108
status=status.HTTP_503_SERVICE_UNAVAILABLE,
107109
reason=f"request to director-v2 service unexpected error {err}",
108110
url=url,

services/web/server/src/simcore_service_webserver/director_v2/_controller/_rest_exceptions.py

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,80 @@
1+
import logging
2+
13
from aiohttp import web
4+
from common_library.error_codes import create_error_code
5+
from models_library.rest_error import ErrorGet
6+
from servicelib import status_codes_utils
27
from servicelib.aiohttp import status
3-
from servicelib.aiohttp.rest_responses import create_http_error
4-
from servicelib.aiohttp.web_exceptions_extension import get_http_error_class_or_none
5-
from simcore_service_webserver.director_v2.exceptions import DirectorServiceError
8+
from servicelib.logging_errors import create_troubleshotting_log_kwargs
69

10+
from ...constants import MSG_TRY_AGAIN_OR_SUPPORT
711
from ...exception_handling import (
812
ExceptionHandlersMap,
913
ExceptionToHttpErrorMap,
1014
HttpErrorInfo,
15+
create_error_context_from_request,
16+
create_error_response,
1117
exception_handling_decorator,
1218
to_exceptions_handlers_map,
1319
)
1420
from ...users.exceptions import UserDefaultWalletNotFoundError
1521
from ...wallets.errors import WalletNotEnoughCreditsError
22+
from ..exceptions import DirectorV2ServiceError
1623

1724
_exceptions_handlers_map: ExceptionHandlersMap = {}
1825

1926

20-
async def _handler_director_service_error(
27+
_logger = logging.getLogger(__name__)
28+
29+
30+
async def _handler_director_service_error_as_503_or_4xx(
2131
request: web.Request, exception: Exception
2232
) -> web.Response:
23-
assert request # nosec
33+
assert isinstance(exception, DirectorV2ServiceError) # nosec
34+
assert status_codes_utils.is_error(
35+
exception.status
36+
), f"DirectorV2ServiceError must be an error, got {exception=}" # nosec
37+
38+
if status_codes_utils.is_5xx_server_error(exception.status):
39+
# NOTE: All directorv2 5XX are mapped to 503
40+
status_code = status.HTTP_503_SERVICE_UNAVAILABLE
41+
user_msg = (
42+
# Most likely the director service is down or misconfigured so the user is asked to try again later.
43+
"This service is temporarily unavailable. The incident was logged and will be investigated. "
44+
+ MSG_TRY_AGAIN_OR_SUPPORT
45+
)
2446

25-
assert isinstance(exception, DirectorServiceError) # nosec
26-
return create_http_error(
27-
exception,
28-
reason=exception.reason,
29-
http_error_cls=get_http_error_class_or_none(exception.status)
30-
or web.HTTPServiceUnavailable,
31-
)
47+
# Log for further investigation
48+
oec = create_error_code(exception)
49+
_logger.exception(
50+
**create_troubleshotting_log_kwargs(
51+
user_msg,
52+
error=exception,
53+
error_code=oec,
54+
error_context={
55+
**create_error_context_from_request(request),
56+
"error_code": oec,
57+
},
58+
)
59+
)
60+
error = ErrorGet.model_construct(
61+
message=user_msg, support_id=oec, status=status_code
62+
)
3263

64+
else:
65+
# NOTE: All directorv2 4XX are mapped one-to-one
66+
assert status_codes_utils.is_4xx_client_error(
67+
exception.status
68+
), f"DirectorV2ServiceError must be a client error, got {exception=}" # nosec
3369

34-
_exceptions_handlers_map[DirectorServiceError] = _handler_director_service_error
70+
error = ErrorGet(status=exception.status, message="{exception}")
71+
72+
return create_error_response(error, status_code=error.status)
73+
74+
75+
_exceptions_handlers_map[DirectorV2ServiceError] = (
76+
_handler_director_service_error_as_503_or_4xx
77+
)
3578

3679

3780
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {

services/web/server/src/simcore_service_webserver/director_v2/_director_v2_service.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from ..users.exceptions import UserDefaultWalletNotFoundError
3232
from ..wallets import api as wallets_service
3333
from ._client_base import DataType, request_director_v2
34-
from .exceptions import ComputationNotFoundError, DirectorServiceError
34+
from .exceptions import ComputationNotFoundError, DirectorV2ServiceError
3535
from .settings import DirectorV2Settings, get_plugin_settings
3636

3737
_logger = logging.getLogger(__name__)
@@ -75,7 +75,7 @@ async def create_or_update_pipeline(
7575
assert isinstance(computation_task_out, dict) # nosec
7676
return computation_task_out
7777

78-
except DirectorServiceError as exc:
78+
except DirectorV2ServiceError as exc:
7979
_logger.error( # noqa: TRY400
8080
"could not create pipeline from project %s: %s",
8181
project_id,
@@ -117,7 +117,7 @@ async def get_computation_task(
117117
_logger.debug("found computation task: %s", f"{task_out=}")
118118

119119
return task_out
120-
except DirectorServiceError as exc:
120+
except DirectorV2ServiceError as exc:
121121
if exc.status == status.HTTP_404_NOT_FOUND:
122122
# the pipeline might not exist and that is ok
123123
return None

services/web/server/src/simcore_service_webserver/director_v2/director_v2_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
is_pipeline_running,
1313
stop_pipeline,
1414
)
15-
from .exceptions import DirectorServiceError
15+
from .exceptions import DirectorV2ServiceError
1616

1717
# director-v2 module internal API
1818
__all__: tuple[str, ...] = (
1919
"AbstractProjectRunPolicy",
20-
"DirectorServiceError",
20+
"DirectorV2ServiceError",
2121
"create_or_update_pipeline",
2222
"delete_pipeline",
2323
"get_batch_tasks_outputs",

services/web/server/src/simcore_service_webserver/director_v2/exceptions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from ..errors import WebServerBaseError
66

77

8-
class DirectorServiceError(WebServerBaseError, RuntimeError):
8+
class DirectorV2ServiceError(WebServerBaseError, RuntimeError):
99
"""Basic exception for errors raised by director-v2"""
1010

1111
msg_template = "Unexpected error: director-v2 returned '{status}', reason '{reason}' after calling '{url}'"
@@ -16,5 +16,5 @@ def __init__(self, *, status: int, reason: str, **ctx: Any) -> None:
1616
self.reason = reason
1717

1818

19-
class ComputationNotFoundError(DirectorServiceError):
19+
class ComputationNotFoundError(DirectorV2ServiceError):
2020
msg_template = "Computation '{project_id}' not found"

services/web/server/src/simcore_service_webserver/exception_handling/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
from ._base import ExceptionHandlersMap, exception_handling_decorator
2-
from ._factory import ExceptionToHttpErrorMap, HttpErrorInfo, to_exceptions_handlers_map
2+
from ._factory import (
3+
ExceptionToHttpErrorMap,
4+
HttpErrorInfo,
5+
create_error_context_from_request,
6+
create_error_response,
7+
to_exceptions_handlers_map,
8+
)
39

410
__all__: tuple[str, ...] = (
511
"ExceptionHandlersMap",
612
"ExceptionToHttpErrorMap",
713
"HttpErrorInfo",
14+
"create_error_context_from_request",
15+
"create_error_response",
816
"exception_handling_decorator",
917
"to_exceptions_handlers_map",
1018
)

services/web/server/src/simcore_service_webserver/exception_handling/_factory.py

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
2-
from typing import NamedTuple, TypeAlias
2+
from typing import Any, NamedTuple, TypeAlias
33

44
from aiohttp import web
55
from common_library.error_codes import create_error_code
@@ -35,6 +35,15 @@ class HttpErrorInfo(NamedTuple):
3535
ExceptionToHttpErrorMap: TypeAlias = dict[type[Exception], HttpErrorInfo]
3636

3737

38+
def create_error_context_from_request(request: web.Request) -> dict[str, Any]:
39+
return {
40+
"request": request,
41+
"request.remote": f"{request.remote}",
42+
"request.method": f"{request.method}",
43+
"request.path": f"{request.path}",
44+
}
45+
46+
3847
def create_error_response(error: ErrorGet, status_code: int) -> web.Response:
3948
assert is_error(status_code), f"{status_code=} must be an error [{error=}]" # nosec
4049

@@ -85,20 +94,19 @@ async def _exception_handler(
8594

8695
if is_5xx_server_error(status_code):
8796
oec = create_error_code(exception)
97+
error_context = {
98+
# NOTE: `error_context` is also used to substitute f-string tokens
99+
# in the `user_msg`
100+
**create_error_context_from_request(request),
101+
"error_code": oec,
102+
}
103+
88104
_logger.exception(
89105
**create_troubleshotting_log_kwargs(
90106
user_msg,
91107
error=exception,
92108
error_code=oec,
93-
error_context={
94-
# NOTE: context is also used to substitute tokens in the error message
95-
# e.g. "support error is {error_code}"
96-
"request": request,
97-
"request.remote": f"{request.remote}",
98-
"request.method": f"{request.method}",
99-
"request.path": f"{request.path}",
100-
"error_code": oec,
101-
},
109+
error_context=error_context,
102110
)
103111
)
104112
error = ErrorGet.model_construct(

services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from simcore_postgres_database.webserver_models import ProjectType
2121

2222
from ..._meta import API_VTAG as VTAG
23-
from ...director_v2.exceptions import DirectorServiceError
23+
from ...director_v2.exceptions import DirectorV2ServiceError
2424
from ...login.decorators import login_required
2525
from ...notifications import project_logs
2626
from ...products import products_web
@@ -141,7 +141,7 @@ async def open_project(request: web.Request) -> web.Response:
141141

142142
return envelope_json_response(ProjectGet.from_domain_model(project))
143143

144-
except DirectorServiceError as exc:
144+
except DirectorV2ServiceError as exc:
145145
# there was an issue while accessing the director-v2/director-v0
146146
# ensure the project is closed again
147147
await _projects_service.try_close_project_for_user(

0 commit comments

Comments
 (0)