Search code examples
python-3.xflaskauthorizationflask-jwt-extended

Flask does not process exception using Flask-JWT-Extended custom handler


What I have

I have a flask app that uses a Flask-JWT-Extended extension for processing user authentication and Flask-RESTx for API definition.

Here is a route for getting current user:

@user_api.route("/", endpoint="get_user")
class GetUser(Resource):
    """Handles HTTP requests to URL: /api/v1/user."""

    @user_api.doc(security="Bearer")
    @user_api.response(int(HTTPStatus.OK), "Token is currently valid.", user_model)
    @user_api.response(int(HTTPStatus.BAD_REQUEST), "Validation error.")
    @user_api.response(int(HTTPStatus.UNAUTHORIZED), "Token is invalid or expired.")
    @user_api.marshal_with(user_model)
    @jwt_required()
    def get(self) -> User:
        """Validate access token and return user info."""
        return current_user

In another place I've defined a custom error handler for the case when jwt_required() decorator raises a flask_jwt_extended.NoAuthorizationError exception:

@jwt.unauthorized_loader
def unauthorized_callback(message: str) -> ResponseReturnValue:
    """Get a response when a token is not present."""
    response, code = default_unauthorized_callback(message)
    response.headers["WWW-Authenticate"] = 'Bearer realm="registered_users@mydomain.com"'
    return response, code

What I expect

When I send a GET /api/v1/user request with no authorization header to I expect to get this response:

{
    "message": "Missing Authorization Header"
}

What really happens

But I get:

{
    "message": "Internal Server Error"
}

and a traceback in logs:

2023-08-06 17:04:28,007 [ERROR]: Exception on /api/v1/user/ [GET]
Traceback (most recent call last):
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask/app.py", line 1484, in full_dispatch_request
    rv = self.dispatch_request()
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask/app.py", line 1469, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask_restx/api.py", line 404, in wrapper
    resp = resource(*args, **kwargs)
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask/views.py", line 109, in view
    return current_app.ensure_sync(self.dispatch_request)(**kwargs)
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask_restx/resource.py", line 46, in dispatch_request
    resp = meth(*args, **kwargs)
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask_restx/marshalling.py", line 244, in wrapper
    resp = f(*args, **kwargs)
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask_jwt_extended/view_decorators.py", line 171, in decorator
    verify_jwt_in_request(
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask_jwt_extended/view_decorators.py", line 98, in verify_jwt_in_request
    jwt_data, jwt_header, jwt_location = _decode_jwt_from_request(
  File "/home/kirill/programming/monet/.venv/lib/python3.10/site-packages/flask_jwt_extended/view_decorators.py", line 362, in _decode_jwt_from_request
    raise NoAuthorizationError(errors[0])
flask_jwt_extended.exceptions.NoAuthorizationError: Missing Authorization Header
2023-08-06 17:04:28,014 [INFO]: 127.0.0.1 - - [06/Aug/2023 17:04:28] "GET /api/v1/user/ HTTP/1.1" 500 -

What is weird

The issue gets weirder because the test for this behavior is passing:

def test_get_user_no_header(client: FlaskClient, db: SQLAlchemy) -> None:
    """Test get user API request with no header provided.

    Checks:
    - Response has a valid structure.
    """
    register_user(client)
    login_user(client)
    response = client.get(url_for("api.get_user"))
    assert response.status_code == HTTPStatus.UNAUTHORIZED
    assert "message" in response.json and response.json["message"] == "Missing Authorization Header"
    assert "WWW-Authenticate" in response.headers
    assert response.headers["WWW-Authenticate"] == WWW_AUTH_NO_TOKEN

And when I run app with --debug argument it returns the right response.

But only when I run app as usual the error appears.

What I tried

I tried adding TRAP_HTTP_EXCEPTIONS = True in config, but it didn't work.


Solution

  • Turns out that Flask-JWT-Extended and Flask-RESTx (fork of Flask-RESTPlus) does not work well together...

    This comment in a Flask-JWT-Extended's issues on GitHub from a Flask-JWT-Extended's maintainer helped.

    To be short: it is required to set PROPAGATE_EXCEPTIONS to True