I'd like to handle errors using AioHttp and Connexion in my python web apis in the same way Flask does through #app.errorhandler(Exception)
In another words, let's say my services raises SomethingAlreadyExists and I want to return 409 Conflict, rather than add the code below in all my apis:
try:
myservice.create_something(..)
except SomethingAlreadyExists as error: # Repeated code -> DRY
return json_response({"message": str(error)}, status=409)
I'd like to just call the myservice.create_something(..) in the API layer and the error handle would return the 409 for SomethingAlreadyExists exceptions or 404 for SomethingNotFound.
Note:
In Flask land it would be something like below:
import connexion
def create_api_app(version='api'):
connexion_app = connexion.FlaskApp(__name__, specification_dir='../api/')
connexion_app.add_api('openapi.yaml', validate_responses=True)
app = connexion_app.app
# It intercepts the specific exception and returns the respective status_code
#app.errorhandler(InvalidValueException)
def bad_request_handler(error):
return 'Bad Request: {}'.format(str(error)), 400
#app.errorhandler(NotFoundException)
def not_found_handler(error):
return 'Not found: {}'.format(str(error)), 404
#app.errorhandler(AlreadyExistsException)
def conflict_handler(error):
return 'Conflict: {}'.format(str(error)), 409
# my_service.py
def get_model(i):
model = get_model_or_none(id)
if btask is None:
raise NotFoundException(f"Model id:{id} not found.")
...
# api.py
def get_model(id):
model = my_service.get_model(id)
# handle errors not required ;)
return btask.to_dict()
I'd like to do the same in my AioHttp connexion app:
from connexion import AioHttpApp
def create_app():
connexion_app = AioHttpApp(__name__, port=8000, specification_dir="../", only_one_api=True)
connexion_app.add_api("openapi.yaml", pass_context_arg_name="request")
# Do something here.. like
# web.Application(middlewares=[handle_already_exists_errors]) --> doesn't work
# OR
# connex_app.add_error_handler(
# AlreadyExistsException, handle_already_exists_errors) --> doesn't work too
return connexion_app
Cheers and I'll appreciate any help!
Roger
I was digging into the connexion and aiohttp code and figured out a way to do it using middlewares:
import json
from aiohttp.web import middleware
from connexion.lifecycle import ConnexionResponse
from connexion import AioHttpApp
from .exceptions import NotFoundException, AlreadyExistsException
def create_app():
connexion_app = AioHttpApp(
__name__, port=8000, specification_dir="../", only_one_api=True
)
connexion_app.add_api("openapi.yaml", pass_context_arg_name="request")
connexion_app.app.middlewares.append(errors_handler_middleware)
return connexion_app
#middleware
async def errors_handler_middleware(request, handler):
""" Handle standard errors returning response following the connexion style messages"""
try:
response = await handler(request)
return response
except NotFoundException as error:
return json_error(message=str(error), title='Not Found', status_code=404)
except AlreadyExistsException as error:
return json_error(message=str(error), title='Conflict', status_code=409)
def json_error(message, title, status_code):
return ConnexionResponse(
body=json.dumps({
'title': title,
'detail': message,
'status': status_code,
}).encode('utf-8'),
status_code=status_code,
content_type='application/json'
)
Related
I have a custom exception shown below where functions are raising these exceptions.
class UnauthorizedToSendException(HTTPException):
code = 400
description = 'Unauthorized to Send.'
They are then defined in Flask's create_app() as follows:
def handle_custom_exceptions(e):
response = {"error": e.description, "message": ""}
if len(e.args) > 0:
response["message"] = e.args[0]
# Add some logging so that we can monitor different types of errors
app.logger.error(f"{e.description}: {response['message']}")
return jsonify(response), e.code
app.register_error_handler(UnauthorizedToSendException, handle_custom_exceptions)
When this exception is raised below:
class LaptopStatus(Resource):
#staticmethod
def get():
raise UnauthorizedToSendException('you are unauthorized to send')
However, the output is always this way:
{
"message": "you are unauthorized to send"
}
Is there something missing here?
flask_restful.Api has its own error handling implementation that mostly replaces Flask's errorhandler functionality, so using the Flask.errorhandler decorator or Flask.register_error_handler won't work.
There are a couple solutions to this.
Don't Inherit from HTTPException and set PROPAGATE_EXCEPTIONS
The flask-restful error routing has short circuits for handling exceptions that don't inherit from HTTPException, and if you set your Flask app to propagate exceptions, it will send the exception to be handled by normal Flask's handlers.
import flask
from flask import Flask
from flask_restful import Resource, Api
app = Flask(__name__)
# Note that this doesn't inherit from HTTPException
class UnauthorizedToSendException(Exception):
code = 400
description = 'Unauthorized to Send'
#app.errorhandler(UnauthorizedToSendException)
def handle_unauth(e: Exception):
rsp = {"error": e.description, "message": ""}
if len(e.args) > 0:
rsp["message"] = e.args[0]
app.logger.error(f"{e.description}: {rsp['message']}")
return flask.jsonify(rsp), e.code
class LaptopStatus(Resource):
#staticmethod
def get():
raise UnauthorizedToSendException("Not authorized")
api = Api(app)
api.add_resource(LaptopStatus, '/status')
if __name__ == "__main__":
# Setting this is important otherwise your raised
# exception will just generate a regular exception
app.config['PROPAGATE_EXCEPTIONS'] = True
app.run()
Running this I get...
#> python flask-test-propagate.py
# ... blah blah ...
#> curl http://localhost:5000/status
{"error":"Unauthorized to Send","message":"Not authorized"}
Replace flask_restul.Api.error_router
This will override the behavior of the Api class's error_router method, and just use the original handler that Flask would use.
You can see it's just a monkey-patch of the class's method with...
Api.error_router = lambda self, hnd, e: hnd(e)
This will allow you to subclass HTTPException and override flask-restful's behavior.
import flask
from flask import Flask
from flask_restful import Resource, Api
from werkzeug.exceptions import HTTPException
# patch the Api class with a custom error router
# that just use's flask's handler (which is passed in as hnd)
Api.error_router = lambda self, hnd, e: hnd(e)
app = Flask(__name__)
class UnauthorizedToSendException(HTTPException):
code = 400
description = 'Unauthorized to Send'
#app.errorhandler(UnauthorizedToSendException)
def handle_unauth(e: Exception):
print("custom!")
rsp = {"error": e.description, "message": ""}
if len(e.args) > 0:
rsp["message"] = e.args[0]
app.logger.error(f"{e.description}: {rsp['message']}")
return flask.jsonify(rsp), e.code
class LaptopStatus(Resource):
#staticmethod
def get():
raise UnauthorizedToSendException("Not authorized")
api = Api(app)
api.add_resource(LaptopStatus, '/status')
if __name__ == "__main__":
app.run()
Switch to using flask.views.MethodView
Documentation:
flask.views.MethodView
Just thought of this, and it's a more drastic change to your code, but flask has facilities for making building REST APIs easier. flask-restful isn't exactly abandoned, but the pace of changes have slowed down the last several years, and various components like the error handling system have become too inflexible.
If you're using flask-restful specifically for the Resource implementation, you can switch to the MethodView, and then you don't need to do any kind of workarounds.
import flask
from flask import Flask
from flask.views import MethodView
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
class UnauthorizedToSendException(HTTPException):
code = 400
description = 'Unauthorized to Send'
#app.errorhandler(UnauthorizedToSendException)
def handle_unauth(e: Exception):
rsp = {"error": e.description, "message": ""}
if len(e.args) > 0:
rsp["message"] = e.args[0]
app.logger.error(f"{e.description}: {rsp['message']}")
return flask.jsonify(rsp), e.code
class LaptopStatusApi(MethodView):
def get(self):
raise UnauthorizedToSendException("Not authorized")
app.add_url_rule("/status", view_func=LaptopStatusApi.as_view("laptopstatus"))
if __name__ == "__main__":
app.run()
read the https://flask.palletsprojects.com/en/2.1.x/errorhandling/
but: See werkzeug.exception for default HTTPException classes.
For HTTP400 (werkzeug.exception.BadRequest)
So, I have a server running FastAPI which will make a API call to a remote API upon request.
I am developping unit-testing for this application, but here comes the question:
Can I, for the purpose of the test, replace a legit remote API server response by a predefined response ?
Example of the tests runned:
from fastapi.testclient import TestClient
from web_api import app
client = TestClient(app)
def test_get_root():
response = client.get('/')
assert response.status_code == 200
assert response.json() == {"running": True}
And the my server
from fastapi import FastAPI
app = FastAPI()
#app.get("/")
def home():
return {"running": True}
This is a simple example, but on other endpoints of my API I would call an external remote API
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
Because I want to test the response of MY API, I would like to replace the remote API with a predefined response.
Also, one user request can end-up in multiple background API requests with transformed pieces of data.
Edit
Here are some more details on the structure of the application:
#app.get("/stuff/.......",
# lots of params
)
def get_stuff_from_things(stuff:list, params):
api = API(api_key=...)
# Do some stuff with the params
things = generate_things_list(params)
api.search_things(params)
# Check the result
# do some other stuff
return some_response
class API:
BASE_URL = 'https://api.example.com/'
def search_things(self, params):
# Do some stuff
# like putting stuff in the params
for s in stuff:
s.update(self.get_thing(params)) # -> get_thing()
# Do some more stuff
return stuff
# get_thing <- search_things
def get_thing(self, params...):
# Some stuff
results = self.call_api('something', params) # -> call_api()
json = results.json()
# Some more stuff
things = []
for thing in json['things']:
t = Thing(thing)
things.append(t)
return things
# call_api <- get_thing
def call_api(self, endpoint:str, params:dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
self.last_response = response
return response
Nb. That is pseudo-code, I simplified the functions by removing the parameters, etc.
I hope it is clear, thanks for your help.
A complex API method might look like this (please pay attention to the depends mechanism - it is crucial):
import urllib
import requests
from fastapi import FastAPI, Depends
app = FastAPI()
# this can be in a different file
class RemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
url = baseurl + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response
#app.get("/complex_api")
def calls_other_api(remote_call_wrapper=Depends(RemoteCallWrapper)):
response = remote_call_wrapper.call_api("https://jsonplaceholder.typicode.com",
"/todos/1", None)
return {"result": response.json()}
Now, we wish to replace the remote call class. I wrote a helper library that simplifies the replacement for tests - pytest-fastapi-deps:
from fastapi.testclient import TestClient
from mock.mock import Mock
from requests import Response
from web_api import app, RemoteCallWrapper
client = TestClient(app)
class MyRemoteCallWrapper:
def call_api(self, baseurl: str, endpoint: str, params: dict):
the_response = Mock(spec=Response)
the_response.json.return_value = {"my": "response"}
return the_response
def test_get_root(fastapi_dep):
with fastapi_dep(app).override({RemoteCallWrapper: MyRemoteCallWrapper}):
response = client.get('/complex_api')
assert response.status_code == 200
assert response.json() == {"result": {"my": "response"}}
You override the RemoteCallWrapper with your MyRemoteCallWrapper implementation for the test, which has the same spec.
As asserted - the response changed to our predefined response.
It sounds like you'd want to mock your call_api() function.
With a small modification to call_api() (returning the result of .json()), you can easily mock the whole function while calling the endpoint in your tests.
I'll use two files, app.py and test_app.py, to demonstrate how I would do this:
# app.py
import requests
import urllib
from fastapi import FastAPI
app = FastAPI()
def call_api(self, endpoint: str, params: dict):
url = self.BASEURL + urllib.parse.quote(endpoint)
try:
response = requests.get(url, params=params)
response.raise_for_status()
except requests.exceptions.HTTPError as error:
print(error)
return response.json() # <-- This is the only change. Makes it easier to test things.
#app.get("/")
def home():
return {"running": True}
#app.get("/call-api")
def make_call_to_external_api():
# `endpoint` and `params` could be anything here and could be different
# depending on the query parameters when calling this endpoint.
response = call_api(endpoint="something", params={})
# Do something with the response...
result = response["some_parameter"]
return result
# test_app.py
from unittest import mock
from fastapi import status
from fastapi.testclient import TestClient
import app as app_module
from app import app
def test_call_api_endpoint():
test_response = {
"some_parameter": "some_value",
"another_parameter": "another_value",
}
# The line below will "replace" the result of `call_api()` with whatever
# is given in `return_value`. The original function is never executed.
with mock.patch.object(app_module, "call_api", return_value=test_response) as mock_call:
with TestClient(app) as client:
res = client.get("/call-api")
assert res.status_code == status.HTTP_200_OK
assert res.json() == "some_value"
# Make sure the function has been called with the right parameters.
# This could be dynamic based on how the endpoint has been called.
mock_call.assert_called_once_with(endpoint="something", params={})
If app.py and test_app.py are in the same directory you can run the tests simply by running pytest inside that directory.
I make FastAPI application I face to structural problem.
For example, let's say there is exist this simple application
from fastapi import FastAPI, Header
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class PydanticSchema(BaseModel):
test_int: int = Field(..., ge=0)
class CustomException(Exception):
def __init__(self, status_code, msg):
self.status_code = status_code
self.msg = msg
#app.exception_handler(CustomException)
async def handle_custom_exception(request, exc: CustomException):
return JSONResponse(
status_code=exc.status_code, content={"error": exc.msg}
)
#app.post("/")
def index(
some_form: PydanticSchema,
some_header: str = Header("", alias="X-Header", max_length=3),
):
is_special_case = some_form.test_int == 42
if is_special_case:
raise CustomException(418, "Very special case")
return "ok"
And here is the problem.
I have THREE independent producers of errors: My manual raise, Pydantic Model, FastApi.
if __name__ == "__main__":
client = TestClient(app)
cases = (
({"X-Header": "abc"}, {"test_int": 0}),
({"X-Header": "abcdef"}, {"test_int": 0}),
({"X-Header": "abc"}, {"test_int": -1}),
({"X-Header": "abc"}, {"test_int": 42}),
)
for case in cases:
r = client.post("/", headers=case[0], json=case[1])
print(f"#####\n{case=}:\n{r.status_code=}\n{r.text=}")
My goal is to have consistent error responses
So I have to add exception handler for my CustomException,
I have to add error_msg_templates to PydanticModel.Config
I have to add incredibly stupid handler for FastApi exception and Pydantic exception to reformat them into my custom response. Becasuse fastapi overrides custom messages from pydantic templates.
Like so:
# ---snip---
from fastapi.exceptions import RequestValidationError
# ---snip---
class PydanticSchema(BaseModel):
test_int: int = Field(..., ge=0)
class Config:
error_msg_templates = {"value_error.number.not_ge": "Custom GE error"}
# ---snip---
#app.exception_handler(RequestValidationError)
async def fastapi_error_handler(request, exc: RequestValidationError):
errors = exc.errors()
error_wrapper = exc.raw_errors[0]
validation_error = error_wrapper.exc
from pydantic import error_wrappers as ew
if isinstance(validation_error, ew.ValidationError):
errors = validation_error.errors()
first_error = errors[0]
msg = first_error.get("msg")
# [!] Demonstration
etype = first_error.get("type")
if etype == "value_error.any_str.max_length":
msg = "Custom MAX length error"
return JSONResponse(status_code=400, content={"error": msg})
# ---snip---
It is unmaintainable! But custom error message is required - I can not rely ond default pydantic\fastapi error response schemas.
Does anyone one faced to this problem and have solved it gracefully?
Why not FastAPI's Exception_handler argument? You can view the status code of the response when this error occurs, then add an exception handler. Exception handlers have access to the Request & Exception objects, which are the request that caused the exception, and the exception raised, respectively.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
def val_err(request: Request, exception: Exception):
if not isinstance(exception, RequestValidationError):
return
errors = exc.errors()
error_wrapper = exc.raw_errors[0]
validation_error = error_wrapper.exc
from pydantic import error_wrappers as ew
if isinstance(validation_error, ew.ValidationError):
errors = validation_error.errors()
first_error = errors[0]
msg = first_error.get("msg")
# [!] Demonstration
etype = first_error.get("type")
if etype == "value_error.any_str.max_length":
msg = "Custom MAX length error"
return JSONResponse(status_code=400, content={"error": msg})
exception_handlers = {422: val_err}
app = FastAPI(exception_handlers=exception_handlers)
You can refer to my other answer which dissects the code and explains it in a bit more detail.
Is it possible to use an after_request for every route in my api?
My main goal would be to do something like this:
#blueprint.route('/endpoint', methods=['GET'])
#after_request_handler(valid_status_codes=[200, 404])
def endpoint():
return {"hello": "world"}, 200
Then in the after_request_handler I would check if the status_code of the response I receive is in the valid_status_codes list I passed as argument and therefore know if the request was successful or not.
Something like this:
def after_request_handler(valid_status_codes, response):
if response.status_code in valid_status_codes:
# Send request was successful metric event
else:
# Send request was unsuccessful metric event
return response
With this I want to have metrics of every endpoint in my api and know if they start to fail for some reason. Maybe there is some library or an easier way to do this. Thanks.
just use errorhandler + after_request:
from flask import Flask
from werkzeug.exceptions import HTTPException
app = Flask(__name__)
#app.route('/good')
def good():
return 'ok'
#app.route('/bad')
def bad():
raise Exception('bad route')
#app.errorhandler(Exception)
def error(e):
# do here all what you need with errors
if isinstance(e, HTTPException):
print('response code %s' % e.code)
else:
print('undefined error %s' % str(e))
raise e
#app.after_request
def log(response):
# do here all what you need with success responses
print('good response %s' % response.status_code)
return response
run server and open /, /bad, /good:
response code 404
undefined error bad route
good response 200
I have created a simple flask app that and I'm reading the response from python as:
response = requests.post(url,data=json.dumps(data), headers=headers )
data = json.loads(response.text)
Now my issue is that under certain conditions I want to return a 400 or 500 message response. So far I'm doing it like this:
abort(400, 'Record not found')
#or
abort(500, 'Some error...')
This does print the message on the terminal:
But in the API response I kept getting a 500 error response:
The structure of the code is as follows:
|--my_app
|--server.py
|--main.py
|--swagger.yml
Where server.py has this code:
from flask import render_template
import connexion
# Create the application instance
app = connexion.App(__name__, specification_dir="./")
# read the swagger.yml file to configure the endpoints
app.add_api("swagger.yml")
# Create a URL route in our application for "/"
#app.route("/")
def home():
"""
This function just responds to the browser URL
localhost:5000/
:return: the rendered template "home.html"
"""
return render_template("home.html")
if __name__ == "__main__":
app.run(host="0.0.0.0", port="33")
And main.py has all the function I'm using for the API endpoints.
E.G:
def my_funct():
abort(400, 'Record not found')
When my_funct is called, I get the Record not found printed on the terminal, but not in the response from the API itself, where I always get the 500 message error.
You have a variety of options:
The most basic:
#app.route('/')
def index():
return "Record not found", 400
If you want to access the headers, you can grab the response object:
#app.route('/')
def index():
resp = make_response("Record not found", 400)
resp.headers['X-Something'] = 'A value'
return resp
Or you can make it more explicit, and not just return a number, but return a status code object
from flask_api import status
#app.route('/')
def index():
return "Record not found", status.HTTP_400_BAD_REQUEST
Further reading:
You can read more about the first two here: About Responses (Flask quickstart)
And the third here: Status codes (Flask API Guide)
I like to use the flask.Response class:
from flask import Response
#app.route("/")
def index():
return Response(
"The response body goes here",
status=400,
)
flask.abort is a wrapper around werkzeug.exceptions.abort which is really just a helper method to make it easier to raise HTTP exceptions. That's fine in most cases, but for restful APIs, I think it may be better to be explicit with return responses.
Here's some snippets from a Flask app I wrote years ago. It has an example of a 400 response
import werkzeug
from flask import Flask, Response, json
from flask_restplus import reqparse, Api, Resource, abort
from flask_restful import request
from flask_cors import CORS
app = Flask(__name__)
CORS(app)
api = Api(app)
parser = reqparse.RequestParser()
parser.add_argument('address_to_score', type=werkzeug.datastructures.FileStorage, location='files')
class MissingColumnException(Exception):
pass
class InvalidDateFormatException(Exception):
pass
#api.route('/project')
class Project(Resource):
#api.expect(parser)
#api.response(200, 'Success')
#api.response(400, 'Validation Error')
def post(self):
"""
Takes in an excel file of addresses and outputs a JSON with scores and rankings.
"""
try:
df, input_trees, needed_zones = data.parse_incoming_file(request)
except MissingColumnException as e:
abort(400, 'Excel File Missing Mandatory Column(s):', columns=str(e))
except Exception as e:
abort(400, str(e))
project_trees = data.load_needed_trees(needed_zones, settings['directories']['current_tree_folder'])
df = data.multiprocess_query(df, input_trees, project_trees)
df = data.score_locations(df)
df = data.rank_locations(df)
df = data.replace_null(df)
output_file = df.to_dict('index')
resp = Response(json.dumps(output_file), mimetype='application/json')
resp.status_code = 200
return resp
#api.route('/project/health')
class ProjectHealth(Resource):
#api.response(200, 'Success')
def get(self):
"""
Returns the status of the server if it's still running.
"""
resp = Response(json.dumps('OK'), mimetype='application/json')
resp.status_code = 200
return resp
You can return a tuple with the second element being the status (either 400 or 500).
from flask import Flask
app = Flask(__name__)
#app.route('/')
def hello():
return "Record not found", 400
if __name__ == '__main__':
app.run()
Example of calling the API from python:
import requests
response = requests.get('http://127.0.0.1:5000/')
response.text
# 'This is a bad request!'
response.status_code
# 400
I think you're using the abort() function correctly. I suspect the issue here is that an error handler is that is catching the 400 error and then erroring out which causes the 500 error. See here for more info on flask error handling.
As an example, the following would change a 400 into a 500 error:
#app.errorhandler(400)
def handle_400_error(e):
raise Exception("Unhandled Exception")
If you're not doing any error handling, it could be coming from the connexion framework, although I'm not familiar with this framework.
You can simply use #app.errorhandler decorator.
example:
#app.errorhandler(400)
def your_function():
return 'your custom text', 400