diff --git a/packages/service-library/src/servicelib/aiohttp/monitoring.py b/packages/service-library/src/servicelib/aiohttp/monitoring.py index 84472c7e2f34..8a52b8eeb9de 100644 --- a/packages/service-library/src/servicelib/aiohttp/monitoring.py +++ b/packages/service-library/src/servicelib/aiohttp/monitoring.py @@ -1,6 +1,5 @@ """Enables monitoring of some quantities needed for diagnostics""" -import asyncio import logging from collections.abc import Awaitable, Callable from time import perf_counter @@ -61,9 +60,8 @@ async def middleware_handler(request: web.Request, handler: Handler): # See https://prometheus.io/docs/concepts/metric_types log_exception: BaseException | None = None - resp: web.StreamResponse = web.HTTPInternalServerError( - reason="Unexpected exception" - ) + response: web.StreamResponse = web.HTTPInternalServerError() + canonical_endpoint = request.path if request.match_info.route.resource: canonical_endpoint = request.match_info.route.resource.canonical @@ -86,29 +84,25 @@ async def middleware_handler(request: web.Request, handler: Handler): endpoint=canonical_endpoint, user_agent=user_agent, ): - resp = await handler(request) + response = await handler(request) assert isinstance( # nosec - resp, web.StreamResponse + response, web.StreamResponse ), "Forgot envelope middleware?" except web.HTTPServerError as exc: - resp = exc + response = exc log_exception = exc - raise resp from exc + raise + except web.HTTPException as exc: - resp = exc + response = exc log_exception = None - raise resp from exc - except asyncio.CancelledError as exc: - resp = web.HTTPInternalServerError(text=f"{exc}") - log_exception = exc - raise resp from exc + raise + except Exception as exc: # pylint: disable=broad-except - resp = web.HTTPInternalServerError(text=f"{exc}") - resp.__cause__ = exc log_exception = exc - raise resp from exc + raise finally: response_latency_seconds = perf_counter() - start_time @@ -118,13 +112,13 @@ async def middleware_handler(request: web.Request, handler: Handler): method=request.method, endpoint=canonical_endpoint, user_agent=user_agent, - http_status=resp.status, + http_status=response.status, response_latency_seconds=response_latency_seconds, ) if exit_middleware_cb: with log_catch(logger=log, reraise=False): - await exit_middleware_cb(request, resp) + await exit_middleware_cb(request, response) if log_exception: log.error( @@ -135,12 +129,12 @@ async def middleware_handler(request: web.Request, handler: Handler): request.method, request.path, response_latency_seconds, - resp.status, + response.status, exc_info=log_exception, stack_info=True, ) - return resp + return response setattr( # noqa: B010 middleware_handler, "__middleware_name__", f"{__name__}.monitor_{app_name}" diff --git a/packages/service-library/src/servicelib/aiohttp/profiler_middleware.py b/packages/service-library/src/servicelib/aiohttp/profiler_middleware.py index eab7d1fc5980..4256820b4b30 100644 --- a/packages/service-library/src/servicelib/aiohttp/profiler_middleware.py +++ b/packages/service-library/src/servicelib/aiohttp/profiler_middleware.py @@ -13,7 +13,7 @@ async def profiling_middleware(request: Request, handler): try: if _profiler.is_running or (_profiler.last_session is not None): raise HTTPInternalServerError( - reason="Profiler is already running. Only a single request can be profiled at any given time.", + text="Profiler is already running. Only a single request can be profiled at any given time.", headers={}, ) _profiler.reset() @@ -24,7 +24,7 @@ async def profiling_middleware(request: Request, handler): if response.content_type != MIMETYPE_APPLICATION_JSON: raise HTTPInternalServerError( - reason=f"Profiling middleware is not compatible with {response.content_type=}", + text=f"Profiling middleware is not compatible with {response.content_type=}", headers={}, ) diff --git a/packages/service-library/src/servicelib/aiohttp/requests_validation.py b/packages/service-library/src/servicelib/aiohttp/requests_validation.py index d555e535fe74..bcae45a54e21 100644 --- a/packages/service-library/src/servicelib/aiohttp/requests_validation.py +++ b/packages/service-library/src/servicelib/aiohttp/requests_validation.py @@ -51,7 +51,7 @@ def handle_validation_as_http_error( } for e in err.errors() ] - reason_msg = error_msg_template.format( + user_error_message = error_msg_template.format( failed=", ".join(d["loc"] for d in details) ) @@ -80,7 +80,7 @@ def handle_validation_as_http_error( error_str = json_dumps( { "error": { - "msg": reason_msg, + "msg": user_error_message, "resource": resource_name, # optional "details": details, # optional } @@ -88,7 +88,6 @@ def handle_validation_as_http_error( ) raise web.HTTPUnprocessableEntity( # 422 - reason=reason_msg, text=error_str, content_type=MIMETYPE_APPLICATION_JSON, ) from err diff --git a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py index b874a57be0c7..1737ee012344 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py @@ -11,15 +11,18 @@ from aiohttp.web_exceptions import HTTPError from aiohttp.web_request import Request from aiohttp.web_response import StreamResponse -from common_library.error_codes import create_error_code +from common_library.error_codes import ErrorCodeStr, create_error_code from common_library.json_serialization import json_dumps, json_loads from common_library.user_messages import user_message +from models_library.basic_types import IDStr from models_library.rest_error import ErrorGet, ErrorItemType, LogMessageType +from servicelib.rest_constants import RESPONSE_MODEL_POLICY +from servicelib.status_codes_utils import is_5xx_server_error from ..logging_errors import create_troubleshotting_log_kwargs from ..mimetype_constants import MIMETYPE_APPLICATION_JSON from ..rest_responses import is_enveloped_from_map, is_enveloped_from_text -from ..utils import is_production_environ +from ..status_codes_utils import get_code_description from . import status from .rest_responses import ( create_data_response, @@ -27,7 +30,6 @@ safe_status_message, wrap_as_envelope, ) -from .rest_utils import EnvelopeFactory from .typing_extension import Handler, Middleware from .web_exceptions_extension import get_http_error_class_or_none @@ -46,15 +48,13 @@ def is_api_request(request: web.Request, api_version: str) -> bool: return bool(request.path.startswith(base_path)) -def _handle_unexpected_exception_as_500( - request: web.BaseRequest, - exception: Exception, - *, - skip_internal_error_details: bool, -) -> web.HTTPInternalServerError: - """Process unexpected exceptions and return them as HTTP errors with proper formatting. +def _create_error_context( + request: web.BaseRequest, exception: Exception +) -> tuple[ErrorCodeStr, dict[str, Any]]: + """Create error code and context for logging purposes. - IMPORTANT: this function cannot throw exceptions, as it is called + Returns: + Tuple of (error_code, error_context) """ error_code = create_error_code(exception) error_context: dict[str, Any] = { @@ -62,18 +62,14 @@ def _handle_unexpected_exception_as_500( "request.method": f"{request.method}", "request.path": f"{request.path}", } + return error_code, error_context - user_error_msg = _FMSG_INTERNAL_ERROR_USER_FRIENDLY - - http_error = create_http_error( - exception, - user_error_msg, - web.HTTPInternalServerError, - skip_internal_error_details=skip_internal_error_details, - error_code=error_code, - ) - error_context["http_error"] = http_error +def _log_5xx_server_error( + request: web.BaseRequest, exception: Exception, user_error_msg: str +) -> ErrorCodeStr: + """Log 5XX server errors with error code and context.""" + error_code, error_context = _create_error_context(request, exception) _logger.exception( **create_troubleshotting_log_kwargs( @@ -83,14 +79,41 @@ def _handle_unexpected_exception_as_500( error_code=error_code, ) ) + return error_code + + +def _handle_unexpected_exception_as_500( + request: web.BaseRequest, exception: Exception +) -> web.HTTPInternalServerError: + """Process unexpected exceptions and return them as HTTP errors with proper formatting. + + IMPORTANT: this function cannot throw exceptions, as it is called + """ + error_code, error_context = _create_error_context(request, exception) + user_error_msg = _FMSG_INTERNAL_ERROR_USER_FRIENDLY + + error_context["http_error"] = http_error = create_http_error( + exception, + user_error_msg, + web.HTTPInternalServerError, + error_code=error_code, + ) + + _log_5xx_server_error(request, exception, user_error_msg) + return http_error def _handle_http_error( request: web.BaseRequest, exception: web.HTTPError ) -> web.HTTPError: - """Handle standard HTTP errors by ensuring they're properly formatted.""" + """Handle standard HTTP errors by ensuring they're properly formatted. + + NOTE: this needs further refactoring to avoid code duplication + """ assert request # nosec + assert not exception.empty_body, "HTTPError should not have an empty body" # nosec + exception.content_type = MIMETYPE_APPLICATION_JSON if exception.reason: exception.set_status( @@ -98,18 +121,36 @@ def _handle_http_error( ) if not exception.text or not is_enveloped_from_text(exception.text): - error_message = exception.text or exception.reason or "Unexpected error" + # NOTE: aiohttp.HTTPException creates `text = f"{self.status}: {self.reason}"` + user_error_msg = exception.text or "Unexpected error" + + error_code: IDStr | None = None + if is_5xx_server_error(exception.status): + error_code = IDStr( + _log_5xx_server_error(request, exception, user_error_msg) + ) + error_model = ErrorGet( errors=[ - ErrorItemType.from_error(exception), + ErrorItemType( + code=exception.__class__.__name__, + message=user_error_msg, + resource=None, + field=None, + ), ], status=exception.status, logs=[ - LogMessageType(message=error_message, level="ERROR"), + LogMessageType(message=user_error_msg, level="ERROR"), ], - message=error_message, + message=user_error_msg, + support_id=error_code, + ) + exception.text = json_dumps( + wrap_as_envelope( + error=error_model.model_dump(mode="json", **RESPONSE_MODEL_POLICY) + ) ) - exception.text = EnvelopeFactory(error=error_model).as_text() return exception @@ -137,10 +178,8 @@ def _handle_http_successful( def _handle_exception_as_http_error( request: web.Request, - exception: Exception, + exception: NotImplementedError | TimeoutError, status_code: int, - *, - skip_internal_error_details: bool, ) -> HTTPError: """ Generic handler for exceptions that map to specific HTTP status codes. @@ -155,16 +194,15 @@ def _handle_exception_as_http_error( ) raise ValueError(msg) - return create_http_error( - exception, - f"{exception}", - http_error_cls, - skip_internal_error_details=skip_internal_error_details, - ) + user_error_msg = get_code_description(status_code) + + if is_5xx_server_error(status_code): + _log_5xx_server_error(request, exception, user_error_msg) + + return create_http_error(exception, user_error_msg, http_error_cls) def error_middleware_factory(api_version: str) -> Middleware: - _is_prod: bool = is_production_environ() @web.middleware async def _middleware_handler(request: web.Request, handler: Handler): @@ -189,27 +227,19 @@ async def _middleware_handler(request: web.Request, handler: Handler): except NotImplementedError as exc: result = _handle_exception_as_http_error( - request, - exc, - status.HTTP_501_NOT_IMPLEMENTED, - skip_internal_error_details=_is_prod, + request, exc, status.HTTP_501_NOT_IMPLEMENTED ) except TimeoutError as exc: result = _handle_exception_as_http_error( - request, - exc, - status.HTTP_504_GATEWAY_TIMEOUT, - skip_internal_error_details=_is_prod, + request, exc, status.HTTP_504_GATEWAY_TIMEOUT ) except Exception as exc: # pylint: disable=broad-except # # Last resort for unexpected exceptions (including those raise by the exception handlers!) # - result = _handle_unexpected_exception_as_500( - request, exc, skip_internal_error_details=_is_prod - ) + result = _handle_unexpected_exception_as_500(request, exc) return result @@ -230,7 +260,6 @@ def envelope_middleware_factory( api_version: str, ) -> Callable[..., Awaitable[StreamResponse]]: # FIXME: This data conversion is very error-prone. Use decorators instead! - _is_prod: bool = is_production_environ() @web.middleware async def _middleware_handler( diff --git a/packages/service-library/src/servicelib/aiohttp/rest_responses.py b/packages/service-library/src/servicelib/aiohttp/rest_responses.py index 8dfa59cf9b15..821c4e42e74c 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_responses.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_responses.py @@ -80,7 +80,6 @@ def create_http_error( ] = web.HTTPInternalServerError, # type: ignore[assignment] *, status_reason: str | None = None, - skip_internal_error_details: bool = False, error_code: ErrorCodeStr | None = None, ) -> T_HTTPError: """ @@ -99,7 +98,7 @@ def create_http_error( # changing the workflows in this function is_internal_error = bool(http_error_cls == web.HTTPInternalServerError) - if is_internal_error and skip_internal_error_details: + if is_internal_error: error_model = ErrorGet.model_validate( { "status": http_error_cls.status_code, diff --git a/packages/service-library/src/servicelib/logging_errors.py b/packages/service-library/src/servicelib/logging_errors.py index 95bf965d0f85..eaafd2eb73a2 100644 --- a/packages/service-library/src/servicelib/logging_errors.py +++ b/packages/service-library/src/servicelib/logging_errors.py @@ -39,10 +39,10 @@ def _collect_causes(exc: BaseException) -> str: debug_data = json_dumps( { "exception_type": f"{type(error)}", - "exception_details": f"{error}", + "exception_string": f"{error}", + "exception_causes": _collect_causes(error), "error_code": error_code, "context": error_context, - "causes": _collect_causes(error), "tip": tip, }, default=representation_encoder, diff --git a/packages/service-library/tests/aiohttp/long_running_tasks/test_long_running_tasks.py b/packages/service-library/tests/aiohttp/long_running_tasks/test_long_running_tasks.py index ad924ec8e73d..939ba7e330a0 100644 --- a/packages/service-library/tests/aiohttp/long_running_tasks/test_long_running_tasks.py +++ b/packages/service-library/tests/aiohttp/long_running_tasks/test_long_running_tasks.py @@ -127,6 +127,7 @@ async def test_failing_task_returns_error( client: TestClient, start_long_running_task: Callable[[TestClient, Any], Awaitable[TaskId]], wait_for_task: Callable[[TestClient, TaskId, TaskContext], Awaitable[None]], + caplog: pytest.LogCaptureFixture, ): assert client.app task_id = await start_long_running_task(client, fail=f"{True}") @@ -138,10 +139,17 @@ async def test_failing_task_returns_error( data, error = await assert_status(result, status.HTTP_500_INTERNAL_SERVER_ERROR) assert not data assert error - assert "errors" in error - assert len(error["errors"]) == 1 - assert error["errors"][0]["code"] == "RuntimeError" - assert error["errors"][0]["message"] == "We were asked to fail!!" + + # The error should contain a supportId field for tracking + assert "supportId" in error + assert isinstance(error["supportId"], str) + assert len(error["supportId"]) > 0 + + # The actual error details should be logged, not returned in response + log_messages = caplog.text + assert "OEC" in log_messages + assert "RuntimeError" in log_messages + assert "We were asked to fail!!" in log_messages async def test_get_results_before_tasks_finishes_returns_404( diff --git a/packages/service-library/tests/aiohttp/test_rest_middlewares.py b/packages/service-library/tests/aiohttp/test_rest_middlewares.py index 8fac98039061..21407d099eda 100644 --- a/packages/service-library/tests/aiohttp/test_rest_middlewares.py +++ b/packages/service-library/tests/aiohttp/test_rest_middlewares.py @@ -240,7 +240,7 @@ async def test_raised_unhandled_exception( # # ERROR servicelib.aiohttp.rest_middlewares:rest_middlewares.py:75 We apologize ... [OEC:128594540599840]. # { - # "exception_details": "Unexpected error", + # "exception_string": "Unexpected error", # "error_code": "OEC:128594540599840", # "context": { # "request.remote": "127.0.0.1", @@ -262,7 +262,7 @@ async def test_raised_unhandled_exception( assert response.method in caplog.text assert response.url.path in caplog.text - assert "exception_details" in caplog.text + assert "exception_string" in caplog.text assert "request.remote" in caplog.text assert "context" in caplog.text assert SomeUnexpectedError.__name__ in caplog.text diff --git a/packages/service-library/tests/aiohttp/test_rest_responses.py b/packages/service-library/tests/aiohttp/test_rest_responses.py index 8c80f86b2cdf..25cb9e3025d1 100644 --- a/packages/service-library/tests/aiohttp/test_rest_responses.py +++ b/packages/service-library/tests/aiohttp/test_rest_responses.py @@ -62,7 +62,6 @@ def test_collected_http_errors_map(status_code: int, http_error_cls: type[HTTPEr assert issubclass(http_error_cls, HTTPError) -@pytest.mark.parametrize("skip_details", [True, False]) @pytest.mark.parametrize("error_code", [None, create_error_code(Exception("fake"))]) @pytest.mark.parametrize( "http_error_cls", @@ -88,7 +87,7 @@ def test_collected_http_errors_map(status_code: int, http_error_cls: type[HTTPEr ], ) def tests_exception_to_response( - skip_details: bool, error_code: ErrorCodeStr | None, http_error_cls: type[HTTPError] + error_code: ErrorCodeStr | None, http_error_cls: type[HTTPError] ): expected_status_reason = "SHORT REASON" expected_error_message = "Something whent wrong !" @@ -99,8 +98,6 @@ def tests_exception_to_response( error_message=expected_error_message, status_reason=expected_status_reason, http_error_cls=http_error_cls, - skip_internal_error_details=skip_details - and (http_error_cls == web.HTTPInternalServerError), error_code=error_code, ) diff --git a/packages/service-library/tests/test_logging_errors.py b/packages/service-library/tests/test_logging_errors.py index ac99c2fd657c..5baf6166943b 100644 --- a/packages/service-library/tests/test_logging_errors.py +++ b/packages/service-library/tests/test_logging_errors.py @@ -58,7 +58,7 @@ class MyError(OsparcErrorMixin, RuntimeError): # ERROR root:test_logging_utils.py:417 Nice message to user [OEC:126055703573984]. # { - # "exception_details": "My error 123", + # "exception_string": "My error 123", # "error_code": "OEC:126055703573984", # "context": { # "user_id": 123, diff --git a/services/web/server/src/simcore_service_webserver/login/_auth_service.py b/services/web/server/src/simcore_service_webserver/login/_auth_service.py index e505ecb789ad..99d1d4698f5b 100644 --- a/services/web/server/src/simcore_service_webserver/login/_auth_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_auth_service.py @@ -53,14 +53,14 @@ async def check_authorized_user_credentials_or_raise( if not user: raise web.HTTPUnauthorized( - reason=MSG_UNKNOWN_EMAIL, content_type=MIMETYPE_APPLICATION_JSON + text=MSG_UNKNOWN_EMAIL, content_type=MIMETYPE_APPLICATION_JSON ) _login_service.validate_user_status(user=user, support_email=product.support_email) if not security_service.check_password(password, user["password_hash"]): raise web.HTTPUnauthorized( - reason=MSG_WRONG_PASSWORD, content_type=MIMETYPE_APPLICATION_JSON + text=MSG_WRONG_PASSWORD, content_type=MIMETYPE_APPLICATION_JSON ) return user @@ -83,5 +83,5 @@ async def check_authorized_user_in_product_or_raise( ) ): raise web.HTTPUnauthorized( - reason=MSG_UNKNOWN_EMAIL, content_type=MIMETYPE_APPLICATION_JSON + text=MSG_UNKNOWN_EMAIL, content_type=MIMETYPE_APPLICATION_JSON ) diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py index 770b4d99cbc1..5874b4e80edc 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/change.py @@ -150,9 +150,9 @@ def _get_error_context( # do not want to forward but rather log due to the special rules in this entrypoint _logger.warning( **create_troubleshotting_log_kwargs( - f"{_error_msg_prefix} for invalid user. {_error_msg_suffix}.", + f"{_error_msg_prefix} for invalid user. {err.text}. {_error_msg_suffix}", error=err, - error_context=_get_error_context(user), + error_context={**_get_error_context(user), "error.text": err.text}, ) ) ok = False @@ -168,7 +168,7 @@ def _get_error_context( ): _logger.warning( **create_troubleshotting_log_kwargs( - f"{_error_msg_prefix} for a user with NO access to this product. {_error_msg_suffix}.", + f"{_error_msg_prefix} for a user with NO access to this product. {_error_msg_suffix}", error=Exception("User cannot access this product"), error_context=_get_error_context(user), ) @@ -297,7 +297,7 @@ async def change_password(request: web.Request): passwords.current.get_secret_value(), user["password_hash"] ): raise web.HTTPUnprocessableEntity( - reason=MSG_WRONG_PASSWORD, content_type=MIMETYPE_APPLICATION_JSON + text=MSG_WRONG_PASSWORD, content_type=MIMETYPE_APPLICATION_JSON ) # 422 await db.update_user( diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py index b657d7d5a83b..04ad8307e3f1 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/confirmation.py @@ -237,7 +237,7 @@ async def phone_confirmation(request: web.Request): if not settings.LOGIN_2FA_REQUIRED: raise web.HTTPServiceUnavailable( - reason="Phone registration is not available", + text="Phone registration is not available", content_type=MIMETYPE_APPLICATION_JSON, ) @@ -257,7 +257,7 @@ async def phone_confirmation(request: web.Request): except UniqueViolation as err: raise web.HTTPUnauthorized( - reason="Invalid phone number", + text="Invalid phone number", content_type=MIMETYPE_APPLICATION_JSON, ) from err @@ -265,7 +265,7 @@ async def phone_confirmation(request: web.Request): # fails because of invalid or no code raise web.HTTPUnauthorized( - reason="Invalid 2FA code", content_type=MIMETYPE_APPLICATION_JSON + text="Invalid 2FA code", content_type=MIMETYPE_APPLICATION_JSON ) @@ -312,7 +312,7 @@ async def complete_reset_password(request: web.Request): return flash_response(MSG_PASSWORD_CHANGED) raise web.HTTPUnauthorized( - reason=MSG_PASSWORD_CHANGE_NOT_ALLOWED.format( + text=MSG_PASSWORD_CHANGE_NOT_ALLOWED.format( support_email=product.support_email ), content_type=MIMETYPE_APPLICATION_JSON, diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py index 88f2dd336f3c..d74a1bf00e15 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/preregistration.py @@ -72,7 +72,7 @@ async def request_product_account(request: web.Request): if body.captcha != session.get(CAPTCHA_SESSION_KEY): raise web.HTTPUnprocessableEntity( - reason=MSG_WRONG_CAPTCHA__INVALID, content_type=MIMETYPE_APPLICATION_JSON + text=MSG_WRONG_CAPTCHA__INVALID, content_type=MIMETYPE_APPLICATION_JSON ) session.pop(CAPTCHA_SESSION_KEY, None) @@ -113,7 +113,7 @@ async def unregister_account(request: web.Request): body.password.get_secret_value(), credentials.password_hash ): raise web.HTTPConflict( - reason="Wrong email or password. Please try again to delete this account" + text="Wrong email or password. Please try again to delete this account" ) with log_context( diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py index f6cffbdbc600..96eeda1f8ff4 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/registration.py @@ -168,7 +168,7 @@ async def register(request: web.Request): < settings.LOGIN_PASSWORD_MIN_LENGTH ): raise web.HTTPUnauthorized( - reason=MSG_WEAK_PASSWORD.format( + text=MSG_WEAK_PASSWORD.format( LOGIN_PASSWORD_MIN_LENGTH=settings.LOGIN_PASSWORD_MIN_LENGTH ), content_type=MIMETYPE_APPLICATION_JSON, @@ -198,7 +198,7 @@ async def register(request: web.Request): invitation_code = registration.invitation if invitation_code is None: raise web.HTTPBadRequest( - reason="invitation field is required", + text="invitation field is required", content_type=MIMETYPE_APPLICATION_JSON, ) @@ -374,7 +374,7 @@ async def register_phone(request: web.Request): if not settings.LOGIN_2FA_REQUIRED: raise web.HTTPServiceUnavailable( - reason="Phone registration is not available", + text="Phone registration is not available", content_type=MIMETYPE_APPLICATION_JSON, ) @@ -436,6 +436,6 @@ async def register_phone(request: web.Request): ) raise web.HTTPServiceUnavailable( - reason=user_error_msg, + text=user_error_msg, content_type=MIMETYPE_APPLICATION_JSON, ) from err diff --git a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py index bb9cc3ff01ce..f95134bfb060 100644 --- a/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py +++ b/services/web/server/src/simcore_service_webserver/login/_controller/rest/twofa.py @@ -55,12 +55,12 @@ async def resend_2fa_code(request: web.Request): user = await db.get_user({"email": resend_2fa_.email}) if not user: raise web.HTTPUnauthorized( - reason=MSG_UNKNOWN_EMAIL, content_type=MIMETYPE_APPLICATION_JSON + text=MSG_UNKNOWN_EMAIL, content_type=MIMETYPE_APPLICATION_JSON ) if not settings.LOGIN_2FA_REQUIRED: raise web.HTTPServiceUnavailable( - reason="2FA login is not available", + text="2FA login is not available", content_type=MIMETYPE_APPLICATION_JSON, ) diff --git a/services/web/server/src/simcore_service_webserver/login/_invitations_service.py b/services/web/server/src/simcore_service_webserver/login/_invitations_service.py index df26f4f6aae0..7b0b5de660aa 100644 --- a/services/web/server/src/simcore_service_webserver/login/_invitations_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_invitations_service.py @@ -102,7 +102,7 @@ async def _raise_if_registered_in_product(app: web.Application, user_email, prod app, user_email=user_email, group_id=product.group_id ): raise web.HTTPConflict( - reason=MSG_EMAIL_ALREADY_REGISTERED, + text=MSG_EMAIL_ALREADY_REGISTERED, content_type=MIMETYPE_APPLICATION_JSON, ) @@ -163,7 +163,7 @@ async def check_other_registrations( UserStatus.DELETED, ) raise web.HTTPConflict( - reason=MSG_USER_DISABLED.format( + text=MSG_USER_DISABLED.format( support_email=current_product.support_email ), content_type=MIMETYPE_APPLICATION_JSON, @@ -230,7 +230,7 @@ def _invitations_request_context(invitation_code: str) -> Iterator[URL]: ) ) raise web.HTTPForbidden( - reason=user_error_msg, + text=user_error_msg, content_type=MIMETYPE_APPLICATION_JSON, ) from err @@ -247,7 +247,7 @@ def _invitations_request_context(invitation_code: str) -> Iterator[URL]: ) ) raise web.HTTPServiceUnavailable( - reason=user_error_msg, + text=user_error_msg, content_type=MIMETYPE_APPLICATION_JSON, ) from err @@ -323,7 +323,7 @@ async def check_and_consume_invitation( _logger.info("Invitation with %s was consumed", f"{confirmation_token=}") raise web.HTTPForbidden( - reason=( + text=( "Invalid invitation code." "Your invitation was already used or might have expired." + MSG_INVITATIONS_CONTACT_SUFFIX diff --git a/services/web/server/src/simcore_service_webserver/login/_login_service.py b/services/web/server/src/simcore_service_webserver/login/_login_service.py index 259795c8cc37..8aa0160b6c94 100644 --- a/services/web/server/src/simcore_service_webserver/login/_login_service.py +++ b/services/web/server/src/simcore_service_webserver/login/_login_service.py @@ -9,7 +9,6 @@ from pydantic import PositiveInt from servicelib.aiohttp import observer from servicelib.aiohttp.status import HTTP_200_OK -from servicelib.mimetype_constants import MIMETYPE_APPLICATION_JSON from simcore_postgres_database.models.users import UserRole from ..db.models import ConfirmationAction, UserStatus @@ -56,26 +55,22 @@ def validate_user_status(*, user: dict, support_email: str): if user_status == DELETED: raise web.HTTPUnauthorized( - reason=MSG_USER_DELETED.format(support_email=support_email), - content_type=MIMETYPE_APPLICATION_JSON, + text=MSG_USER_DELETED.format(support_email=support_email), ) # 401 if user_status == BANNED or user["role"] == ANONYMOUS: raise web.HTTPUnauthorized( - reason=MSG_USER_BANNED.format(support_email=support_email), - content_type=MIMETYPE_APPLICATION_JSON, + text=MSG_USER_BANNED.format(support_email=support_email), ) # 401 if user_status == EXPIRED: raise web.HTTPUnauthorized( - reason=MSG_USER_EXPIRED.format(support_email=support_email), - content_type=MIMETYPE_APPLICATION_JSON, + text=MSG_USER_EXPIRED.format(support_email=support_email), ) # 401 if user_status == CONFIRMATION_PENDING: raise web.HTTPUnauthorized( - reason=MSG_ACTIVATION_REQUIRED, - content_type=MIMETYPE_APPLICATION_JSON, + text=MSG_ACTIVATION_REQUIRED, ) # 401 assert user_status == ACTIVE # nosec diff --git a/services/web/server/src/simcore_service_webserver/login/errors.py b/services/web/server/src/simcore_service_webserver/login/errors.py index 835c971d312f..628014621b9d 100644 --- a/services/web/server/src/simcore_service_webserver/login/errors.py +++ b/services/web/server/src/simcore_service_webserver/login/errors.py @@ -41,7 +41,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: ) ) raise web.HTTPServiceUnavailable( - reason=front_end_msg, + text=front_end_msg, content_type=MIMETYPE_APPLICATION_JSON, ) from exc diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/nodes_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/nodes_rest.py index 4e556e95d22d..a44355851d9d 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/nodes_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/nodes_rest.py @@ -104,7 +104,7 @@ async def create_node(request: web.Request) -> web.Response: req_ctx.product_name, ): raise web.HTTPNotAcceptable( - reason=f"Service {body.service_key}:{body.service_version} is deprecated" + text=f"Service {body.service_key}:{body.service_version} is deprecated" ) # ensure the project exists @@ -155,7 +155,7 @@ async def get_node(request: web.Request) -> web.Response: ): project_node = project["workbench"][f"{path_params.node_id}"] raise web.HTTPNotAcceptable( - reason=f"Service {project_node['key']}:{project_node['version']} is deprecated!" + text=f"Service {project_node['key']}:{project_node['version']} is deprecated!" ) service_data: NodeGetIdle | NodeGetUnknown | DynamicServiceGet | NodeGet = ( @@ -447,13 +447,11 @@ async def replace_node_resources(request: web.Request) -> web.Response: return envelope_json_response(new_node_resources) except ProjectNodeResourcesInvalidError as exc: raise web.HTTPUnprocessableEntity( # 422 - reason=f"{exc}", text=f"{exc}", content_type=MIMETYPE_APPLICATION_JSON, ) from exc except ProjectNodeResourcesInsufficientRightsError as exc: raise web.HTTPForbidden( - reason=f"{exc}", text=f"{exc}", content_type=MIMETYPE_APPLICATION_JSON, ) from exc diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/projects_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/projects_rest.py index 9fcfc3cad888..277d235dbb10 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/projects_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/projects_rest.py @@ -360,7 +360,7 @@ async def delete_project(request: web.Request): # that project is still in use if req_ctx.user_id in project_users: raise web.HTTPForbidden( - reason="Project is still open in another tab/browser." + text="Project is still open in another tab/browser." "It cannot be deleted until it is closed." ) if project_users: @@ -369,7 +369,7 @@ async def delete_project(request: web.Request): for uid in project_users } raise web.HTTPForbidden( - reason=f"Project is open by {other_user_names}. " + text=f"Project is open by {other_user_names}. " "It cannot be deleted until the project is closed." ) @@ -379,7 +379,7 @@ async def delete_project(request: web.Request): project_uuid=path_params.project_id, ): raise web.HTTPConflict( - reason=f"Project {path_params.project_id} is locked: {project_locked_state=}" + text=f"Project {path_params.project_id} is locked: {project_locked_state=}" ) await _projects_service.submit_delete_project_task( diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py index 8c11767cc93f..934fbcb9df65 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/projects_states_rest.py @@ -154,7 +154,7 @@ async def open_project(request: web.Request) -> web.Response: ), ) raise web.HTTPServiceUnavailable( - reason="Unexpected error while starting services." + text="Unexpected error while starting services." ) from exc diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/wallets_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/wallets_rest.py index 3646cfc7950b..cd5e93be9bb6 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/wallets_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/wallets_rest.py @@ -116,7 +116,7 @@ async def pay_project_debt(request: web.Request): ) if not current_wallet: raise web.HTTPNotFound( - reason="Project doesn't have any wallet associated to the project" + text="Project doesn't have any wallet associated to the project" ) if current_wallet.wallet_id == path_params.wallet_id: diff --git a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py index 04671fdd21b1..861d635a8bbd 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py +++ b/services/web/server/src/simcore_service_webserver/projects/_crud_api_create.py @@ -92,7 +92,7 @@ async def _prepare_project_copy( ) if project_data_size >= max_bytes: raise web.HTTPUnprocessableEntity( - reason=f"Source project data size is {project_data_size.human_readable()}." + text=f"Source project data size is {project_data_size.human_readable()}." f"This is larger than the maximum {max_bytes.human_readable()} allowed for copying." "TIP: Please reduce the study size or contact application support." ) diff --git a/services/web/server/src/simcore_service_webserver/publications/_rest.py b/services/web/server/src/simcore_service_webserver/publications/_rest.py index 63b9b64b61c3..6966a36baf22 100644 --- a/services/web/server/src/simcore_service_webserver/publications/_rest.py +++ b/services/web/server/src/simcore_service_webserver/publications/_rest.py @@ -46,11 +46,13 @@ async def service_submission(request: web.Request): maxsize = 10 * 1024 * 1024 # 10MB actualsize = len(filedata) if actualsize > maxsize: - raise web.HTTPRequestEntityTooLarge(maxsize, actualsize) + raise web.HTTPRequestEntityTooLarge( + max_size=maxsize, actual_size=actualsize + ) filename = part.filename # type: ignore[union-attr] # PC, IP Whoever is in charge of this. please have a look continue raise web.HTTPUnsupportedMediaType( - reason=f"One part had an unexpected type: {part.headers[hdrs.CONTENT_TYPE]}" + text=f"One part had an unexpected type: {part.headers[hdrs.CONTENT_TYPE]}" ) support_email_address = product.support_email diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_utils.py b/services/web/server/src/simcore_service_webserver/resource_usage/_utils.py index 38c1f038b179..1476a16faf05 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_utils.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_utils.py @@ -25,11 +25,11 @@ def handle_client_exceptions(app: web.Application) -> Iterator[ClientSession]: if err.status == status.HTTP_404_NOT_FOUND: raise web.HTTPNotFound(text=MSG_RESOURCE_USAGE_TRACKER_NOT_FOUND) raise web.HTTPServiceUnavailable( - reason=MSG_RESOURCE_USAGE_TRACKER_SERVICE_UNAVAILABLE + text=MSG_RESOURCE_USAGE_TRACKER_SERVICE_UNAVAILABLE ) from err except (TimeoutError, ClientConnectionError) as err: _logger.debug("Request to resource usage tracker service failed: %s", err) raise web.HTTPServiceUnavailable( - reason=MSG_RESOURCE_USAGE_TRACKER_SERVICE_UNAVAILABLE + text=MSG_RESOURCE_USAGE_TRACKER_SERVICE_UNAVAILABLE ) from err diff --git a/services/web/server/src/simcore_service_webserver/rest/_handlers.py b/services/web/server/src/simcore_service_webserver/rest/_handlers.py index 5425d7341e40..721b9a651007 100644 --- a/services/web/server/src/simcore_service_webserver/rest/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/rest/_handlers.py @@ -1,7 +1,4 @@ -""" Basic healthckeck and configuration handles to the rest API - - -""" +"""Basic healthckeck and configuration handles to the rest API""" import datetime import logging @@ -40,7 +37,7 @@ async def healthcheck_liveness_probe(request: web.Request): health_report = await healthcheck.run(request.app) except HealthCheckError as err: _logger.warning("%s", err) - raise web.HTTPServiceUnavailable(reason="unhealthy") from err + raise web.HTTPServiceUnavailable(text="unhealthy") from err return web.json_response(data={"data": health_report}) diff --git a/services/web/server/src/simcore_service_webserver/security/_authz_web.py b/services/web/server/src/simcore_service_webserver/security/_authz_web.py index 7df6e4bfb945..8e88269918d3 100644 --- a/services/web/server/src/simcore_service_webserver/security/_authz_web.py +++ b/services/web/server/src/simcore_service_webserver/security/_authz_web.py @@ -6,7 +6,7 @@ from models_library.users import UserID from ._authz_access_model import AuthContextDict, OptionalContext -from ._constants import PERMISSION_PRODUCT_LOGIN_KEY +from ._constants import MSG_UNAUTHORIZED, PERMISSION_PRODUCT_LOGIN_KEY assert PERMISSION_PRODUCT_LOGIN_KEY # nosec @@ -28,7 +28,7 @@ async def check_user_authorized(request: web.Request) -> UserID: # NOTE: Same as aiohttp_security.api.check_authorized user_id: UserID | None = await aiohttp_security.api.authorized_userid(request) if user_id is None: - raise web.HTTPUnauthorized + raise web.HTTPUnauthorized(text=MSG_UNAUTHORIZED) return user_id @@ -53,4 +53,4 @@ async def check_user_permission( msg += f" {context.get('product_name')}" else: msg += f" {permission}" - raise web.HTTPForbidden(reason=msg) + raise web.HTTPForbidden(text=msg) diff --git a/services/web/server/src/simcore_service_webserver/security/_constants.py b/services/web/server/src/simcore_service_webserver/security/_constants.py index a7b03fb3db7e..62974bb6adb6 100644 --- a/services/web/server/src/simcore_service_webserver/security/_constants.py +++ b/services/web/server/src/simcore_service_webserver/security/_constants.py @@ -1,5 +1,5 @@ from typing import Final MSG_AUTH_NOT_AVAILABLE: Final[str] = "Authentication service is temporary unavailable" - +MSG_UNAUTHORIZED: Final[str] = "Unauthorized" PERMISSION_PRODUCT_LOGIN_KEY: Final[str] = "product.login" diff --git a/services/web/server/src/simcore_service_webserver/storage/_rest.py b/services/web/server/src/simcore_service_webserver/storage/_rest.py index d48d9f3a680b..7166e8096cc6 100644 --- a/services/web/server/src/simcore_service_webserver/storage/_rest.py +++ b/services/web/server/src/simcore_service_webserver/storage/_rest.py @@ -139,7 +139,7 @@ async def _forward_request_to_storage( match resp.status: case status.HTTP_422_UNPROCESSABLE_ENTITY: raise web.HTTPUnprocessableEntity( - reason=await resp.text(), content_type=resp.content_type + text=await resp.text(), content_type=resp.content_type ) case status.HTTP_404_NOT_FOUND: raise web.HTTPNotFound(text=await resp.text()) diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_redirects_handlers.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_redirects_handlers.py index 605b00f623d1..d4f53399ccbd 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_redirects_handlers.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_redirects_handlers.py @@ -102,14 +102,14 @@ async def wrapper(request: web.Request) -> web.StreamResponse: except web.HTTPUnauthorized as err: raise _create_redirect_response_to_error_page( request.app, - message=f"{err.reason}. Please reload this page to login/register.", + message=f"{err.text}. Please reload this page to login/register.", status_code=err.status_code, ) from err except web.HTTPUnprocessableEntity as err: raise _create_redirect_response_to_error_page( request.app, - message=f"Invalid parameters in link: {err.reason}", + message=f"Invalid parameters in link: {err.text}", status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, # 422 ) from err diff --git a/services/web/server/src/simcore_service_webserver/users/_tokens_rest.py b/services/web/server/src/simcore_service_webserver/users/_tokens_rest.py index ef38c61eb295..19c5b9d23a30 100644 --- a/services/web/server/src/simcore_service_webserver/users/_tokens_rest.py +++ b/services/web/server/src/simcore_service_webserver/users/_tokens_rest.py @@ -33,7 +33,7 @@ async def _wrapper(request: web.Request) -> web.StreamResponse: except TokenNotFoundError as exc: raise web.HTTPNotFound( - reason=f"Token for {exc.service_id} not found" + text=f"Token for {exc.service_id} not found" ) from exc return _wrapper diff --git a/services/web/server/src/simcore_service_webserver/wallets/_constants.py b/services/web/server/src/simcore_service_webserver/wallets/_constants.py index eab6335e3df7..579c403ef28e 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_constants.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_constants.py @@ -1,10 +1,12 @@ from typing import Final -MSG_PRICE_NOT_DEFINED_ERROR: Final[ - str -] = "No payments are accepted until this product has a price" +from common_library.user_messages import user_message -MSG_BILLING_DETAILS_NOT_DEFINED_ERROR: Final[str] = ( +MSG_PRICE_NOT_DEFINED_ERROR: Final[str] = user_message( + "No payments are accepted until this product has a price" +) + +MSG_BILLING_DETAILS_NOT_DEFINED_ERROR: Final[str] = user_message( "Payments cannot be processed: Required billing details (e.g. country for tax) are missing from your account." "Please contact support to resolve this configuration issue." ) diff --git a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py index 4dcef92b71cc..386477b7f768 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py @@ -2,7 +2,6 @@ import logging from aiohttp import web -from common_library.error_codes import create_error_code from models_library.api_schemas_webserver.wallets import ( CreateWalletBodyParams, PutWalletBodyParams, @@ -18,7 +17,6 @@ parse_request_path_parameters_as, ) from servicelib.aiohttp.typing_extension import Handler -from servicelib.logging_errors import create_troubleshotting_log_kwargs from servicelib.request_keys import RQT_USERID_KEY from .._meta import API_VTAG as VTAG @@ -95,19 +93,9 @@ async def wrapper(request: web.Request) -> web.StreamResponse: raise web.HTTPPaymentRequired(text=f"{exc}") from exc except BillingDetailsNotFoundError as exc: - - error_code = create_error_code(exc) - user_error_msg = MSG_BILLING_DETAILS_NOT_DEFINED_ERROR - - _logger.exception( - **create_troubleshotting_log_kwargs( - user_error_msg, - error=exc, - error_code=error_code, - ) - ) - - raise web.HTTPServiceUnavailable(text=user_error_msg) from exc + raise web.HTTPServiceUnavailable( + text=MSG_BILLING_DETAILS_NOT_DEFINED_ERROR + ) from exc return wrapper diff --git a/services/web/server/tests/unit/with_dbs/04/wallets/payments/test_payments.py b/services/web/server/tests/unit/with_dbs/04/wallets/payments/test_payments.py index ade4a9d58c1f..cf06b0f2aeee 100644 --- a/services/web/server/tests/unit/with_dbs/04/wallets/payments/test_payments.py +++ b/services/web/server/tests/unit/with_dbs/04/wallets/payments/test_payments.py @@ -333,6 +333,7 @@ async def test_billing_info_missing_error( assert not data assert MSG_BILLING_DETAILS_NOT_DEFINED_ERROR in error["message"] + assert error["supportId"] is not None async def test_payment_not_found(