Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions src/flask_jsonrpc/site.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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]]]:
Expand Down
30 changes: 30 additions & 0 deletions tests/unit/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand Down
Loading