Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add error handler to unexpected exception #568

Merged
merged 1 commit into from
Sep 27, 2024

Conversation

nycholas
Copy link
Member

Example:

app = Flask('minimal')
jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True)

class MyException(Exception):
    pass

@jsonrpc.errorhandler(MyException)
def handle_my_exception(ex: MyException) -> t.Dict[str, t.Any]:
    return {'message': 'It is a custom exception', 'code': '0001'}

@jsonrpc.method('App.failsWithCustomException')
def fails_with_custom_exception(_string: t.Optional[str] = None) -> t.NoReturn:
    raise MyException('example of fail with custom exception that will be handled')

Note: The implementation does not support a global error handler, only a local one. In other words, the registration of an error handler will only be available for the JSON-RPC that has been registered.

Resolve #375

Example:

```
app = Flask('minimal')
jsonrpc = JSONRPC(app, '/api', enable_web_browsable_api=True)

class MyException(Exception):
    pass

@jsonrpc.errorhandler(MyException)
def handle_my_exception(ex: MyException) -> t.Dict[str, t.Any]:
    return {'message': 'It is a custom exception', 'code': '0001'}

@jsonrpc.method('App.failsWithCustomException')
def fails_with_custom_exception(_string: t.Optional[str] = None) -> t.NoReturn:
    raise MyException('example of fail with custom exception that will be handled')
````

Note: The implementation does not support a global error handler, only
a local one. In other words, the registration of an error handler will
only be available for the JSON-RPC that has been registered.

Resolve #375
@nycholas
Copy link
Member Author

github-actions-workflow-tests

Copy link

Running "Tests [Docker]" workflow.

@nycholas nycholas merged commit 475b258 into master Sep 27, 2024
13 checks passed
@nycholas nycholas deleted the feature/375-handling-error branch September 27, 2024 15:27
@Talkless
Copy link
Contributor

Talkless commented Oct 9, 2024

@nycholas But I wonder, can I re-throw that exception so that if @jwt_required() throws anything, it will pass through jrson-rpc and default flask-jwt-ext handlers will be invoked. They handle JWT errors and return proper responses, like 401, inestead of 500 we get due to flask-jsonrcp.

@Talkless
Copy link
Contributor

Talkless commented Oct 9, 2024

@nycholas maybe we could retrun same excetion ex object so it will be "passed" (re-raised) to the default Flask handlers, as it looks like flask-json-rpc uses @app.errorhandler for it's implementation:

https://github.com/vimalloc/flask-jwt-extended/blob/1326eb77158a0bc27d9d56b8c25a4c30be6133aa/flask_jwt_extended/jwt_manager.py#L130

@nycholas
Copy link
Member Author

nycholas commented Oct 10, 2024

@Talkless Extends the error handler implementation to be able to control the HTTP headers and HTTPS status code it can be done, something like this.

What the JSON-RPC specification disallowed is changing the struct of the response, it must be:

{
  'code': -32000,
  'data': { // can be everything
    'message': "Some message"
  },
  'message': 'Server error',
  'name': 'ServerError',
}

And, you can mix the @jsonrpc.errorhandler and @app.errorhandler to achieve your objective.

[1] - https://www.jsonrpc.org/specification

@Talkless
Copy link
Contributor

I believe there's miscommunication of sorts.

If we have endpoint like this:

@jsonrpc_client.method('setting.get', notification=False)
@jwt_required()
def setting__get(
        key: str
) -> Optional[str]:

And if @jwt_required throws jwt.ExpiredSignatureError for example, it gets "sinked" by flask-jsonrcp and made into 500 Internal Sever Error instead of whatever specific (like 401 Unauthorized) which is already defined in flask-jwt-exteded: https://github.com/vimalloc/flask-jwt-extended/blob/1326eb77158a0bc27d9d56b8c25a4c30be6133aa/flask_jwt_extended/default_callbacks.py#L63 .

That's why it seems it whould be usefull to not sink or to pass-through some exception to already-defined handlers.

New @jsonrpc.errorhandler() is fine but should we then re-define handling of all the exceptions that flask-jwt-extended uses by ourselves?

paveikslas

What the JSON-RPC specification disallowed is changing the struct of the response, it must be:

I dunno, so you say we should also respond in json-rcp-way even in low-level authentication error?

@nycholas
Copy link
Member Author

I understood the use case, and what I mentioned, if the exception happens inside the JSON-RPC context, it needs to be a JSON-RPC exception, and I have suggested for that use case, the Flask handler errors can be used if you want to treat that the different way than JSON-RPC treat, besides that, I will allow that the JSON-RPC handler errors return the tuple of response, status_code, and headers to provide better integration with HTTP standards.

Another suggestion is that you can use a custom JSON-RPC API, to do everything that you want with the response, look at the example:

import typing as t

# Added in version 3.11.
from typing_extensions import Self

from flask import Flask, request

from flask_jsonrpc import JSONRPC, JSONRPCView

if t.TYPE_CHECKING:
    from flask.typing import ResponseReturnValue


class UnauthorizedError(Exception):
    pass


class AuthorizationView(JSONRPCView):
    def check_auth(self: Self) -> bool:
        username = request.headers.get('X-Username')
        password = request.headers.get('X-Password')
        return username == 'username' and password == 'secret'

    def dispatch_request(self: Self) -> 'ResponseReturnValue':
        if not self.check_auth():
            raise UnauthorizedError()
        return super().dispatch_request()


app = Flask('multiplesite')
jsonrpc = JSONRPC(app, '/api/v2', enable_web_browsable_api=True, jsonrpc_site_api=AuthorizationView)


@jsonrpc.method('App.index')
def index() -> str:
    return 'Welcome to Flask JSON-RPC Version API 1'

And the tests:

def test_index_with_invalid_auth(client: 'FlaskClient') -> None:
    with pytest.raises(UnauthorizedError):
        client.post(
            '/api/v2',
            json={'id': 1, 'jsonrpc': '2.0', 'method': 'App.index'},
            headers={'X-Username': 'username', 'X-Password': 'invalid'},
        )

So, the Flask handler error can be used to treat that exception. In other words, in your use case, the exception jwt.ExpiredSignatureError will be treated by the @app.errorhandler from flask-jwt-extended.

Does it make sense to you?

Thank you.

@nycholas
Copy link
Member Author

I dunno, so you say we should also respond in json-rcp-way even in low-level authentication error?

Yes, according to the specification, but if you see the answer above, I think that can solve your use case.

@Talkless
Copy link
Contributor

Does it make sense to you?

Yes, thanks, but not sure I would want to re-implement def check_auth as it's already done via @jwt_required().

I guess we'll just catch all flask-jwt-exteded exceptions and return either JSON-RPC or response tuple with HTTP error code then.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow to use custom error codes for unexpected errors
2 participants