diff --git a/src/flask_jsonrpc/site.py b/src/flask_jsonrpc/site.py index 4345c101..d3d4e677 100644 --- a/src/flask_jsonrpc/site.py +++ b/src/flask_jsonrpc/site.py @@ -310,20 +310,6 @@ def handle_view_func(self: Self, view_func: t.Callable[..., t.Any], params: t.An if validate: binded_params = type_checker(view_func, binded_params) - - resp_view = current_app.ensure_sync(view_func)(**binded_params) - - # TODO: Enhance the checker to return the type - view_fun_annotations = t.get_type_hints(view_func) if validate else {} - view_fun_return: t.Any | None = view_fun_annotations.pop('return', type(None)) - if validate and resp_view is not None and view_fun_return is type(None): - resp_view_qn = qualified_name(resp_view) - view_fun_return_qn = qualified_name(view_fun_return) - raise TypeError( - f'return type of {resp_view_qn} must be a type; got {view_fun_return_qn} instead' - ) from None - - return resp_view except AnnotatedMetadataTypeError as e: self.logger.info('invalid annotated type checked for: %s', view_func.__name__, exc_info=e) raise InvalidParamsError( @@ -338,6 +324,27 @@ def handle_view_func(self: Self, view_func: t.Callable[..., t.Any], params: t.An self.logger.info('invalid type checked for: %s', getattr(view_func, '__name__', view_func), exc_info=e) raise InvalidParamsError(data={'message': str(e)}) from e + try: + resp_view = current_app.ensure_sync(view_func)(**binded_params) + except TypeCheckError as e: + # Only the argument type checking performed by typeguard while calling the view is an + # Invalid params error; a TypeError raised by the view's own logic is left to propagate + # so that it is reported as a Server error instead. + self.logger.info('invalid type checked for: %s', getattr(view_func, '__name__', view_func), exc_info=e) + raise InvalidParamsError(data={'message': str(e)}) from e + + # TODO: Enhance the checker to return the type + view_fun_annotations = t.get_type_hints(view_func) if validate else {} + view_fun_return: t.Any | None = view_fun_annotations.pop('return', type(None)) + if validate and resp_view is not None and view_fun_return is type(None): + resp_view_qn = qualified_name(resp_view) + view_fun_return_qn = qualified_name(view_fun_return) + raise InvalidParamsError( + data={'message': f'return type of {resp_view_qn} must be a type; got {view_fun_return_qn} instead'} + ) from None + + return resp_view + def dispatch( self: Self, req_json: dict[str, t.Any] ) -> tuple[t.Any, int, Headers | dict[str, str] | tuple[str] | list[tuple[str]]]: diff --git a/tests/unit/test_site.py b/tests/unit/test_site.py index 61bb4a60..786dfeaa 100644 --- a/tests/unit/test_site.py +++ b/tests/unit/test_site.py @@ -843,6 +843,36 @@ def view_func(name: str) -> None: assert headers == {} +def test_site_with_view_func_raising_type_error() -> None: + def view_func(name: str) -> str: + raise TypeError('some runtime error') + + view_func.jsonrpc_validate = True + view_func.jsonrpc_method_params = {'name': str} + view_func.jsonrpc_method_return = str + + app = Flask('site') + jsonrpc_site = JSONRPCSite(version='1.0.0', path='/path', base_url='/base') + jsonrpc_site.register('app.view_func', view_func=view_func) + + with app.test_request_context( + '/base/path', method='POST', json={'id': 1, 'jsonrpc': '2.0', 'method': 'app.view_func', 'params': ['Lou']} + ): + rv, status_code, headers = jsonrpc_site.dispatch_request() + assert rv == { + 'id': 1, + 'jsonrpc': '2.0', + 'error': { + 'code': -32000, + 'data': {'message': 'some runtime error'}, + 'message': 'Server error', + 'name': 'ServerError', + }, + } + assert status_code == 500 + assert headers == {} + + def test_site_with_view_func_return_annotated_raises_exc() -> None: def view_func( name: t.Annotated[str, 'documentation of name parameter'],