diff --git a/packages/service-library/src/servicelib/aiohttp/long_running_tasks/_error_handlers.py b/packages/service-library/src/servicelib/aiohttp/long_running_tasks/_error_handlers.py index 9e67b2b7f4d3..4534d7c951cb 100644 --- a/packages/service-library/src/servicelib/aiohttp/long_running_tasks/_error_handlers.py +++ b/packages/service-library/src/servicelib/aiohttp/long_running_tasks/_error_handlers.py @@ -18,13 +18,13 @@ async def base_long_running_error_handler(request, handler): return await handler(request) except (TaskNotFoundError, TaskNotCompletedError) as exc: _logger.debug("", exc_info=True) - error_fields = dict(code=exc.code, message=f"{exc}") + error_fields = {"code": exc.code, "message": f"{exc}"} raise web.HTTPNotFound( - reason=f"{json_dumps(error_fields)}", + text=f"{json_dumps(error_fields)}", ) from exc except TaskCancelledError as exc: # NOTE: only use-case would be accessing an already cancelled task # which should not happen, so we return a conflict _logger.debug("", exc_info=True) - error_fields = dict(code=exc.code, message=f"{exc}") - raise web.HTTPConflict(reason=f"{json_dumps(error_fields)}") from exc + error_fields = {"code": exc.code, "message": f"{exc}"} + raise web.HTTPConflict(text=f"{json_dumps(error_fields)}") from exc diff --git a/packages/service-library/src/servicelib/aiohttp/monitoring.py b/packages/service-library/src/servicelib/aiohttp/monitoring.py index 1c89b89ffd1b..84472c7e2f34 100644 --- a/packages/service-library/src/servicelib/aiohttp/monitoring.py +++ b/packages/service-library/src/servicelib/aiohttp/monitoring.py @@ -101,11 +101,11 @@ async def middleware_handler(request: web.Request, handler: Handler): log_exception = None raise resp from exc except asyncio.CancelledError as exc: - resp = web.HTTPInternalServerError(reason=f"{exc}") + resp = web.HTTPInternalServerError(text=f"{exc}") log_exception = exc raise resp from exc except Exception as exc: # pylint: disable=broad-except - resp = web.HTTPInternalServerError(reason=f"{exc}") + resp = web.HTTPInternalServerError(text=f"{exc}") resp.__cause__ = exc log_exception = exc raise resp from exc diff --git a/packages/service-library/src/servicelib/aiohttp/requests_validation.py b/packages/service-library/src/servicelib/aiohttp/requests_validation.py index d2141dee6a36..d555e535fe74 100644 --- a/packages/service-library/src/servicelib/aiohttp/requests_validation.py +++ b/packages/service-library/src/servicelib/aiohttp/requests_validation.py @@ -209,7 +209,7 @@ async def parse_request_body_as( try: body = await request.json() except json.decoder.JSONDecodeError as err: - raise web.HTTPBadRequest(reason=f"Invalid json in body: {err}") from err + raise web.HTTPBadRequest(text=f"Invalid json in body: {err}") from err if hasattr(model_schema_cls, "model_validate"): # NOTE: model_schema can be 'list[T]' or 'dict[T]' which raise TypeError diff --git a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py index 423965e2d9fb..a936cb8bfd62 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_middlewares.py @@ -81,24 +81,22 @@ async def _middleware_handler(request: web.Request, handler: Handler): # noqa: return await handler(request) except web.HTTPError as err: - # TODO: differenciate between server/client error - if not err.reason: - err.set_status(err.status_code, reason="Unexpected error") err.content_type = MIMETYPE_APPLICATION_JSON if not err.text or not is_enveloped_from_text(err.text): - error = ErrorGet( + error_message = err.text or err.reason or "Unexpected error" + error_model = ErrorGet( errors=[ ErrorItemType.from_error(err), ], status=err.status, logs=[ - LogMessageType(message=err.reason, level="ERROR"), + LogMessageType(message=error_message, level="ERROR"), ], - message=err.reason, + message=error_message, ) - err.text = EnvelopeFactory(error=error).as_text() + err.text = EnvelopeFactory(error=error_model).as_text() raise diff --git a/packages/service-library/src/servicelib/aiohttp/rest_responses.py b/packages/service-library/src/servicelib/aiohttp/rest_responses.py index 68ce54bfc4b7..08405fe54cf3 100644 --- a/packages/service-library/src/servicelib/aiohttp/rest_responses.py +++ b/packages/service-library/src/servicelib/aiohttp/rest_responses.py @@ -92,8 +92,7 @@ def create_http_error( ) return http_error_cls( - # Multiline not allowed in HTTP reason - reason=reason.replace("\n", " ") if reason else None, + reason=safe_status_message(reason), text=json_dumps( payload, ), @@ -129,3 +128,29 @@ def _pred(obj) -> bool: assert len(http_statuses) == len(found), "No duplicates" # nosec return http_statuses + + +def safe_status_message(message: str | None, max_length: int = 50) -> str | None: + """ + Truncates a status-message (i.e. `reason` in HTTP errors) to a maximum length, replacing newlines with spaces. + + If the message is longer than max_length, it will be truncated and "..." will be appended. + + This prevents issues such as: + - `aiohttp.http_exceptions.LineTooLong`: 400, message: Got more than 8190 bytes when reading Status line is too long. + - Multiline not allowed in HTTP reason attribute (aiohttp now raises ValueError). + + See: + - When to use http status and/or text messages https://github.com/ITISFoundation/osparc-simcore/pull/7760 + - [RFC 9112, Section 4.1: HTTP/1.1 Message Syntax and Routing](https://datatracker.ietf.org/doc/html/rfc9112#section-4.1) (status line length limits) + - [RFC 9110, Section 15.5: Reason Phrase](https://datatracker.ietf.org/doc/html/rfc9110#section-15.5) (reason phrase definition) + """ + if not message: + return None + + flat_message = message.replace("\n", " ") + if len(flat_message) <= max_length: + return flat_message + + # Truncate and add ellipsis + return flat_message[: max_length - 3] + "..." diff --git a/packages/service-library/tests/aiohttp/test_rest_middlewares.py b/packages/service-library/tests/aiohttp/test_rest_middlewares.py index 64e15801f94b..de5e80b85ae5 100644 --- a/packages/service-library/tests/aiohttp/test_rest_middlewares.py +++ b/packages/service-library/tests/aiohttp/test_rest_middlewares.py @@ -112,7 +112,7 @@ async def raise_error(_request: web.Request): @staticmethod async def raise_error_with_reason(_request: web.Request): - raise web.HTTPNotFound(reason="I did not find it") + raise web.HTTPNotFound(reason="A short phrase") @staticmethod async def raise_success(_request: web.Request): diff --git a/packages/service-library/tests/aiohttp/test_rest_responses.py b/packages/service-library/tests/aiohttp/test_rest_responses.py index 9865c805bfd8..d172d636f666 100644 --- a/packages/service-library/tests/aiohttp/test_rest_responses.py +++ b/packages/service-library/tests/aiohttp/test_rest_responses.py @@ -97,3 +97,58 @@ def tests_exception_to_response(skip_details: bool, error_code: ErrorCodeStr | N assert response_json["error"]["message"] == expected_reason assert response_json["error"]["supportId"] == error_code assert response_json["error"]["status"] == response.status + + +@pytest.mark.parametrize( + "input_message, expected_output", + [ + (None, None), # None input returns None + ("", None), # Empty string returns None + ("Simple message", "Simple message"), # Simple message stays the same + ( + "Message\nwith\nnewlines", + "Message with newlines", + ), # Newlines are replaced with spaces + ("A" * 100, "A" * 47 + "..."), # Long message gets truncated with ellipsis + ( + "Line1\nLine2\nLine3" + "X" * 100, + "Line1 Line2 Line3" + "X" * 30 + "...", + ), # Combined case: newlines and truncation with ellipsis + ], + ids=[ + "none_input", + "empty_string", + "simple_message", + "newlines_replaced", + "long_message_truncated", + "newlines_and_truncation", + ], +) +def test_safe_status_message(input_message: str | None, expected_output: str | None): + from servicelib.aiohttp.rest_responses import safe_status_message + + result = safe_status_message(input_message) + assert result == expected_output + + # Test with custom max_length + custom_max = 10 + result_custom = safe_status_message(input_message, max_length=custom_max) + + # Check length constraint is respected + if result_custom is not None: + assert len(result_custom) <= custom_max + # Check that ellipsis is added when truncated + if input_message and len(input_message.replace("\n", " ")) > custom_max: + assert result_custom.endswith("...") + + # Verify it can be used in a web response without raising exceptions + try: + # This would fail with long or multiline reasons + if result is not None: + web.Response(reason=result) + + # Test with custom length result too + if result_custom is not None: + web.Response(reason=result_custom) + except ValueError: + pytest.fail("safe_status_message result caused an exception in web.Response") diff --git a/services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py b/services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py index 127cc1d53e50..ac2e37836d59 100644 --- a/services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py +++ b/services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py @@ -39,7 +39,7 @@ def _handle_client_exceptions(app: web.Application) -> Iterator[ClientSession]: except ClientResponseError as err: if err.status == status.HTTP_404_NOT_FOUND: - raise web.HTTPNotFound(reason=MSG_CATALOG_SERVICE_NOT_FOUND) + raise web.HTTPNotFound(text=MSG_CATALOG_SERVICE_NOT_FOUND) from err raise web.HTTPServiceUnavailable( reason=MSG_CATALOG_SERVICE_UNAVAILABLE ) from err diff --git a/services/web/server/src/simcore_service_webserver/exception_handling/_factory.py b/services/web/server/src/simcore_service_webserver/exception_handling/_factory.py index 29e3e325117c..9aa00b9c997b 100644 --- a/services/web/server/src/simcore_service_webserver/exception_handling/_factory.py +++ b/services/web/server/src/simcore_service_webserver/exception_handling/_factory.py @@ -5,6 +5,7 @@ from common_library.error_codes import create_error_code from common_library.json_serialization import json_dumps from models_library.rest_error import ErrorGet +from servicelib.aiohttp.rest_responses import safe_status_message from servicelib.aiohttp.web_exceptions_extension import get_all_aiohttp_http_exceptions from servicelib.logging_errors import create_troubleshotting_log_kwargs from servicelib.status_codes_utils import is_5xx_server_error, is_error @@ -40,8 +41,7 @@ def create_error_response(error: ErrorGet, status_code: int) -> web.Response: return web.json_response( data={"error": error.model_dump(exclude_unset=True, mode="json")}, dumps=json_dumps, - # NOTE: Multiline not allowed in HTTP reason attribute (aiohttp now raises ValueError) - reason=error.message.replace("\n", " ") if error.message else None, + reason=safe_status_message(error.message), status=status_code, ) diff --git a/services/web/server/src/simcore_service_webserver/exporter/exceptions.py b/services/web/server/src/simcore_service_webserver/exporter/exceptions.py index baeff42beadf..4edaa66f2264 100644 --- a/services/web/server/src/simcore_service_webserver/exporter/exceptions.py +++ b/services/web/server/src/simcore_service_webserver/exporter/exceptions.py @@ -6,5 +6,4 @@ class SDSException(HTTPBadRequest): # pylint: disable=too-many-ancestors """Basic exception for errors raised inside the module""" def __init__(self, message: str): - # Multiline not allowed in HTTP reason attribute - super().__init__(reason=message.replace("\n", " ") if message else None) + super().__init__(text=message) diff --git a/services/web/server/src/simcore_service_webserver/exporter/utils.py b/services/web/server/src/simcore_service_webserver/exporter/utils.py index c74f250c1690..8d8e8992f2a0 100644 --- a/services/web/server/src/simcore_service_webserver/exporter/utils.py +++ b/services/web/server/src/simcore_service_webserver/exporter/utils.py @@ -6,6 +6,7 @@ from aiohttp.abc import AbstractStreamWriter from aiohttp.typedefs import LooseHeaders from aiohttp.web import BaseRequest, FileResponse +from servicelib.aiohttp.rest_responses import safe_status_message class CleanupFileResponse(FileResponse): # pylint: disable=too-many-ancestors @@ -27,8 +28,7 @@ def __init__( path=path, chunk_size=chunk_size, status=status, - # Multiline not allowed in HTTP reason - reason=reason.replace("\n", " ") if reason else None, + reason=safe_status_message(reason), headers=headers, ) self.remove_tmp_dir_cb = remove_tmp_dir_cb 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 6d4767f85654..14a61d428438 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 @@ -216,7 +216,7 @@ def _get_error_context( error_context=_get_error_context(user), ) ) - raise web.HTTPServiceUnavailable(reason=MSG_CANT_SEND_MAIL) from err + raise web.HTTPServiceUnavailable(text=MSG_CANT_SEND_MAIL) from err # NOTE: Always same response: guideline #1 return flash_response(MSG_EMAIL_SENT.format(email=request_body.email), "INFO") @@ -241,7 +241,7 @@ async def initiate_change_email(request: web.Request): async with pass_or_acquire_connection(get_asyncpg_engine(request.app)) as conn: if await UsersRepo.is_email_used(conn, email=request_body.email): - raise web.HTTPUnprocessableEntity(reason="This email cannot be used") + raise web.HTTPUnprocessableEntity(text="This email cannot be used") # Reset if previously requested confirmation = await db.get_confirmation({"user": user, "action": CHANGE_EMAIL}) @@ -268,7 +268,7 @@ async def initiate_change_email(request: web.Request): except Exception as err: # pylint: disable=broad-except _logger.exception("Can not send change_email_email") await db.delete_confirmation(confirmation) - raise web.HTTPServiceUnavailable(reason=MSG_CANT_SEND_MAIL) from err + raise web.HTTPServiceUnavailable(text=MSG_CANT_SEND_MAIL) from err return flash_response(MSG_CHANGE_EMAIL_REQUESTED) 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 ba13b0dd7200..f6cffbdbc600 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 @@ -301,7 +301,7 @@ async def register(request: web.Request): await db.delete_confirmation_and_user(user, _confirmation) - raise web.HTTPServiceUnavailable(reason=user_error_msg) from err + raise web.HTTPServiceUnavailable(text=user_error_msg) from err return flash_response( "You are registered successfully! To activate your account, please, " 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 2c02ca019591..c7f98d6e3750 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 @@ -305,7 +305,7 @@ async def _stop_dynamic_service_task( except (RPCServerError, ServiceWaitingForManualInterventionError) as exc: # in case there is an error reply as not found - raise web.HTTPNotFound(reason=f"{exc}") from exc + raise web.HTTPNotFound(text=f"{exc}") from exc except ServiceWasNotFoundError: # in case the service is not found reply as all OK diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/ports_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/ports_rest.py index 396ba2bbae73..9e49a471c165 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/ports_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/ports_rest.py @@ -101,7 +101,7 @@ async def update_project_inputs(request: web.Request) -> web.Response: for input_update in inputs_updates: node_id = input_update.key if node_id not in current_inputs: - raise web.HTTPBadRequest(reason=f"Invalid input key [{node_id}]") + raise web.HTTPBadRequest(text=f"Invalid input key [{node_id}]") workbench[node_id].outputs = {KeyIDStr("out_1"): input_update.value} partial_workbench_data[node_id] = workbench[node_id].model_dump( 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 3085674e3036..a9fcd9619bd4 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 @@ -63,7 +63,7 @@ async def open_project(request: web.Request) -> web.Response: client_session_id = await request.json() except json.JSONDecodeError as exc: - raise web.HTTPBadRequest(reason="Invalid request body") from exc + raise web.HTTPBadRequest(text="Invalid request body") from exc try: project_type: ProjectType = await _projects_service.get_project_type( @@ -74,7 +74,7 @@ async def open_project(request: web.Request) -> web.Response: ) if project_type is ProjectType.TEMPLATE and user_role < UserRole.USER: # only USERS/TESTERS can do that - raise web.HTTPForbidden(reason="Wrong user role to open/edit a template") + raise web.HTTPForbidden(text="Wrong user role to open/edit a template") project = await _projects_service.get_project_for_user( request.app, @@ -101,7 +101,7 @@ async def open_project(request: web.Request) -> web.Response: app=request.app, max_number_of_studies_per_user=product.max_open_studies_per_user, ): - raise HTTPLockedError(reason="Project is locked, try later") + raise HTTPLockedError(text="Project is locked, try later") # the project can be opened, let's update its product links await _projects_service.update_project_linked_product( @@ -175,7 +175,7 @@ async def close_project(request: web.Request) -> web.Response: client_session_id = await request.json() except json.JSONDecodeError as exc: - raise web.HTTPBadRequest(reason="Invalid request body") from exc + raise web.HTTPBadRequest(text="Invalid request body") from exc # ensure the project exists await _projects_service.get_project_for_user( diff --git a/services/web/server/src/simcore_service_webserver/projects/_controller/tags_rest.py b/services/web/server/src/simcore_service_webserver/projects/_controller/tags_rest.py index 9fce75cda63e..ac38405f9ad0 100644 --- a/services/web/server/src/simcore_service_webserver/projects/_controller/tags_rest.py +++ b/services/web/server/src/simcore_service_webserver/projects/_controller/tags_rest.py @@ -32,7 +32,7 @@ async def add_project_tag(request: web.Request): request.match_info["project_uuid"], ) except KeyError as err: - raise web.HTTPBadRequest(reason=f"Invalid request parameter {err}") from err + raise web.HTTPBadRequest(text=f"Invalid request parameter {err}") from err project = await tags_api.add_tag( request.app, 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 6076cf781705..102707b2821e 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 @@ -469,10 +469,10 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche ) except JsonSchemaValidationError as exc: - raise web.HTTPBadRequest(reason="Invalid project data") from exc + raise web.HTTPBadRequest(text="Invalid project data") from exc except ProjectNotFoundError as exc: - raise web.HTTPNotFound(reason=f"Project {exc.project_uuid} not found") from exc + raise web.HTTPNotFound(text=f"Project {exc.project_uuid} not found") from exc except (ProjectInvalidRightsError, WorkspaceAccessForbiddenError) as exc: raise web.HTTPForbidden from exc @@ -485,7 +485,7 @@ async def create_project( # pylint: disable=too-many-arguments,too-many-branche user_id=user_id, simcore_user_agent=simcore_user_agent, ) - raise web.HTTPNotFound(reason=f"{exc}") from exc + raise web.HTTPNotFound(text=f"{exc}") from exc except asyncio.CancelledError: log.warning( diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_rest.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_rest.py index 338eed936a52..63442df2ce45 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_rest.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_admin_rest.py @@ -58,7 +58,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: return await handler(request) except (ValueError, PricingUnitDuplicationError) as exc: - raise web.HTTPBadRequest(reason=f"{exc}") from exc + raise web.HTTPBadRequest(text=f"{exc}") from exc except RPCServerError as exc: # NOTE: This will be improved; we will add a mapping between diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_rest.py b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_rest.py index 5bd826a24a56..1f4497e07cc9 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_rest.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_pricing_plans_rest.py @@ -39,7 +39,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: return await handler(request) except WalletAccessForbiddenError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc + raise web.HTTPForbidden(text=f"{exc}") from exc return wrapper diff --git a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_rest.py b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_rest.py index 53b6297937bc..68cf2c6a946a 100644 --- a/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_rest.py +++ b/services/web/server/src/simcore_service_webserver/resource_usage/_service_runs_rest.py @@ -47,38 +47,38 @@ async def wrapper(request: web.Request) -> web.StreamResponse: return await handler(request) except WalletAccessForbiddenError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc + raise web.HTTPForbidden(text=f"{exc}") from exc return wrapper -_ResorceUsagesListOrderQueryParams: type[ - RequestParameters -] = create_ordering_query_model_class( - ordering_fields={ - "wallet_id", - "wallet_name", - "user_id", - "user_email", - "project_id", - "project_name", - "node_id", - "node_name", - "root_parent_project_id", - "root_parent_project_name", - "service_key", - "service_version", - "service_type", - "started_at", - "stopped_at", - "service_run_status", - "credit_cost", - "transaction_status", - }, - default=OrderBy(field=IDStr("started_at"), direction=OrderDirection.DESC), - ordering_fields_api_to_column_map={ - "credit_cost": "osparc_credits", - }, +_ResorceUsagesListOrderQueryParams: type[RequestParameters] = ( + create_ordering_query_model_class( + ordering_fields={ + "wallet_id", + "wallet_name", + "user_id", + "user_email", + "project_id", + "project_name", + "node_id", + "node_name", + "root_parent_project_id", + "root_parent_project_name", + "service_key", + "service_version", + "service_type", + "started_at", + "stopped_at", + "service_run_status", + "credit_cost", + "transaction_status", + }, + default=OrderBy(field=IDStr("started_at"), direction=OrderDirection.DESC), + ordering_fields_api_to_column_map={ + "credit_cost": "osparc_credits", + }, + ) ) 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 3d74b4f6c56a..38c1f038b179 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 @@ -1,4 +1,3 @@ -import asyncio import logging from collections.abc import Iterator from contextlib import contextmanager @@ -22,14 +21,14 @@ def handle_client_exceptions(app: web.Application) -> Iterator[ClientSession]: session: ClientSession = get_client_session(app) yield session - except (ClientResponseError) as err: + except ClientResponseError as err: if err.status == status.HTTP_404_NOT_FOUND: - raise web.HTTPNotFound(reason=MSG_RESOURCE_USAGE_TRACKER_NOT_FOUND) + raise web.HTTPNotFound(text=MSG_RESOURCE_USAGE_TRACKER_NOT_FOUND) raise web.HTTPServiceUnavailable( reason=MSG_RESOURCE_USAGE_TRACKER_SERVICE_UNAVAILABLE ) from err - except (asyncio.TimeoutError, ClientConnectionError) as 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 diff --git a/services/web/server/src/simcore_service_webserver/security/_authz_policy.py b/services/web/server/src/simcore_service_webserver/security/_authz_policy.py index 3bd5408f4d3c..89b84859d399 100644 --- a/services/web/server/src/simcore_service_webserver/security/_authz_policy.py +++ b/services/web/server/src/simcore_service_webserver/security/_authz_policy.py @@ -38,7 +38,7 @@ def _handle_exceptions_as_503(): yield except DatabaseError as err: _logger.exception("Auth unavailable due to database error") - raise web.HTTPServiceUnavailable(reason=MSG_AUTH_NOT_AVAILABLE) from err + raise web.HTTPServiceUnavailable(text=MSG_AUTH_NOT_AVAILABLE) from err class AuthorizationPolicy(AbstractAuthorizationPolicy): diff --git a/services/web/server/src/simcore_service_webserver/session/access_policies.py b/services/web/server/src/simcore_service_webserver/session/access_policies.py index 064dddbc5562..8f76dc4db7b5 100644 --- a/services/web/server/src/simcore_service_webserver/session/access_policies.py +++ b/services/web/server/src/simcore_service_webserver/session/access_policies.py @@ -116,11 +116,11 @@ async def _wrapper(request: web.Request): with _access_tokens_cleanup_ctx(session) as access_tokens: access: _AccessToken | None = access_tokens.get(name, None) if not access: - raise web.HTTPUnauthorized(reason=unauthorized_reason) + raise web.HTTPUnauthorized(text=unauthorized_reason) access["count"] -= 1 # consume access count if access["count"] < 0 or _is_expired(access): - raise web.HTTPUnauthorized(reason=unauthorized_reason) + raise web.HTTPUnauthorized(text=unauthorized_reason) # update and keep for future accesses (e.g. retry this route) access_tokens[name] = access diff --git a/services/web/server/src/simcore_service_webserver/socketio/_handlers.py b/services/web/server/src/simcore_service_webserver/socketio/_handlers.py index 85a618f15d17..c864a735d52d 100644 --- a/services/web/server/src/simcore_service_webserver/socketio/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/socketio/_handlers.py @@ -1,7 +1,7 @@ -""" Defines **async** handlers for socket.io server +"""Defines **async** handlers for socket.io server - SEE https://pypi.python.org/pypi/python-socketio - SEE http://python-socketio.readthedocs.io/en/latest/ +SEE https://pypi.python.org/pypi/python-socketio +SEE http://python-socketio.readthedocs.io/en/latest/ """ import logging @@ -62,7 +62,7 @@ async def _handler(request: web.Request) -> tuple[UserID, ProductName]: _logger.error( "Tab ID is missing", extra=get_log_record_extra(user_id=user_id) ) - raise web.HTTPUnauthorized(reason=_MSG_UNAUTHORIZED_MISSING_SESSION_INFO) + raise web.HTTPUnauthorized(text=_MSG_UNAUTHORIZED_MISSING_SESSION_INFO) # here we keep the original HTTP request in the socket session storage sio = get_socket_server(app) diff --git a/services/web/server/src/simcore_service_webserver/statics/_handlers.py b/services/web/server/src/simcore_service_webserver/statics/_handlers.py index 0f37438e69c7..8b0fad4f997d 100644 --- a/services/web/server/src/simcore_service_webserver/statics/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/statics/_handlers.py @@ -30,7 +30,7 @@ async def get_cached_frontend_index(request: web.Request): APP_FRONTEND_CACHED_INDEXES_KEY ] if product_name not in cached_index_per_product: - raise web.HTTPNotFound(reason=f"No index.html found for {product_name}") + raise web.HTTPNotFound(text=f"No index.html found for {product_name}") return web.Response( body=cached_index_per_product[product_name], content_type=MIMETYPE_TEXT_HTML 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 b8a1f18a3981..9efd512aee88 100644 --- a/services/web/server/src/simcore_service_webserver/storage/_rest.py +++ b/services/web/server/src/simcore_service_webserver/storage/_rest.py @@ -142,9 +142,9 @@ async def _forward_request_to_storage( reason=await resp.text(), content_type=resp.content_type ) case status.HTTP_404_NOT_FOUND: - raise web.HTTPNotFound(reason=await resp.text()) + raise web.HTTPNotFound(text=await resp.text()) case _ if resp.status >= status.HTTP_400_BAD_REQUEST: - raise web.HTTPError(reason=await resp.text()) + raise web.HTTPError(text=await resp.text()) case _: payload = await resp.json() return _ResponseTuple(payload=payload, status_code=resp.status) diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py index e49f7ad1b4e7..0ca360d5aeb9 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_users.py @@ -204,7 +204,7 @@ async def get_or_create_guest_user( if not allow_anonymous_or_guest_users and (not user or user.get("role") == GUEST): # NOTE: if allow_anonymous_users=False then GUEST users are NOT allowed! - raise web.HTTPUnauthorized(reason=MSG_GUESTS_NOT_ALLOWED) + raise web.HTTPUnauthorized(text=MSG_GUESTS_NOT_ALLOWED) assert isinstance(user, dict) # nosec diff --git a/services/web/server/src/simcore_service_webserver/users/_preferences_rest.py b/services/web/server/src/simcore_service_webserver/users/_preferences_rest.py index 1793cd65ccd0..ba7094227322 100644 --- a/services/web/server/src/simcore_service_webserver/users/_preferences_rest.py +++ b/services/web/server/src/simcore_service_webserver/users/_preferences_rest.py @@ -34,7 +34,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: CouldNotCreateOrUpdateUserPreferenceError, FrontendUserPreferenceIsNotDefinedError, ) as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc + raise web.HTTPNotFound(text=f"{exc}") from exc return wrapper diff --git a/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py b/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py index 4ad171090ed3..3f567ca64349 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_groups_handlers.py @@ -1,6 +1,4 @@ -""" Handlers for project comments operations - -""" +"""Handlers for project comments operations""" import functools import logging @@ -36,10 +34,10 @@ async def wrapper(request: web.Request) -> web.StreamResponse: return await handler(request) except WalletGroupNotFoundError as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc + raise web.HTTPNotFound(text=f"{exc}") from exc except WalletAccessForbiddenError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc + raise web.HTTPForbidden(text=f"{exc}") from exc return wrapper @@ -97,13 +95,13 @@ async def list_wallet_groups(request: web.Request): req_ctx = RequestContext.model_validate(request) path_params = parse_request_path_parameters_as(WalletsPathParams, request) - wallets: list[ - WalletGroupGet - ] = await _groups_api.list_wallet_groups_by_user_and_wallet( - request.app, - user_id=req_ctx.user_id, - wallet_id=path_params.wallet_id, - product_name=req_ctx.product_name, + wallets: list[WalletGroupGet] = ( + await _groups_api.list_wallet_groups_by_user_and_wallet( + request.app, + user_id=req_ctx.user_id, + wallet_id=path_params.wallet_id, + product_name=req_ctx.product_name, + ) ) return envelope_json_response(wallets, web.HTTPOk) 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 22d085b90f1e..4dcef92b71cc 100644 --- a/services/web/server/src/simcore_service_webserver/wallets/_handlers.py +++ b/services/web/server/src/simcore_service_webserver/wallets/_handlers.py @@ -68,7 +68,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: PaymentMethodNotFoundError, UserDefaultWalletNotFoundError, ) as exc: - raise web.HTTPNotFound(reason=f"{exc}") from exc + raise web.HTTPNotFound(text=f"{exc}") from exc except ( PaymentUniqueViolationError, @@ -77,22 +77,22 @@ async def wrapper(request: web.Request) -> web.StreamResponse: PaymentMethodUniqueViolationError, InvalidPaymentMethodError, ) as exc: - raise web.HTTPConflict(reason=f"{exc}") from exc + raise web.HTTPConflict(text=f"{exc}") from exc except PaymentServiceUnavailableError as exc: - raise web.HTTPServiceUnavailable(reason=f"{exc}") from exc + raise web.HTTPServiceUnavailable(text=f"{exc}") from exc except WalletAccessForbiddenError as exc: - raise web.HTTPForbidden(reason=f"{exc}") from exc + raise web.HTTPForbidden(text=f"{exc}") from exc except BelowMinimumPaymentError as exc: - raise web.HTTPUnprocessableEntity(reason=f"{exc}") from exc + raise web.HTTPUnprocessableEntity(text=f"{exc}") from exc except ProductPriceNotDefinedError as exc: - raise web.HTTPConflict(reason=MSG_PRICE_NOT_DEFINED_ERROR) from exc + raise web.HTTPConflict(text=MSG_PRICE_NOT_DEFINED_ERROR) from exc except WalletNotEnoughCreditsError as exc: - raise web.HTTPPaymentRequired(reason=f"{exc}") from exc + raise web.HTTPPaymentRequired(text=f"{exc}") from exc except BillingDetailsNotFoundError as exc: @@ -107,7 +107,7 @@ async def wrapper(request: web.Request) -> web.StreamResponse: ) ) - raise web.HTTPServiceUnavailable(reason=user_error_msg) from exc + raise web.HTTPServiceUnavailable(text=user_error_msg) from exc return wrapper diff --git a/services/web/server/tests/unit/isolated/exporter/test_exporter_formatter_archive.py b/services/web/server/tests/unit/isolated/exporter/test_exporter_formatter_archive.py index 5d468b043a4b..55499a0caeb1 100644 --- a/services/web/server/tests/unit/isolated/exporter/test_exporter_formatter_archive.py +++ b/services/web/server/tests/unit/isolated/exporter/test_exporter_formatter_archive.py @@ -51,6 +51,6 @@ async def test_archive_already_exists(tmp_path, project_id): assert exc_info.type is SDSException assert ( - exc_info.value.args[0] + exc_info.value.text == f"Cannot archive '{tmp_path}/nested' because '{tmp_path}/nested/sds_{project_id}.zip' already exists" ) diff --git a/services/web/server/tests/unit/isolated/test_security_api.py b/services/web/server/tests/unit/isolated/test_security_api.py index 6f43d45e26c4..83751e25695a 100644 --- a/services/web/server/tests/unit/isolated/test_security_api.py +++ b/services/web/server/tests/unit/isolated/test_security_api.py @@ -59,7 +59,7 @@ async def _get_product_name(request: web.Request) -> ProductName | None: return product_name # NOTE: this or deduce from url - raise web.HTTPUnauthorized(reason="Invalid session. Reload / first") + raise web.HTTPUnauthorized(text="Invalid session. Reload / first") async def _forget_product_name(request: web.Request) -> ProductName | None: @@ -173,11 +173,11 @@ async def _login(request: web.Request): # Permission in this product: Has user access to product? if product_name not in registered_users[email]["registered_products"]: - raise web.HTTPForbidden(reason="no access to this product") + raise web.HTTPForbidden(text="no access to this product") # Authentication takes place here if body.get("password") != "secret": - raise web.HTTPUnauthorized(reason="wrong password") + raise web.HTTPUnauthorized(text="wrong password") # if all good, let's update session with return await remember_identity(request, web.HTTPOk(), user_email=email) 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 4b028a61dd81..ade4a9d58c1f 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 @@ -334,9 +334,6 @@ async def test_billing_info_missing_error( assert not data assert MSG_BILLING_DETAILS_NOT_DEFINED_ERROR in error["message"] - assert response.reason - assert MSG_BILLING_DETAILS_NOT_DEFINED_ERROR in response.reason - async def test_payment_not_found( latest_osparc_price: Decimal, @@ -356,7 +353,7 @@ async def test_payment_not_found( data, error = await assert_status(response, status.HTTP_404_NOT_FOUND) assert data is None - error_msg = error["errors"][0]["message"] + error_msg = error["message"] assert payment_id in error_msg assert ":cancel" not in error_msg @@ -391,7 +388,7 @@ async def test_payment_on_wallet_without_access( assert data is None assert error - error_msg = error["errors"][0]["message"] + error_msg = error["message"] assert f"{wallet.wallet_id}" in error_msg