Problem: I'm building a Flask backend using flask-restful, flask-jwt-extended, and PostgreSQL. When testing JWT token expiration via Postman, expired tokens consistently result in a 500 Internal Server Error instead of a 401 Unauthorized response.
Desired Behavior: When a JWT token expires, my API should return a JSON response: {"message": "Token has expired"}
Currently, an expired token results in this error: {"message": "Internal Server Error"}
Server Logs Traceback: jwt.exceptions.ExpiredSignatureError: Signature has expired
Relevent Code: Flask App Initialization (init.py)
from flask import Flask, jsonify
from flask_jwt_extended import JWTManager
from flask_restful import Api
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
CORS(app, supports_credentials=True)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'temporary_secret')
jwt = JWTManager(app)
db = SQLAlchemy(app)
api = Api(app)
migrate = Migrate(app, db)
# JWT error handlers
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return jsonify({"message": "Token has expired"}), 401
@jwt.invalid_token_loader
def invalid_token_callback(error):
return jsonify({"message": "Invalid token"}), 401
@jwt.unauthorized_loader
def unauthorized_callback(error):
return jsonify({"message": "Missing or invalid Authorization header"}), 401
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
Auth Resource (auth_resource.py)
from flask_restful import Resource, reqparse
from flask_jwt_extended import (
create_access_token, create_refresh_token, jwt_required, get_jwt_identity
)
from werkzeug.security import check_password_hash
from datetime import timedelta
from app.models import User
parser = reqparse.RequestParser()
parser.add_argument('username', required=True)
parser.add_argument('password', required=True)
class LoginResource(Resource):
def post(self):
data = parser.parse_args()
user = User.query.filter_by(username=data['username']).first()
if user and check_password_hash(user.password_hash, data['password']):
access_token = create_access_token(identity=user.id, expires_delta=timedelta(seconds=30))
refresh_token = create_refresh_token(identity=user.id, expires_delta=timedelta(minutes=2))
return {'access_token': access_token, 'refresh_token': refresh_token}, 200
return {'msg': 'Invalid credentials'}, 401
class ProtectedResource(Resource):
@jwt_required()
def get(self):
identity = get_jwt_identity()
return {'logged_in_as': identity}, 200
Testing Approach & Results (Postman) Login works (200 OK), returns tokens.
Access protected resource (200 OK) initially with valid token.
Wait 30 seconds (token expiration), then call protected resource again.
Expected: 401 {"message": "Token has expired"}
Actual: 500 Internal Server Error
Server Logs:
ERROR in app: Exception on /api/protected [GET]
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.11/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File "/usr/local/lib/python3.11/site-packages/flask_restful/__init__.py", line 604, in dispatch_request
resp = meth(*args, **kwargs)
File "/usr/local/lib/python3.11/site-packages/flask_jwt_extended/view_decorators.py", line 167, in decorator
verify_jwt_in_request(
File "/usr/local/lib/python3.11/site-packages/flask_jwt_extended/utils.py", line 128, in decode_token
return jwt_manager._decode_jwt_from_config(encoded_token, csrf_value, allow_expired)
File "/usr/local/lib/python3.11/site-packages/jwt/api_jwt.py", line 363, in _validate_exp
raise ExpiredSignatureError("Signature has expired")
jwt.exceptions.ExpiredSignatureError: Signature has expired
What I've Tried without success: Implemented JWT global error handlers (expired_token_loader).
Verified correct registration of JWT callbacks.
Simplified the endpoint code to remove internal exception handling (as JWT errors should be handled globally).
Fully rebuilt Docker containers multiple times to ensure fresh deployment.
Questions Why isn't my global expired_token_loader capturing the expired token exceptions from the decorator?
Is there an error in the way I've configured flask_jwt_extended with flask_restful that prevents global handlers from triggering?
What steps can I take to isolate or debug the issue further?
Environment Details Python: 3.11 (Dockerized)
Flask: latest
Flask-JWT-Extended: latest
Flask-Restful: latest
PostgreSQL database (Dockerized)
Docker Compose setup for backend, frontend, and database
Problem: I'm building a Flask backend using flask-restful, flask-jwt-extended, and PostgreSQL. When testing JWT token expiration via Postman, expired tokens consistently result in a 500 Internal Server Error instead of a 401 Unauthorized response.
Desired Behavior: When a JWT token expires, my API should return a JSON response: {"message": "Token has expired"}
Currently, an expired token results in this error: {"message": "Internal Server Error"}
Server Logs Traceback: jwt.exceptions.ExpiredSignatureError: Signature has expired
Relevent Code: Flask App Initialization (init.py)
from flask import Flask, jsonify
from flask_jwt_extended import JWTManager
from flask_restful import Api
from flask_cors import CORS
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
CORS(app, supports_credentials=True)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('DATABASE_URL')
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'temporary_secret')
jwt = JWTManager(app)
db = SQLAlchemy(app)
api = Api(app)
migrate = Migrate(app, db)
# JWT error handlers
@jwt.expired_token_loader
def expired_token_callback(jwt_header, jwt_payload):
return jsonify({"message": "Token has expired"}), 401
@jwt.invalid_token_loader
def invalid_token_callback(error):
return jsonify({"message": "Invalid token"}), 401
@jwt.unauthorized_loader
def unauthorized_callback(error):
return jsonify({"message": "Missing or invalid Authorization header"}), 401
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000, debug=True)
Auth Resource (auth_resource.py)
from flask_restful import Resource, reqparse
from flask_jwt_extended import (
create_access_token, create_refresh_token, jwt_required, get_jwt_identity
)
from werkzeug.security import check_password_hash
from datetime import timedelta
from app.models import User
parser = reqparse.RequestParser()
parser.add_argument('username', required=True)
parser.add_argument('password', required=True)
class LoginResource(Resource):
def post(self):
data = parser.parse_args()
user = User.query.filter_by(username=data['username']).first()
if user and check_password_hash(user.password_hash, data['password']):
access_token = create_access_token(identity=user.id, expires_delta=timedelta(seconds=30))
refresh_token = create_refresh_token(identity=user.id, expires_delta=timedelta(minutes=2))
return {'access_token': access_token, 'refresh_token': refresh_token}, 200
return {'msg': 'Invalid credentials'}, 401
class ProtectedResource(Resource):
@jwt_required()
def get(self):
identity = get_jwt_identity()
return {'logged_in_as': identity}, 200
Testing Approach & Results (Postman) Login works (200 OK), returns tokens.
Access protected resource (200 OK) initially with valid token.
Wait 30 seconds (token expiration), then call protected resource again.
Expected: 401 {"message": "Token has expired"}
Actual: 500 Internal Server Error
Server Logs:
ERROR in app: Exception on /api/protected [GET]
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/flask/app.py", line 917, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.11/site-packages/flask/app.py", line 902, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File "/usr/local/lib/python3.11/site-packages/flask_restful/__init__.py", line 604, in dispatch_request
resp = meth(*args, **kwargs)
File "/usr/local/lib/python3.11/site-packages/flask_jwt_extended/view_decorators.py", line 167, in decorator
verify_jwt_in_request(
File "/usr/local/lib/python3.11/site-packages/flask_jwt_extended/utils.py", line 128, in decode_token
return jwt_manager._decode_jwt_from_config(encoded_token, csrf_value, allow_expired)
File "/usr/local/lib/python3.11/site-packages/jwt/api_jwt.py", line 363, in _validate_exp
raise ExpiredSignatureError("Signature has expired")
jwt.exceptions.ExpiredSignatureError: Signature has expired
What I've Tried without success: Implemented JWT global error handlers (expired_token_loader).
Verified correct registration of JWT callbacks.
Simplified the endpoint code to remove internal exception handling (as JWT errors should be handled globally).
Fully rebuilt Docker containers multiple times to ensure fresh deployment.
Questions Why isn't my global expired_token_loader capturing the expired token exceptions from the decorator?
Is there an error in the way I've configured flask_jwt_extended with flask_restful that prevents global handlers from triggering?
What steps can I take to isolate or debug the issue further?
Environment Details Python: 3.11 (Dockerized)
Flask: latest
Flask-JWT-Extended: latest
Flask-Restful: latest
PostgreSQL database (Dockerized)
Docker Compose setup for backend, frontend, and database
Share Improve this question edited Apr 2 at 3:23 Charles Duffy 297k43 gold badges434 silver badges489 bronze badges asked Apr 1 at 12:08 24wolF24wolF 213 bronze badges New contributor 24wolF is a new contributor to this site. Take care in asking for clarification, commenting, and answering. Check out our Code of Conduct. 2- Thanks a lot. Sorry I didn’t have my error logs tabbed out. – 24wolF Commented Apr 1 at 13:04
- To mark your question as solved, use the "add an answer" button to add an answer, separate from the text of the question itself, then (later, when the system allows it) click the checkbox by that answer to accept it. Do not edit answers into questions. – Charles Duffy Commented Apr 2 at 3:23
1 Answer
Reset to default 0Figured out that I had to force handling JWT exceptions globally:
Configured Flask and Flask-RESTful to propagate JWT exceptions correctly by adding the following code to init.py:
app.config['PROPAGATE_EXCEPTIONS'] = True # Propagate exceptions to the client
api.handle_errors = False # Disable Flask-RESTful
This provided the results I was looking for and I successfully tested the JWT lifecycle:
Login: Issued JWT tokens via /api/login.
Valid Token: Accessed protected resource successfully.
Expired Token: Received expected 401 error ("Token has expired").
Token Refresh: Successfully refreshed JWT token via /api/refresh.
New Token: Validated new token with protected endpoint access.
Sources for the developed solution:
https://github/vimalloc/flask-jwt-extended/issues/308
https://github/vimalloc/flask-jwt-extended/issues/86
https://github/vimalloc/flask-jwt-extended/issues/83
https://github/vimalloc/flask-jwt-extended/blob/main/flask_jwt_extended/jwt_manager.py#L81