Handling Application Errors
Applications fail, servers fail. Sooner or later you will see an exception
in production. Even if your code is 100% correct, you will still see
exceptions from time to time. Why? Because everything else involved will
fail. Here are some situations where perfectly fine code can lead to server
errors:
- the client terminated the request early and the application was still
reading from the incoming data - the database server was overloaded and could not handle the query
- a filesystem is full
- a harddrive crashed
- a backend server overloaded
- a programming error in a library you are using
- network connection of the server to another system failed
And that’s just a small sample of issues you could be facing. So how do we
deal with that sort of problem? By default if your application runs in
production mode, and an exception is raised Flask will display a very simple
page for you and log the exception to the :attr:`~flask.Flask.logger`.
But there is more you can do, and we will cover some better setups to deal
with errors including custom exceptions and 3rd party tools.
Error Logging Tools
Sending error mails, even if just for critical ones, can become
overwhelming if enough users are hitting the error and log files are
typically never looked at. This is why we recommend using Sentry for dealing with application errors. It’s
available as a source-available project on GitHub and is also available as a hosted version which you can try for free. Sentry
aggregates duplicate errors, captures the full stack trace and local
variables for debugging, and sends you mails based on new errors or
frequency thresholds.
To use Sentry you need to install the sentry-sdk
client with extra
flask
dependencies.
$ pip install sentry-sdk[flask]
And then add this to your Flask app:
import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()])
The YOUR_DSN_HERE
value needs to be replaced with the DSN value you
get from your Sentry installation.
After installation, failures leading to an Internal Server Error
are automatically reported to Sentry and from there you can
receive error notifications.
See also:
- Sentry also supports catching errors from a worker queue
(RQ, Celery, etc.) in a similar fashion. See the Python SDK docs for more information. - Getting started with Sentry
- Flask-specific documentation
Error Handlers
When an error occurs in Flask, an appropriate HTTP status code will be
returned. 400-499 indicate errors with the client’s request data, or
about the data requested. 500-599 indicate errors with the server or
application itself.
You might want to show custom error pages to the user when an error occurs.
This can be done by registering error handlers.
An error handler is a function that returns a response when a type of error is
raised, similar to how a view is a function that returns a response when a
request URL is matched. It is passed the instance of the error being handled,
which is most likely a :exc:`~werkzeug.exceptions.HTTPException`.
The status code of the response will not be set to the handler’s code. Make
sure to provide the appropriate HTTP status code when returning a response from
a handler.
Registering
Register handlers by decorating a function with
:meth:`~flask.Flask.errorhandler`. Or use
:meth:`~flask.Flask.register_error_handler` to register the function later.
Remember to set the error code when returning the response.
@app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): return 'bad request!', 400 # or, without the decorator app.register_error_handler(400, handle_bad_request)
:exc:`werkzeug.exceptions.HTTPException` subclasses like
:exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable
when registering handlers. (BadRequest.code == 400
)
Non-standard HTTP codes cannot be registered by code because they are not known
by Werkzeug. Instead, define a subclass of
:class:`~werkzeug.exceptions.HTTPException` with the appropriate code and
register and raise that exception class.
class InsufficientStorage(werkzeug.exceptions.HTTPException): code = 507 description = 'Not enough storage space.' app.register_error_handler(InsufficientStorage, handle_507) raise InsufficientStorage()
Handlers can be registered for any exception class, not just
:exc:`~werkzeug.exceptions.HTTPException` subclasses or HTTP status
codes. Handlers can be registered for a specific class, or for all subclasses
of a parent class.
Handling
When building a Flask application you will run into exceptions. If some part
of your code breaks while handling a request (and you have no error handlers
registered), a «500 Internal Server Error»
(:exc:`~werkzeug.exceptions.InternalServerError`) will be returned by default.
Similarly, «404 Not Found»
(:exc:`~werkzeug.exceptions.NotFound`) error will occur if a request is sent to an unregistered route.
If a route receives an unallowed request method, a «405 Method Not Allowed»
(:exc:`~werkzeug.exceptions.MethodNotAllowed`) will be raised. These are all
subclasses of :class:`~werkzeug.exceptions.HTTPException` and are provided by
default in Flask.
Flask gives you the ability to raise any HTTP exception registered by
Werkzeug. However, the default HTTP exceptions return simple exception
pages. You might want to show custom error pages to the user when an error occurs.
This can be done by registering error handlers.
When Flask catches an exception while handling a request, it is first looked up by code.
If no handler is registered for the code, Flask looks up the error by its class hierarchy; the most specific handler is chosen.
If no handler is registered, :class:`~werkzeug.exceptions.HTTPException` subclasses show a
generic message about their code, while other exceptions are converted to a
generic «500 Internal Server Error».
For example, if an instance of :exc:`ConnectionRefusedError` is raised,
and a handler is registered for :exc:`ConnectionError` and
:exc:`ConnectionRefusedError`, the more specific :exc:`ConnectionRefusedError`
handler is called with the exception instance to generate the response.
Handlers registered on the blueprint take precedence over those registered
globally on the application, assuming a blueprint is handling the request that
raises the exception. However, the blueprint cannot handle 404 routing errors
because the 404 occurs at the routing level before the blueprint can be
determined.
Generic Exception Handlers
It is possible to register error handlers for very generic base classes
such as HTTPException
or even Exception
. However, be aware that
these will catch more than you might expect.
For example, an error handler for HTTPException
might be useful for turning
the default HTML errors pages into JSON. However, this
handler will trigger for things you don’t cause directly, such as 404
and 405 errors during routing. Be sure to craft your handler carefully
so you don’t lose information about the HTTP error.
from flask import json from werkzeug.exceptions import HTTPException @app.errorhandler(HTTPException) def handle_exception(e): """Return JSON instead of HTML for HTTP errors.""" # start with the correct headers and status code from the error response = e.get_response() # replace the body with JSON response.data = json.dumps({ "code": e.code, "name": e.name, "description": e.description, }) response.content_type = "application/json" return response
An error handler for Exception
might seem useful for changing how
all errors, even unhandled ones, are presented to the user. However,
this is similar to doing except Exception:
in Python, it will
capture all otherwise unhandled errors, including all HTTP status
codes.
In most cases it will be safer to register handlers for more
specific exceptions. Since HTTPException
instances are valid WSGI
responses, you could also pass them through directly.
from werkzeug.exceptions import HTTPException @app.errorhandler(Exception) def handle_exception(e): # pass through HTTP errors if isinstance(e, HTTPException): return e # now you're handling non-HTTP exceptions only return render_template("500_generic.html", e=e), 500
Error handlers still respect the exception class hierarchy. If you
register handlers for both HTTPException
and Exception
, the
Exception
handler will not handle HTTPException
subclasses
because it the HTTPException
handler is more specific.
Unhandled Exceptions
When there is no error handler registered for an exception, a 500
Internal Server Error will be returned instead. See
:meth:`flask.Flask.handle_exception` for information about this
behavior.
If there is an error handler registered for InternalServerError
,
this will be invoked. As of Flask 1.1.0, this error handler will always
be passed an instance of InternalServerError
, not the original
unhandled error.
The original error is available as e.original_exception
.
An error handler for «500 Internal Server Error» will be passed uncaught
exceptions in addition to explicit 500 errors. In debug mode, a handler
for «500 Internal Server Error» will not be used. Instead, the
interactive debugger will be shown.
Custom Error Pages
Sometimes when building a Flask application, you might want to raise a
:exc:`~werkzeug.exceptions.HTTPException` to signal to the user that
something is wrong with the request. Fortunately, Flask comes with a handy
:func:`~flask.abort` function that aborts a request with a HTTP error from
werkzeug as desired. It will also provide a plain black and white error page
for you with a basic description, but nothing fancy.
Depending on the error code it is less or more likely for the user to
actually see such an error.
Consider the code below, we might have a user profile route, and if the user
fails to pass a username we can raise a «400 Bad Request». If the user passes a
username and we can’t find it, we raise a «404 Not Found».
from flask import abort, render_template, request # a username needs to be supplied in the query args # a successful request would be like /profile?username=jack @app.route("/profile") def user_profile(): username = request.arg.get("username") # if a username isn't supplied in the request, return a 400 bad request if username is None: abort(400) user = get_user(username=username) # if a user can't be found by their username, return 404 not found if user is None: abort(404) return render_template("profile.html", user=user)
Here is another example implementation for a «404 Page Not Found» exception:
from flask import render_template @app.errorhandler(404) def page_not_found(e): # note that we set the 404 status explicitly return render_template('404.html'), 404
When using :doc:`/patterns/appfactories`:
from flask import Flask, render_template def page_not_found(e): return render_template('404.html'), 404 def create_app(config_filename): app = Flask(__name__) app.register_error_handler(404, page_not_found) return app
An example template might be this:
{% extends "layout.html" %} {% block title %}Page Not Found{% endblock %} {% block body %} <h1>Page Not Found</h1> <p>What you were looking for is just not there. <p><a href="{{ url_for('index') }}">go somewhere nice</a> {% endblock %}
Further Examples
The above examples wouldn’t actually be an improvement on the default
exception pages. We can create a custom 500.html template like this:
{% extends "layout.html" %} {% block title %}Internal Server Error{% endblock %} {% block body %} <h1>Internal Server Error</h1> <p>Oops... we seem to have made a mistake, sorry!</p> <p><a href="{{ url_for('index') }}">Go somewhere nice instead</a> {% endblock %}
It can be implemented by rendering the template on «500 Internal Server Error»:
from flask import render_template @app.errorhandler(500) def internal_server_error(e): # note that we set the 500 status explicitly return render_template('500.html'), 500
When using :doc:`/patterns/appfactories`:
from flask import Flask, render_template def internal_server_error(e): return render_template('500.html'), 500 def create_app(): app = Flask(__name__) app.register_error_handler(500, internal_server_error) return app
When using :doc:`/blueprints`:
from flask import Blueprint blog = Blueprint('blog', __name__) # as a decorator @blog.errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500 # or with register_error_handler blog.register_error_handler(500, internal_server_error)
Blueprint Error Handlers
In :doc:`/blueprints`, most error handlers will work as expected.
However, there is a caveat concerning handlers for 404 and 405
exceptions. These error handlers are only invoked from an appropriate
raise
statement or a call to abort
in another of the blueprint’s
view functions; they are not invoked by, e.g., an invalid URL access.
This is because the blueprint does not «own» a certain URL space, so
the application instance has no way of knowing which blueprint error
handler it should run if given an invalid URL. If you would like to
execute different handling strategies for these errors based on URL
prefixes, they may be defined at the application level using the
request
proxy object.
from flask import jsonify, render_template # at the application level # not the blueprint level @app.errorhandler(404) def page_not_found(e): # if a request is in our blog URL space if request.path.startswith('/blog/'): # we return a custom blog 404 page return render_template("blog/404.html"), 404 else: # otherwise we return our generic site-wide 404 page return render_template("404.html"), 404 @app.errorhandler(405) def method_not_allowed(e): # if a request has the wrong method to our API if request.path.startswith('/api/'): # we return a json saying so return jsonify(message="Method Not Allowed"), 405 else: # otherwise we return a generic site-wide 405 page return render_template("405.html"), 405
Returning API Errors as JSON
When building APIs in Flask, some developers realise that the built-in
exceptions are not expressive enough for APIs and that the content type of
:mimetype:`text/html` they are emitting is not very useful for API consumers.
Using the same techniques as above and :func:`~flask.json.jsonify` we can return JSON
responses to API errors. :func:`~flask.abort` is called
with a description
parameter. The error handler will
use that as the JSON error message, and set the status code to 404.
from flask import abort, jsonify @app.errorhandler(404) def resource_not_found(e): return jsonify(error=str(e)), 404 @app.route("/cheese") def get_one_cheese(): resource = get_resource() if resource is None: abort(404, description="Resource not found") return jsonify(resource)
We can also create custom exception classes. For instance, we can
introduce a new custom exception for an API that can take a proper human readable message,
a status code for the error and some optional payload to give more context
for the error.
This is a simple example:
from flask import jsonify, request class InvalidAPIUsage(Exception): status_code = 400 def __init__(self, message, status_code=None, payload=None): super().__init__() self.message = message if status_code is not None: self.status_code = status_code self.payload = payload def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message return rv @app.errorhandler(InvalidAPIUsage) def invalid_api_usage(e): return jsonify(e.to_dict()), e.status_code # an API app route for getting user information # a correct request might be /api/user?user_id=420 @app.route("/api/user") def user_api(user_id): user_id = request.arg.get("user_id") if not user_id: raise InvalidAPIUsage("No user id provided!") user = get_user(user_id=user_id) if not user: raise InvalidAPIUsage("No such user!", status_code=404) return jsonify(user.to_dict())
A view can now raise that exception with an error message. Additionally
some extra payload can be provided as a dictionary through the payload
parameter.
Logging
See :doc:`/logging` for information about how to log exceptions, such as
by emailing them to admins.
Debugging
See :doc:`/debugging` for information about how to debug errors in
development and production.
Applications fail, servers fail. Sooner or later you will see an exception
in production. Even if your code is 100% correct, you will still see
exceptions from time to time. Why? Because everything else involved will
fail. Here are some situations where perfectly fine code can lead to server
errors:
-
the client terminated the request early and the application was still
reading from the incoming data -
the database server was overloaded and could not handle the query
-
a filesystem is full
-
a harddrive crashed
-
a backend server overloaded
-
a programming error in a library you are using
-
network connection of the server to another system failed
And that’s just a small sample of issues you could be facing. So how do we
deal with that sort of problem? By default if your application runs in
production mode, and an exception is raised Flask will display a very simple
page for you and log the exception to the logger
.
But there is more you can do, and we will cover some better setups to deal
with errors including custom exceptions and 3rd party tools.
Error Logging Tools¶
Sending error mails, even if just for critical ones, can become
overwhelming if enough users are hitting the error and log files are
typically never looked at. This is why we recommend using Sentry for dealing with application errors. It’s
available as a source-available project on GitHub and is also available as a hosted version which you can try for free. Sentry
aggregates duplicate errors, captures the full stack trace and local
variables for debugging, and sends you mails based on new errors or
frequency thresholds.
To use Sentry you need to install the sentry-sdk
client with extra
flask
dependencies.
$ pip install sentry-sdk[flask]
And then add this to your Flask app:
import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()])
The YOUR_DSN_HERE
value needs to be replaced with the DSN value you
get from your Sentry installation.
After installation, failures leading to an Internal Server Error
are automatically reported to Sentry and from there you can
receive error notifications.
See also:
-
Sentry also supports catching errors from a worker queue
(RQ, Celery, etc.) in a similar fashion. See the Python SDK docs for more information. -
Getting started with Sentry
-
Flask-specific documentation
Error Handlers¶
When an error occurs in Flask, an appropriate HTTP status code will be
returned. 400-499 indicate errors with the client’s request data, or
about the data requested. 500-599 indicate errors with the server or
application itself.
You might want to show custom error pages to the user when an error occurs.
This can be done by registering error handlers.
An error handler is a function that returns a response when a type of error is
raised, similar to how a view is a function that returns a response when a
request URL is matched. It is passed the instance of the error being handled,
which is most likely a HTTPException
.
The status code of the response will not be set to the handler’s code. Make
sure to provide the appropriate HTTP status code when returning a response from
a handler.
Registering¶
Register handlers by decorating a function with
errorhandler()
. Or use
register_error_handler()
to register the function later.
Remember to set the error code when returning the response.
@app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): return 'bad request!', 400 # or, without the decorator app.register_error_handler(400, handle_bad_request)
werkzeug.exceptions.HTTPException
subclasses like
BadRequest
and their HTTP codes are interchangeable
when registering handlers. (BadRequest.code == 400
)
Non-standard HTTP codes cannot be registered by code because they are not known
by Werkzeug. Instead, define a subclass of
HTTPException
with the appropriate code and
register and raise that exception class.
class InsufficientStorage(werkzeug.exceptions.HTTPException): code = 507 description = 'Not enough storage space.' app.register_error_handler(InsufficientStorage, handle_507) raise InsufficientStorage()
Handlers can be registered for any exception class, not just
HTTPException
subclasses or HTTP status
codes. Handlers can be registered for a specific class, or for all subclasses
of a parent class.
Handling¶
When building a Flask application you will run into exceptions. If some part
of your code breaks while handling a request (and you have no error handlers
registered), a “500 Internal Server Error”
(InternalServerError
) will be returned by default.
Similarly, “404 Not Found”
(NotFound
) error will occur if a request is sent to an unregistered route.
If a route receives an unallowed request method, a “405 Method Not Allowed”
(MethodNotAllowed
) will be raised. These are all
subclasses of HTTPException
and are provided by
default in Flask.
Flask gives you the ability to raise any HTTP exception registered by
Werkzeug. However, the default HTTP exceptions return simple exception
pages. You might want to show custom error pages to the user when an error occurs.
This can be done by registering error handlers.
When Flask catches an exception while handling a request, it is first looked up by code.
If no handler is registered for the code, Flask looks up the error by its class hierarchy; the most specific handler is chosen.
If no handler is registered, HTTPException
subclasses show a
generic message about their code, while other exceptions are converted to a
generic “500 Internal Server Error”.
For example, if an instance of ConnectionRefusedError
is raised,
and a handler is registered for ConnectionError
and
ConnectionRefusedError
, the more specific ConnectionRefusedError
handler is called with the exception instance to generate the response.
Handlers registered on the blueprint take precedence over those registered
globally on the application, assuming a blueprint is handling the request that
raises the exception. However, the blueprint cannot handle 404 routing errors
because the 404 occurs at the routing level before the blueprint can be
determined.
Generic Exception Handlers¶
It is possible to register error handlers for very generic base classes
such as HTTPException
or even Exception
. However, be aware that
these will catch more than you might expect.
For example, an error handler for HTTPException
might be useful for turning
the default HTML errors pages into JSON. However, this
handler will trigger for things you don’t cause directly, such as 404
and 405 errors during routing. Be sure to craft your handler carefully
so you don’t lose information about the HTTP error.
from flask import json from werkzeug.exceptions import HTTPException @app.errorhandler(HTTPException) def handle_exception(e): """Return JSON instead of HTML for HTTP errors.""" # start with the correct headers and status code from the error response = e.get_response() # replace the body with JSON response.data = json.dumps({ "code": e.code, "name": e.name, "description": e.description, }) response.content_type = "application/json" return response
An error handler for Exception
might seem useful for changing how
all errors, even unhandled ones, are presented to the user. However,
this is similar to doing except Exception:
in Python, it will
capture all otherwise unhandled errors, including all HTTP status
codes.
In most cases it will be safer to register handlers for more
specific exceptions. Since HTTPException
instances are valid WSGI
responses, you could also pass them through directly.
from werkzeug.exceptions import HTTPException @app.errorhandler(Exception) def handle_exception(e): # pass through HTTP errors if isinstance(e, HTTPException): return e # now you're handling non-HTTP exceptions only return render_template("500_generic.html", e=e), 500
Error handlers still respect the exception class hierarchy. If you
register handlers for both HTTPException
and Exception
, the
Exception
handler will not handle HTTPException
subclasses
because it the HTTPException
handler is more specific.
Unhandled Exceptions¶
When there is no error handler registered for an exception, a 500
Internal Server Error will be returned instead. See
flask.Flask.handle_exception()
for information about this
behavior.
If there is an error handler registered for InternalServerError
,
this will be invoked. As of Flask 1.1.0, this error handler will always
be passed an instance of InternalServerError
, not the original
unhandled error.
The original error is available as e.original_exception
.
An error handler for “500 Internal Server Error” will be passed uncaught
exceptions in addition to explicit 500 errors. In debug mode, a handler
for “500 Internal Server Error” will not be used. Instead, the
interactive debugger will be shown.
Custom Error Pages¶
Sometimes when building a Flask application, you might want to raise a
HTTPException
to signal to the user that
something is wrong with the request. Fortunately, Flask comes with a handy
abort()
function that aborts a request with a HTTP error from
werkzeug as desired. It will also provide a plain black and white error page
for you with a basic description, but nothing fancy.
Depending on the error code it is less or more likely for the user to
actually see such an error.
Consider the code below, we might have a user profile route, and if the user
fails to pass a username we can raise a “400 Bad Request”. If the user passes a
username and we can’t find it, we raise a “404 Not Found”.
from flask import abort, render_template, request # a username needs to be supplied in the query args # a successful request would be like /profile?username=jack @app.route("/profile") def user_profile(): username = request.arg.get("username") # if a username isn't supplied in the request, return a 400 bad request if username is None: abort(400) user = get_user(username=username) # if a user can't be found by their username, return 404 not found if user is None: abort(404) return render_template("profile.html", user=user)
Here is another example implementation for a “404 Page Not Found” exception:
from flask import render_template @app.errorhandler(404) def page_not_found(e): # note that we set the 404 status explicitly return render_template('404.html'), 404
When using Application Factories:
from flask import Flask, render_template def page_not_found(e): return render_template('404.html'), 404 def create_app(config_filename): app = Flask(__name__) app.register_error_handler(404, page_not_found) return app
An example template might be this:
{% extends "layout.html" %} {% block title %}Page Not Found{% endblock %} {% block body %} <h1>Page Not Found</h1> <p>What you were looking for is just not there. <p><a href="{{ url_for('index') }}">go somewhere nice</a> {% endblock %}
Further Examples¶
The above examples wouldn’t actually be an improvement on the default
exception pages. We can create a custom 500.html template like this:
{% extends "layout.html" %} {% block title %}Internal Server Error{% endblock %} {% block body %} <h1>Internal Server Error</h1> <p>Oops... we seem to have made a mistake, sorry!</p> <p><a href="{{ url_for('index') }}">Go somewhere nice instead</a> {% endblock %}
It can be implemented by rendering the template on “500 Internal Server Error”:
from flask import render_template @app.errorhandler(500) def internal_server_error(e): # note that we set the 500 status explicitly return render_template('500.html'), 500
When using Application Factories:
from flask import Flask, render_template def internal_server_error(e): return render_template('500.html'), 500 def create_app(): app = Flask(__name__) app.register_error_handler(500, internal_server_error) return app
When using Modular Applications with Blueprints:
from flask import Blueprint blog = Blueprint('blog', __name__) # as a decorator @blog.errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500 # or with register_error_handler blog.register_error_handler(500, internal_server_error)
Blueprint Error Handlers¶
In Modular Applications with Blueprints, most error handlers will work as expected.
However, there is a caveat concerning handlers for 404 and 405
exceptions. These error handlers are only invoked from an appropriate
raise
statement or a call to abort
in another of the blueprint’s
view functions; they are not invoked by, e.g., an invalid URL access.
This is because the blueprint does not “own” a certain URL space, so
the application instance has no way of knowing which blueprint error
handler it should run if given an invalid URL. If you would like to
execute different handling strategies for these errors based on URL
prefixes, they may be defined at the application level using the
request
proxy object.
from flask import jsonify, render_template # at the application level # not the blueprint level @app.errorhandler(404) def page_not_found(e): # if a request is in our blog URL space if request.path.startswith('/blog/'): # we return a custom blog 404 page return render_template("blog/404.html"), 404 else: # otherwise we return our generic site-wide 404 page return render_template("404.html"), 404 @app.errorhandler(405) def method_not_allowed(e): # if a request has the wrong method to our API if request.path.startswith('/api/'): # we return a json saying so return jsonify(message="Method Not Allowed"), 405 else: # otherwise we return a generic site-wide 405 page return render_template("405.html"), 405
Returning API Errors as JSON¶
When building APIs in Flask, some developers realise that the built-in
exceptions are not expressive enough for APIs and that the content type of
text/html they are emitting is not very useful for API consumers.
Using the same techniques as above and jsonify()
we can return JSON
responses to API errors. abort()
is called
with a description
parameter. The error handler will
use that as the JSON error message, and set the status code to 404.
from flask import abort, jsonify @app.errorhandler(404) def resource_not_found(e): return jsonify(error=str(e)), 404 @app.route("/cheese") def get_one_cheese(): resource = get_resource() if resource is None: abort(404, description="Resource not found") return jsonify(resource)
We can also create custom exception classes. For instance, we can
introduce a new custom exception for an API that can take a proper human readable message,
a status code for the error and some optional payload to give more context
for the error.
This is a simple example:
from flask import jsonify, request class InvalidAPIUsage(Exception): status_code = 400 def __init__(self, message, status_code=None, payload=None): super().__init__() self.message = message if status_code is not None: self.status_code = status_code self.payload = payload def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message return rv @app.errorhandler(InvalidAPIUsage) def invalid_api_usage(e): return jsonify(e.to_dict()), e.status_code # an API app route for getting user information # a correct request might be /api/user?user_id=420 @app.route("/api/user") def user_api(user_id): user_id = request.arg.get("user_id") if not user_id: raise InvalidAPIUsage("No user id provided!") user = get_user(user_id=user_id) if not user: raise InvalidAPIUsage("No such user!", status_code=404) return jsonify(user.to_dict())
A view can now raise that exception with an error message. Additionally
some extra payload can be provided as a dictionary through the payload
parameter.
Logging¶
See Logging for information about how to log exceptions, such as
by emailing them to admins.
Debugging¶
See Debugging Application Errors for information about how to debug errors in
development and production.
The error handling in APIFlask is based on the following basic concepts:
- All the automatic errors (404, 405, 500) will be in JSON format by default.
- Errors are built on top of the
HTTPError
base exception class. - Use
APIFlask.abort()
function or raiseHTTPError
classes to generate an error response. - Use
app.error_processor
(app
is an instance ofapiflask.APIFlask
) to register a
custom error response processor. - Use
auth.error_processor
(auth
is an instance ofapiflask.HTTPBasicAuth
or
apiflask.HTTPTokenAuth
) to register a custom auth error response processor. - Subclass
HTTPError
to create custom error classes for your errors.
Tip
The error handler registered with app.errorhandler
for specific HTTP errors will be
used over the custom error response processor registered with app.error_processor
.
Automatic JSON error response¶
In Flask, for 400/404/405/500 errors, a default error response will be generated.
The default error response will be in HTML format with a default error message and error
description. However, in APIFlask, these errors will be returned in JSON format with
the following preset fields:
message
: The HTTP reason phrase or a custom error description.detail
: An empty dict (404/405/500) or the error details of the request validation (400).
You can control this behavior with the json_errors
parameter when creating the APIFlask
instance, and it defaults to True
:
from apiflask import APIFlask
# this will disable the automatic JSON error response
app = APIFlask(__name__, json_errors=False)
You can use the app.error_processor
decorator to register a custom error processor
to customize the error response body. See more details
here.
Make an error response with abort
and HTTPError
¶
There are two ways to abort the request handling process and return an error response
in the view function:
- Call the
abort
function
Just like what you do in a normal Flask view function, but this abort
function is
provided by APIFlask:
from apiflask import abort
@app.route('/')
def hello():
abort(400, message='Something is wrong...')
return 'Hello, world!' # this line will never be reached
It will raise an HTTPError
behind the scene, so it will take the same arguments (see below).
- Raise the
HTTPError
class
Raise HTTPError
will do the same thing:
from apiflask import HTTPError
@app.route('/')
def hello():
raise HTTPError(400, message='Something is wrong...')
return 'Hello, world!' # this line will never be reached
The call will generate an error response like this:
{
"message": "Something is wrong...",
"detail": {}
}
See HTTPError
‘s API docs for
all the parameters you can pass to abort
and HTTPError
.
The extra_data
is useful when you want to add more fields to the response body, for example:
abort(
400,
message='Something is wrong...',
extra_data={
'docs': 'http://example.com',
'error_code': 1234
}
)
will produce the below response:
{
"message": "Something is wrong...",
"detail": {},
"docs": "http://example.com",
"error_code": 1234
}
In most cases, you should create custom error classes with preset values instead of passing them to
abort
or HTTPError
directly. See more details in the section below.
Custom error classes¶
To reuse errors, you can create custom error classes with preset error information. The
custom error classes should be inherited from HTTPError
, and you can use the following attributes
in the error class:
- status_code
- message
- detail
- extra_data
- headers
Here is a simple example:
from apiflask import HTTPError
class PetNotFound(HTTPError):
status_code = 404
message = 'This pet is missing.'
extra_data = {
'error_code': '2323',
'error_docs': 'https://example.com/docs/missing'
}
Then you can raise
this exception class in your view function:
@app.get('/pets/<pet_id>')
def get_pet(pet_id):
pets = [1, 2, 3]
if pet_id not in pets:
raise PetNotFound
return {'message': 'Pet'}
Use exception classes from Werkzeug
If you didn’t set the json_errors
to False
when creating app
instance,
APIFlask will catch all the Werkzeug exceptions, including the one you raised
directly:
from werkzeug.exceptions import NotFound
@app.get('/')
def say_hello():
if user is None:
raise NotFound
return {'message': 'Hello!'}
However, the description
and body
of the exception will be discarded.
Custom error status code and description¶
The following configuration variables can be used to customize the validation and
authentication errors:
VALIDATION_ERROR_DESCRIPTION
AUTH_ERROR_DESCRIPTION
VALIDATION_ERROR_STATUS_CODE
AUTH_ERROR_STATUS_CODE
See the Response customization section in the
configuration docs for the details.
Custom error response processor¶
You can use the app.error_processor
decorator to register a custom error response
processor function. It’s a global error processor for all HTTP errors.
The decorated callback function will be called in the following situations:
- Any HTTP exception is raised by Flask when
APIFlask(json_errors=True)
(default). - A validation error happened when parsing a request.
- An exception triggered with
HTTPError
- An exception triggered with
abort
.
You can still register a specific error handler for a specific error code or
exception with the app.errorhandler(code_or_exection)
decorator. In that case,
the return value of the error handler will be used as the response when the
corresponding error or exception happens.
The callback function must accept an error object as an argument and return a valid
response:
from apiflask import APIFlask
app = APIFlask(__name__)
@app.error_processor
def my_error_processor(error):
return {
'status_code': error.status_code,
'message': error.message,
'detail': error.detail
}, error.status_code, error.headers
The error object is an instance of HTTPError
,
so you can get error information via its attributes:
- status_code: If the error is triggered by a validation error, the value will be
422 (default) or the value you passed in configVALIDATION_ERROR_STATUS_CODE
.
If the error is triggered byHTTPError
orabort
, it will be the status code
you passed. Otherwise, it will be the status code set by Werkzueg when
processing the request. - message: The error description for this error, either you passed or grabbed from
Werkzeug. -
detail: The detail of the error. When the validation error happens, it will
be filled automatically in the following structure:"<location>": { "<field_name>": ["<error_message>", ...], "<field_name>": ["<error_message>", ...], ... }, "<location>": { ... }, ...
The value of
location
can bejson
(i.e., request body),query
(i.e., query string) or other values depending on the place where the
validation error happened (it matches the value you passed inapp.input
). -
headers: The value will be
{}
unless you pass it inHTTPError
orabort
. - extra_data: Additional error information passed with
HTTPError
orabort
.
If you want, you can rewrite the whole response body to anything you like:
@app.error_processor
def my_error_processor(error):
body = {
'error_message': error.message,
'error_detail': error.detail,
'status_code': error.status_code
}
return body, error.status_code, error.headers
Tip
I would recommend keeping the error.detail
data in the response since it contains
the detailed information about the validation error when it happened.
After you change the error response, you have to update the corresponding OpenAPI schema
for error responses so the API docs will match your custom error response schema.
Update the OpenAPI schema of error responses¶
There are two error schemas in APIFlask: one for generic errors (including auth errors),
and one for validation errors. They can be configured with HTTP_ERROR_SCHEMA
and
VALIDATION_ERROR_SCHEMA
, respectively.
Why do we need two schemas for error responses?
The reason behind a separate schema for the validation error response is that the detail
field of the validation errors will always have values. While for generic HTTP errors,
the detail
field will be empty unless you passed something with HTTPError
and
abort
.
When you change the error response body with error_processor
, you will also need
to update the error response schema, so it will update the OpenAPI spec of the error
response. The schema can be a dict of OpenAPI schema or a marshmallow schema class.
Here is an example that adds a status_code
field to the default error response
and renames the existing fields (with OpenAPI schema dict):
# use the built-in `validation_error_detail_schema` for the `detail` field
from apiflask import APIFlask
from apiflask.schemas import validation_error_detail_schema
# schema for generic error response, including auth errors
http_error_schema = {
"properties": {
"error_detail": {
"type": "object"
},
"error_message": {
"type": "string"
},
"status_code": {
"type": "integer"
}
},
"type": "object"
}
# schema for validation error response
validation_error_schema = {
"properties": {
"error_detail": validation_error_detail_schema,
"error_message": {
"type": "string"
},
"status_code": {
"type": "integer"
}
},
"type": "object"
}
app = APIFlask(__name__)
app.config['VALIDATION_ERROR_SCHEMA'] = validation_error_schema
app.config['HTTP_ERROR_SCHEMA'] = http_error_schema
Handling authentication errors¶
When you set the json_errors
to True
when creating the APIFlask instance (defaults to True
),
APIFlask will return JSON errors for auth errors and use the built-in errors callback or the
error processor you created with app.error_processor
.
In the following situations, you need to register a separate error processor for auth
errors:
- If you want to make some additional process for 401/403 error, instead of using
app.errorhandler(401)
orapp.errorhandler(403)
to register a specific error
handler, you have to useauth.error_processor
to register an auth error processor. - If you have set
json_errors
toFalse
, but also want to customize the error
response, you also need to register a custom auth error processor since the global
error processor will not be used.
You can use the auth.error_processor
decorator to register an auth error processor. It
works just like app.error_processor
:
from apiflask import HTTPTokenAuth
auth = HTTPTokenAuth()
@auth.error_processor
def my_auth_error_processor(error):
body = {
'error_message': error.message,
'error_detail': error.detail,
'status_code': error.status_code
}
return body, error.status_code, error.headers
If you registered an auth error processor when json_error
is True
, it will overwrite the
global error processor.
Why do we need a separate error processor for auth errors?
APIFlask’s authentication feature is backed with Flask-HTTPAuth. Since Flask-HTTPAuth
uses a separate error handler for its errors, APIFlask has to add a separate
error processor to handle it. We may figure out a simple way for this in the future.
(издание 2018)
Miguel Grinberg
Туда Сюда
Это двадцать третья часть Мега-Учебника, в которой я расскажу вам, как расширить микроблог с помощью интерфейса прикладного программирования (или API), который клиенты могут использовать для работы с приложением более прямым способом, чем традиционный рабочий процесс веб-браузера.
Под спойлером приведен список всех статей серии 2018 года.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Вся функциональность которую я построил до сих пор для этого приложения, предназначена для одного конкретного типа клиента: веб-браузер. Но как насчет других типов клиентов? Например, если бы я хотел создать приложение для Android или iOS, у меня есть два основных способа его решения. Самым простым решением было бы создать приложение с помощью веб-компонента, который заполнит весь экран и загрузит веб-сайт Microblog, но это не будет качественно лучшим по сравнению с открытием приложения в веб-браузере устройства. Лучшим решением (хотя и гораздо более трудоемким) было бы создание собственного приложения, но как это приложение может взаимодействовать с сервером, который возвращает только HTML-страницы?
Это проблемная область, в которой могут помочь Интерфейсы Прикладного Программирования (или API). API-это коллекция HTTP-маршрутов, которые разрабатываются как низкоуровневые точки входа в приложение. Вместо того, чтобы определять маршруты и просматривать функции, возвращающие HTML, которые будут использоваться веб-браузерами, API позволяют клиенту работать непосредственно с ресурсами приложения, оставляя решение о том, как представить информацию пользователю полностью клиенту. Например, API в микроблоге может предоставить клиенту информацию о пользователе и записи в блоге, а также позволить пользователю редактировать существующую запись в блоге, но только на уровне данных, не смешивая эту логику с HTML.
Если вы изучите все маршруты (routes), определенные в настоящее время в приложении, Вы заметите, что есть несколько, которые могут соответствовать определению API, которое я использовал выше. Вы их нашли? Я говорю о нескольких маршрутах, которые возвращают JSON, таких как маршрут /translate, определенный в главе 14. Это маршрут, который принимает текст, исходный и конечный языки, все данные в формате JSON в запросе POST
. Ответом на этот запрос является перевод этого текста, также в формате JSON. Сервер возвращает только запрошенную информацию, оставляя клиента с ответственностью представить эту информацию пользователю.
Хотя маршруты JSON в приложении имеют API-интерфейс, остается «ощущение», что они были разработаны для поддержки веб-приложения, запущенного в браузере. Учтите, что если приложение для смартфонов вдруг захотело использовать эти маршруты, оно не сможет, потому что нужен зарегистрированный пользователь, а вход в систему возможен только через HTML-форму. В этой главе я расскажу, как создавать API-интерфейсы, не полагающиеся на веб-браузер, и не делать никаких предположений о том, какой клиент подключается к ним.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
REST как основа проектирования API
Кто то может категорически не согласиться с моим утверждением выше, что /translate и другие маршруты JSON являются маршрутами API. Другие могут согласиться с оговоркой, что они считают их плохо разработанным API. Итак, каковы характеристики хорошо разработанного API, и почему маршруты JSON вне этой категории?
Возможно, вы слышали термин rest API. REST, который означает Representational State Transfer (Передача Состояния Представления), является архитектурой, предложенной доктором Роем Филдингом в его докторской диссертации. В своей работе д-р Филдинг представляет шесть определяющих характеристик REST в довольно абстрактном и общем виде.
Кроме диссертации доктора Филдинга, нет никакой другой авторитетной спецификации REST, что оставляет много чего для свободной интерпретации читателю. Тема о том, соответствует ли данный API REST или нет, часто является источником жарких дебатов между REST «пуристами», которые считают, что REST API должен соблюдать все шесть характеристик и делать это чётко определенным образом по сравнению с «прагматиками» REST, которые берут идеи, представленные д-ром Филдингом в своей диссертации в качестве руководящих принципов или рекомендаций. Д-р Филдинг сам встал на сторону пуристского лагеря и дал некоторое дополнительное представление о своем видении в блогах и онлайн-комментариях.
Подавляющее большинство API-интерфейсов, реализованных в настоящее время, придерживаются «прагматичной» реализации REST. Это включает в себя большинство API-интерфейсов от «крупных игроков», таких как Facebook, GitHub, Twitter и т.д. Существует очень мало публичных API, которые единодушно считаются чистыми REST, поскольку большинство API-интерфейсов пропускают некоторые детали реализации, которые пуристы считают обязательными. Несмотря на строгие взгляды д-ра Филдинга и других пуристов REST на то, что является или не является REST API, в индустрии программного обеспечения обычно упоминается REST в прагматическом смысле.
Чтобы дать вам представление о том, что находится в диссертации REST, в следующих разделах описываются шесть принципов, перечисленных д-ром Филдингом.
Client-Server
Принцип клиент-сервер довольно прост, так как он просто гласит, что в REST API роли клиента и сервера должны быть четко дифференцированы. На практике это означает, что клиент и сервер находятся в отдельных процессах, которые взаимодействуют через транспорт, который в большинстве случаев является протоколом HTTP по сети TCP.
Layered System
Принцип Layered System (многоуровневой системы) говорит, что когда клиент должен взаимодействовать с сервером, он может быть связан с посредником, а не с фактическим сервером. Идея заключается в том, что для клиента не должно быть абсолютно никакой разницы в том, как он отправляет запросы, если не подключен непосредственно к серверу, на самом деле он может даже не знать, подключен ли он к целевому серверу или нет. Аналогичным образом, этот принцип гласит, что сервер может получать клиентские запросы от посредника, а не непосредственно от клиента, поэтому он никогда не должен предполагать, что другая сторона соединения является клиентом.
Это важная функция REST, поскольку возможность добавления промежуточных узлов позволяет архитекторам приложений разрабатывать большие и сложные сети, которые могут удовлетворить большой объем запросов с помощью балансировщика нагрузки, кэшей, прокси-серверов и т.д.
Cache
Этот принцип расширяет многоуровневую систему, явно указывая, что сервер или посредник может кэшировать ответы на запросы, которые часто поступают для повышения производительности системы. Существует реализация кэша, с которой вы, вероятно, знакомы: один во всех веб-браузерах. Слой кэша веб-браузера часто используется, чтобы избежать необходимости запрашивать одни и те же файлы, такие как изображения, снова и снова.
Для целей API целевой сервер должен указать с помощью элементов управления кэшем, может ли ответ кэшироваться посредниками, когда он возвращается клиенту. Обратите внимание, что поскольку по соображениям безопасности API, развернутые в рабочей среде, должны использовать шифрование, кэширование обычно не выполняется на промежуточном узле, если только этот узел не завершает соединение SSL или не выполняет расшифровку и повторное шифрование.
Code On Demand
Это необязательное требование, указывающее, что сервер может предоставлять исполняемый код в ответах клиенту. Поскольку этот принцип требует соглашения между сервером и клиентом о том, какой исполняемый код может выполнять клиент, это редко используется в API. Вы могли бы подумать, что сервер может вернуть код JavaScript для запуска веб-браузеров, но REST специально не предназначен для клиентов веб-браузера. Например, выполнение JavaScript может привести к усложнению, если клиент является iOS или Android-устройством.
Stateless
Принцип stateless является одним из двух в центре большинства дебатов между пуристами REST и прагматиками. В нем указано, что REST API не должен сохранять любое состояние клиента, которое будет вызвано каждый раз, когда данный клиент отправляет запрос. Это означает, что ни один из механизмов, которые являются обычными в веб-разработке для «запоминания» пользователей при навигации по страницам приложения, не может быть использован. В API без состояния каждый запрос должен включать информацию, которую сервер должен идентифицировать и аутентифицировать клиента и выполнить запрос. Это также означает, что сервер не может хранить данные, относящиеся к клиентскому соединению в базе данных или другой форме хранения.
Если вам интересно, почему REST требует сервер без состояния, то основная причина заключается в том, что серверы без учета состояния (stateless) очень просты в масштабировании, все, что вам нужно сделать, это запустить несколько экземпляров сервера за балансировщиком нагрузки. Если сервер хранит состояние клиента, ситуация становится более сложной, так как вам нужно выяснить, как несколько серверов могут получить доступ и обновить это состояние, или же гарантировать, что данный клиент всегда обрабатывается одним и тем же сервером, что обычно называется липкими сеансами.
Если вы снова рассмотрите маршрут /translate, обсуждаемый в начале главы, вы поймете, что его нельзя считать RESTful, потому что функция вида, связанная с этим маршрутом, полагается на декодер @login_required
из Flask-Login, который, в свою очередь, хранит зарегистрированный в состоянии пользователя в сеансе пользователя Flask.
Uniform Interface
Последний, самый важный, самый обсуждаемый и наиболее неопределенно документированный принцип REST — это единый интерфейс. Д-р Филдинг перечисляет четыре отличительных аспекта единого интерфейса REST: уникальные идентификаторы ресурсов, представления ресурсов, самоописательные сообщения и гипермедиа.
Уникальные идентификаторы ресурсов получаются путем назначения уникального URL-адреса каждому ресурсу. Например, URL-адрес, связанный с данным пользователем, может быть /api/users/<user-id>, где <user-id> — это идентификатор, назначенный пользователю в качестве первичного ключа таблицы базы данных. Это вполне приемлемо реализовано большинством API.
Использование представлений ресурсов означает, что если сервер и клиент обмениваются информацией о ресурсе, они должны использовать согласованный Формат. Для большинства современных API Формат JSON используется для построения представлений ресурсов. API может поддерживать несколько форматов представления ресурсов, и в этом случае параметры согласования содержимого в протоколе HTTP являются механизмом, с помощью которого клиент и сервер могут согласовать формат, который нравится обоим.
Самоописательные сообщения означают, что запросы и ответы, которыми обмениваются клиенты и сервер, должны включать всю информацию, необходимую другой стороне. Типичный пример — это метод запроса HTTP используемый для указания, какую операцию клиент хочет получить от сервера. Запрос GET
указывает, что клиент хочет получить сведения о ресурсе, запрос POST
указывает, что клиент хочет создать новый ресурс, запросы PUT
или PATCH
определяют изменения существующих ресурсов, а запрос DELETE
указывает на удаление ресурса. Целевой ресурс указывается как URL-адрес запроса с дополнительной информацией, представленной в заголовках HTTP, части строки запроса URL-адреса или тела(body) запроса.
Требование hypermedia является наиболее полемичным из множества, и тот, который реализуется немногими API, и те API, которые реализуют его, редко делают так, чтобы удовлетворить пуристов REST. Поскольку все ресурсы в приложении взаимосвязаны, это требует обязательного включения связей в представления ресурсов, чтобы клиенты могли обнаруживать новые ресурсы путем обхода связей, почти так же, как вы обнаруживаете новые страницы в веб-приложении, щелкая ссылки, которые ведут вас от одной страницы к другой. Идея заключается в том, что клиент может войти в API без каких-либо предварительных знаний о ресурсах в нем и узнать о них, просто перейдя по ссылкам hypermedia. Одним из аспектов, которые усложняют выполнение данного требования заключается в том, что в отличие от HTML и XML, Формат json, который обычно используется для представления ресурсов в API не определяет стандартный способ включения ссылок, так что вы вынуждены использовать специальные настраиваемые структуры, или один из предлагаемых расширений JSON, которые пытаются восполнить этот пробел, такие как JSON-API, HAL, JSON-LD или похожие.
Реализация концепции API Blueprint
Чтобы дать вам представление о том, что участвует в разработке API, я собираюсь добавить его в микроблог. Это не будет полный API, я собираюсь реализовать все функции, связанные с пользователями, оставляя реализацию других ресурсов, таких как сообщения в блоге для читателя в качестве упражнения.
Чтобы все было организовано и структурировано в соответствии с концепцией описанной в Главе 15, я собираюсь создать новый проект, который будет содержать все маршруты API. Итак, давайте начнем с создания каталога, в котором будет жить этот проект:
(venv) $ mkdir app/api
Blueprint-овый файл __init __. py
создает объект blueprint, аналогичный другим blueprint-овым приложениям:
app/api/__init__.py:
API blueprint constructor.
from flask import Blueprint
bp = Blueprint('api', __name__)
from app.api import users, errors, tokens
Вы, вероятно, помните, что иногда необходимо переместить импорт на самое дно модуля, чтобы избежать циклических ошибок зависимостей. Это причина, почему app/api/users.py, app/api/errors.py и app/api/tokens.py модули (что мне еще предстоит написать) импортируются после создания проекта.
Основное содержание API будет храниться в модуле app/api/users.py. В следующей таблице перечислены маршруты, которые я собираюсь реализовать:
Каркас модуля с заполнителями для всех этих маршрутов будет такой:
app/api/users.py:
Заполнители ресурсов API пользователя.
from app.api import bp
@bp.route('/users/<int:id>', methods=['GET'])
def get_user(id):
pass
@bp.route('/users', methods=['GET'])
def get_users():
pass
@bp.route('/users/<int:id>/followers', methods=['GET'])
def get_followers(id):
pass
@bp.route('/users/<int:id>/followed', methods=['GET'])
def get_followed(id):
pass
@bp.route('/users', methods=['POST'])
def create_user():
pass
@bp.route('/users/<int:id>', methods=['PUT'])
def update_user(id):
pass
В модуле app/api/errors.py надо бы определить несколько вспомогательных функций, которые имеют дело с ответами на ошибки. Но сейчас, я создам заполнитель, который заполню позже:
app/api/errors.py:
Заполнитель обработки ошибок.
def bad_request():
pass
app/api/tokens.py модуль, в котором будет определена подсистема аутентификации. Это обеспечит альтернативный способ входа для клиентов, которые не являются веб-браузерами. Напишем заполнитель и для этого модуля:
app/api/tokens.py:
Обработки маркеров.
def get_token():
pass
def revoke_token():
pass
Новая схема элементов API Blueprint должна быть зарегистрирована в функции фабрики приложений:
app/__init__.py:
Зарегистрируйте схему элементов API в приложении.
# ...
def create_app(config_class=Config):
app = Flask(__name__)
# ...
from app.api import bp as api_bp
app.register_blueprint(api_bp, url_prefix='/api')
# ...
Представление пользователей в виде объектов JSON
Первый аспект, который следует учитывать при реализации API, — это решить, каким будет представление его ресурсов. Я собираюсь реализовать API, который работает с пользователями, поэтому представление для моих пользовательских ресурсов-это то, что мне нужно решить. После некоторого мозгового штурма, я придумал следующее представление json :
{
"id": 123,
"username": "susan",
"password": "my-password",
"email": "susan@example.com",
"last_seen": "2017-10-20T15:04:27Z",
"about_me": "Hello, my name is Susan!",
"post_count": 7,
"follower_count": 35,
"followed_count": 21,
"_links": {
"self": "/api/users/123",
"followers": "/api/users/123/followers",
"followed": "/api/users/123/followed",
"avatar": "https://www.gravatar.com/avatar/..."
}
}
Многие из полей непосредственно поступают из модели пользовательской базы данных. Поле password
отличается тем, что оно будет использоваться только при регистрации нового пользователя. Как вы помните из главы 5, пользовательские пароли не хранятся в базе данных, а только хэш, поэтому пароль никогда не возвращается. Поле email
также обрабатывается специально, потому что я не хочу раскрывать адреса электронной почты пользователей. Поле электронной почты будет возвращено только тогда, когда пользователи будут запрашивать их собственную запись, но не при получении записей от других пользователей. Поля post_count
, follower_count
и follow_count
являются «виртуальными» полями, которые не существуют в качестве полей в базе данных, но предоставляются клиенту в качестве удобства. Это отличный пример, демонстрирующий, что представление ресурса не обязательно должно соответствовать тому, как фактический ресурс определен на сервере.
Обратите внимание на раздел _links
, который реализует требования hypermedia. Определенные ссылки включают ссылки на текущий ресурс, список пользователей, следующих за этим пользователем, список пользователей, за которыми следует пользователь, и, наконец, ссылку на изображение аватара пользователя. В будущем, если я решу добавить сообщения в этот API, ссылка на список сообщений пользователя также должна быть здесь включена.
Одна из приятных особенностей формата JSON заключается в том, что он всегда переводится как представление в виде словаря или списка Python. Пакет json
из стандартной библиотеки Python заботится о преобразовании структур данных Python в JSON и из него. Поэтому, чтобы сгенерировать эти представления, я собираюсь добавить метод к модели User
, называемый to_dict()
, который возвращает словарь Python:
app/models.py:
Модель пользователя для представления.
from flask import url_for
# ...
class User(UserMixin, db.Model):
# ...
def to_dict(self, include_email=False):
data = {
'id': self.id,
'username': self.username,
'last_seen': self.last_seen.isoformat() + 'Z',
'about_me': self.about_me,
'post_count': self.posts.count(),
'follower_count': self.followers.count(),
'followed_count': self.followed.count(),
'_links': {
'self': url_for('api.get_user', id=self.id),
'followers': url_for('api.get_followers', id=self.id),
'followed': url_for('api.get_followed', id=self.id),
'avatar': self.avatar(128)
}
}
if include_email:
data['email'] = self.email
return data
Этот метод не должен вызывать особых вопросов и быть в основном понятным. Словарь с пользовательским представлением, на котором я остановился, просто генерируется и возвращается. Как я уже упоминал выше, поле email
нуждается в специальной обработке, потому что я хочу включить электронную почту только тогда, когда пользователи запрашивают свои собственные данные. Поэтому я использую флаг include_email
, чтобы определить, включено ли это поле в представление или нет.
Обратите внимание, как генерируется поле last_seen
. Для полей даты и времени я собираюсь использовать Формат ISO 8601, который может генерировать datetime
Python с помощью метода isoformat()
. Но поскольку я использую наивные объекты datetime
, которые являются UTC, но не имеют часового пояса, записанного в их состоянии, мне нужно добавить Z
в конце, что является кодом часового пояса ISO 8601 для UTC.
Пояснение от amkko : В python объекты datetime могут быть «наивными» или «осведомлёнными» (naive/aware) относительно часового пояса.
Наконец, зацените, как я реализовал hipermedia-ссылки. Для трех ссылок, которые указывают на другие маршруты приложений, я использую url_for()
для генерации URL-адресов (которые в настоящее время указывают на функции просмотра замещающих элементов, определенные в app/api/users.py). Ссылка аватара особенная, потому что это URL-адрес Gravatar, внешний для приложения. Для этой ссылки я использую тот же метод avatar()
, который я использовал для рендеринга аватаров на веб-страницах.
Метод to_dict()
преобразует пользовательский объект в представление Python, которое затем будет преобразовано в JSON. Мне также нужно позаботиться об обратном направлении, где клиент передает представление пользователя в запросе, а сервер должен проанализировать его и преобразовать в объект User
. Вот метод from_dict()
, который достигает преобразования из словаря Python в модель:
app/models.py:
Представление модели пользователя.
class User(UserMixin, db.Model):
# ...
def from_dict(self, data, new_user=False):
for field in ['username', 'email', 'about_me']:
if field in data:
setattr(self, field, data[field])
if new_user and 'password' in data:
self.set_password(data['password'])
В этом случае я решил использовать цикл для импорта любого из полей, которые клиент может установить: username
, email
и about_me
. Для каждого поля я проверяю, есть ли значение в аргументе data
, и если есть, я использую setattr()
Python, чтобы установить новое значение в соответствующем атрибуте для объекта.
Поле password
рассматривается как особый случай, поскольку оно не является полем в объекте. Аргумент new_user
определяет, является ли это регистрацией нового пользователя, что означает, что пароль включен. Чтобы установить password
в пользовательской модели, я вызываю метод set_password()
, который создает хэш пароля.
Представление коллекций пользователей
Помимо работы с одиночными представлениями ресурсов, этот API будет нуждаться в представлении для группы пользователей. Это будет Формат, используемый, например, когда клиент запрашивает список пользователей или подписчиков. Вот представление для коллекции пользователей:
{
"items": [
{ ... user resource ... },
{ ... user resource ... },
...
],
"_meta": {
"page": 1,
"per_page": 10,
"total_pages": 20,
"total_items": 195
},
"_links": {
"self": "http://localhost:5000/api/users?page=1",
"next": "http://localhost:5000/api/users?page=2",
"prev": null
}
}
В этом представлении items
-это список пользовательских ресурсов, каждый из которых определен, как описано в предыдущем разделе. Раздел _meta
включает в себя метаданные коллекции, которые клиент может найти полезными при представлении пользователю элементов управления разбиением на страницы. В разделе _links
определяются соответствующие ссылки, включая ссылку на саму коллекцию, а также ссылки на предыдущую и следующую страницы, чтобы помочь клиенту разбить список на страницы.
Создание представления коллекции пользователей сложно из-за логики разбиения на страницы, но логика будет общей для других ресурсов, которые я, возможно, захочу Добавить в этот API в будущем, поэтому я собираюсь реализовать это представление общим способом, который я могу затем применить к другим моделям. Еще в главе 16 я был в аналогичной ситуации с индексами полнотекстового поиска, еще одной функцией, которую я хотел реализовать в общем виде, чтобы ее можно было применить к любым моделям. Решение, которое я использовал, состояло в том, чтобы реализовать класс SearchableMixin
, от которого могут наследовать любые модели, которым нужен полнотекстовый индекс. Я собираюсь использовать ту же идею для этого, так вот новый класс mixin, который я назвал PaginatedAPIMixin
:
app/models.py:
Разбитое на страницы представление класса mixin.
class PaginatedAPIMixin(object):
@staticmethod
def to_collection_dict(query, page, per_page, endpoint, **kwargs):
resources = query.paginate(page, per_page, False)
data = {
'items': [item.to_dict() for item in resources.items],
'_meta': {
'page': page,
'per_page': per_page,
'total_pages': resources.pages,
'total_items': resources.total
},
'_links': {
'self': url_for(endpoint, page=page, per_page=per_page,
**kwargs),
'next': url_for(endpoint, page=page + 1, per_page=per_page,
**kwargs) if resources.has_next else None,
'prev': url_for(endpoint, page=page - 1, per_page=per_page,
**kwargs) if resources.has_prev else None
}
}
return data
Метод to_collection_dict()
создает словарь с представлением пользовательской коллекции, включая разделы items
, _meta
и _links
. Я бы советовал вам внимательно изучить метод, чтобы понять, как он работает. Первые три аргумента-объект запроса Flask-SQLAlchemy, номер страницы и Размер страницы. Эти аргументы определяют, какие элементы будут возвращены. Реализация использует метод paginate()
объекта запроса, чтобы получить стоимость страницы элементов, как я сделал с сообщениями в индексе, исследуйте и профилируйте страницы веб-приложения.
Сложная часть заключается в создании ссылок, которые включают в себя ссылку и ссылки на следующую и предыдущие страницы. Я хотел бы сделать эту функцию обобщенной, поэтому я не мог, например, использовать url_for ('api.get_users', id = id, page = page)
для создания собственной ссылки. Аргументы для url_for()
будут зависеть от конкретной коллекции ресурсов, поэтому я буду полагаться на передачу вызывающего в аргументе конечной точки функции представления, которую нужно отправить url_for()
. И поскольку у многих маршрутов есть аргументы, мне также нужно захватить дополнительные аргументы ключевого слова в kwargs
и передать их url_for()
. Строка аргумента строки page
и per_page
указывается явно, поскольку они управляют разбиением на страницы для всех маршрутов API.
Этот класс mixin должен быть добавлен в модель User
в качестве родительского класса:
app/models.py:
Добавьте PaginatedAPIMixin в модель пользователя.
class User(PaginatedAPIMixin, UserMixin, db.Model):
# ...
В случае с коллекциями мне не понадобится обратное направление, потому что у меня не будет маршрутов, требующих от клиента отправки списков пользователей.
Обработка ошибок
Страницы ошибок, которые я определил в главе 7, подходят только для пользователя, который взаимодействует с приложением, используя веб-браузер. Когда API должен возвращать ошибку, она должен быть «дружественного машине» типа ошибки, то, что клиентское приложение сможет легко интерпретировать. Точно так же я определил представления для своих ресурсов API в JSON, теперь я собираюсь принять решение о представлении сообщений об ошибках API. Вот основная структура, которую я собираюсь использовать:
{
"error": "short error description",
"message": "error message (optional)"
}
В дополнение к полезной нагрузке ошибки я буду использовать коды состояния из протокола HTTP для указания общего класса ошибки. Чтобы помочь мне сгенерировать эти ответы на ошибки, я собираюсь написать функцию error_response()
в app/api/errors.py:
app/api/errors.py:
Ответы об ошибках.
from flask import jsonify
from werkzeug.http import HTTP_STATUS_CODES
def error_response(status_code, message=None):
payload = {'error': HTTP_STATUS_CODES.get(status_code, 'Unknown error')}
if message:
payload['message'] = message
response = jsonify(payload)
response.status_code = status_code
return response
Эта функция использует удобный словарь HTTP_STATUS_CODES
из Werkzeug
(основная зависимость Flask), который предоставляет краткое описательное имя для каждого кода состояния HTTP. Я использую эти имена для поля error
в своих представлениях ошибок, поэтому мне нужно беспокоиться только о числовом коде состояния и необязательном длинном описании. Функция jsonify()
возвращает объект Response
Flask с кодом состояния по умолчанию 200, поэтому после создания ответа я устанавливаю код состояния на правильный для ошибки.
Наиболее распространенной ошибкой, которую API собирается вернуть, будет код 400, который является ошибкой для «плохого запроса». Это-ошибка, которая используется, когда клиент передает запрос, который имеет недопустимые данные в нем. Чтобы сделать эту ошибку еще проще, я добавлю для нее специальную функцию, которая требует только длинного описательного сообщения в качестве аргумента. Это заполнитель bad_request()
, который я добавил ранее:
app/api/errors.py:
Ответы на плохие запросы.
# ...
def bad_request(message):
return error_response(400, message)
Конечные точки пользовательских ресурсов
Поддержка, которая мне нужна для работы с представлениями пользователей JSON, теперь завершена, поэтому я готов начать кодирование конечных точек API.
Получение пользователя
Начнем с запроса на получение одного пользователя, заданного id
:
app/api/users.py:
Возврат пользователя.
from flask import jsonify
from app.models import User
@bp.route('/users/<int:id>', methods=['GET'])
def get_user(id):
return jsonify(User.query.get_or_404(id).to_dict())
Функция view получает идентификатор запрошенного пользователя в качестве динамического аргумента в URL-адресе. Метод get_or_404()
объекта запроса является очень полезным вариантом метода get()
, который вы видели ранее, который также возвращает объект с заданным идентификатором, если он существует, но вместо того, чтобы возвращать None
, когда id
не существует, он прерывает запрос и возвращает ошибку 404 клиенту. Преимущество get_or_404()
перед get()
заключается в том, что он устраняет необходимость проверять результат запроса, упрощая логику в функциях представления.
Метод to_dict()
, который я добавил к User
, используется для создания словаря с представлением ресурса для выбранного пользователя, а затем функция Flask jsonify()
преобразует этот словарь в формат JSON для возврата клиенту.
Если вы хотите увидеть, как работает этот первый маршрут API, запустите сервер, а затем введите следующий URL-адрес в адресной строке браузера:
http://localhost:5000/api/users/1
Результат должен показать вам первого пользователя, отображенного в формате JSON. Также попробуйте использовать большое значение id
, чтобы увидеть, как метод get_or_404()
объекта запроса SQLAlchemy вызывает ошибку 404 (я позже покажу вам, как расширить обработку ошибок, чтобы эти ошибки также возвращались в формате JSON).
Чтобы протестировать этот новый маршрут, я установлю HTTPie, HTTP-клиент командной строки, написанный на Python, который упрощает отправку запросов API:
(venv) $ pip install httpie
Теперь я могу запросить информацию о пользователе с идентификатором 1 (который, вероятно, ты сам и есть) с помощью следующей команды:
(venv) $ http GET http://localhost:5000/api/users/1
HTTP/1.0 200 OK
Content-Length: 457
Content-Type: application/json
Date: Mon, 27 Nov 2017 20:19:01 GMT
Server: Werkzeug/0.12.2 Python/3.6.3
{
"_links": {
"avatar": "https://www.gravatar.com/avatar/993c...2724?d=identicon&s=128",
"followed": "/api/users/1/followed",
"followers": "/api/users/1/followers",
"self": "/api/users/1"
},
"about_me": "Hello! I'm the author of the Flask Mega-Tutorial.",
"followed_count": 0,
"follower_count": 1,
"id": 1,
"last_seen": "2017-11-26T07:40:52.942865Z",
"post_count": 10,
"username": "miguel"
}
Получение коллекций пользователей
Чтобы вернуть коллекцию всех пользователей, теперь я могу полагаться на метод to_collection_dict()
PaginatedAPIMixin:
app/api/users.py: Возвращает коллекцию всех пользователей.
from flask import request
@bp.route('/users', methods=['GET'])
def get_users():
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
data = User.to_collection_dict(User.query, page, per_page, 'api.get_users')
return jsonify(data)
Для этой реализации я сначала извлекаю page
и per_page
из строки запроса, используя значения по умолчанию 1 и 10 соответственно, если они не определены. per_page
имеет дополнительную логику, которая ограничивает его 100. Предоставление клиентского элемента управления для запроса действительно больших страниц не является хорошей идеей, так как это может вызвать на сервере проблемы с производительностью. Аргументы page
и per_page
затем передаются методу to_collection_query()
вместе с запросом, который в данном случае является просто User.query
-самый универсальный запрос, возвращающий всех пользователей. Последний аргумент-api.get_users
, является именем конечной точки, который мне нужен для трех ссылок что бы использовать их в представлении.
Чтобы проверить эту конечную точку с помощью HTTPie, используйте следующую команду:
(venv) $ http GET http://localhost:5000/api/users
The next two endpoints are the ones that return the follower and followed users. These are fairly similar to the one above:
app/api/users.py: Return followers and followed users.
@bp.route('/users/<int:id>/followers', methods=['GET'])
def get_followers(id):
user = User.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
data = User.to_collection_dict(user.followers, page, per_page,
'api.get_followers', id=id)
return jsonify(data)
@bp.route('/users/<int:id>/followed', methods=['GET'])
def get_followed(id):
user = User.query.get_or_404(id)
page = request.args.get('page', 1, type=int)
per_page = min(request.args.get('per_page', 10, type=int), 100)
data = User.to_collection_dict(user.followed, page, per_page,
'api.get_followed', id=id)
return jsonify(data)
Поскольку эти два маршрута специфичны для пользователя, у них есть динамический аргумент id
. Идентификатор используется для получения пользователя из базы данных, а затем для предоставления user.followers
и user.followed
отношения на основе запросов к to_collection_dict()
, так что, надеюсь, теперь вы можете увидеть, почему затраты дополнительного времени и проектирование этого метода в общем виде действительно окупается. Последние два аргумента to_collection_dict()
— это имя конечной точки и идентификатор, который метод будет принимать в качестве дополнительного аргумента ключевого слова в kwargs
, а затем передавать его в url_for()
при создании раздела ссылок представления.
Как и в предыдущем примере, вы можете использовать эти два маршрута с HTTPie следующим образом:
(venv) $ http GET http://localhost:5000/api/users/1/followers
(venv) $ http GET http://localhost:5000/api/users/1/followed
Я должен отметить, что благодаря hypermedia вам не нужно запоминать эти URL-адреса, поскольку они включены в раздел _links
пользовательского представления.
Регистрация новых пользователей
Запрос POST
на маршрут /users будет использоваться для регистрации новых учетных записей пользователей. Вы можете увидеть реализацию этого маршрута ниже:
app/api/users.py:
Зарегистрируйте нового пользователя.
from flask import url_for
from app import db
from app.api.errors import bad_request
@bp.route('/users', methods=['POST'])
def create_user():
data = request.get_json() or {}
if 'username' not in data or 'email' not in data or 'password' not in data:
return bad_request('must include username, email and password fields')
if User.query.filter_by(username=data['username']).first():
return bad_request('please use a different username')
if User.query.filter_by(email=data['email']).first():
return bad_request('please use a different email address')
user = User()
user.from_dict(data, new_user=True)
db.session.add(user)
db.session.commit()
response = jsonify(user.to_dict())
response.status_code = 201
response.headers['Location'] = url_for('api.get_user', id=user.id)
return response
Этот запрос будет принимать представление пользователя в формате JSON от клиента, предоставленного в теле запроса. Flask предоставляет метод request.get_json()
, чтобы извлечь JSON из запроса и вернуть его в виде структуры Python. Этот метод возвращает None
, если данные JSON не найдены в запросе, поэтому я могу гарантировать, что я всегда получаю словарь, используя запрос request.get_json()
или {}
.
Прежде чем я смогу использовать данные, мне нужно убедиться, что у меня есть вся информация, поэтому я начинаю с проверки того, что включены три обязательных поля. Это username
, email
и password
. Если какой-либо из них отсутствует, то я использую вспомогательную функцию bad_request()
из app/api/errors.py для возврата клиенту ошибки. В дополнение к этой проверке мне нужно убедиться, что поля username
и email
еще не используются другим пользователем, поэтому для этого я пытаюсь загрузить пользователя из базы данных по имени пользователя и электронной почте, и если какой-либо из них возвращает действительного пользователя, я также возвращаю ошибку обратно клиенту.
После того, как я прошел проверку данных, я могу легко создать объект пользователя и добавить его в базу данных. Для создания пользователя я использую метод from_dict()
в пользовательской модели. Аргумент new_user
имеет значение True
, поэтому он также принимает поле password
, которое обычно не является частью представления пользователя.
Ответ, который я верну для этого запроса, будет представлением нового пользователя, поэтому to_dict()
генерирует эту полезную нагрузку. Код состояния для запроса POST
, который создает ресурс, должен быть 201
, который используется, когда новый объект был успешно создан. Кроме того, для протокола HTTP требуется, чтобы ответ 201
включал заголовок Location, который задан URL-адресом нового ресурса.
Ниже вы можете увидеть, как зарегистрировать нового пользователя из командной строки через HTTPie:
(venv) $ http POST http://localhost:5000/api/users username=alice password=dog
email=alice@example.com "about_me=Hello, my name is Alice!"
Редактирование пользователей
Последняя конечная точка, которую я собираюсь использовать в своем API, — это та, которая изменяет существующего пользователя:
app/api/users.py:
Изменение пользователя.
@bp.route('/users/<int:id>', methods=['PUT'])
def update_user(id):
user = User.query.get_or_404(id)
data = request.get_json() or {}
if 'username' in data and data['username'] != user.username and
User.query.filter_by(username=data['username']).first():
return bad_request('please use a different username')
if 'email' in data and data['email'] != user.email and
User.query.filter_by(email=data['email']).first():
return bad_request('please use a different email address')
user.from_dict(data, new_user=False)
db.session.commit()
return jsonify(user.to_dict())
Для этого запроса я получаю id
пользователя как динамическую часть URL, поэтому я могу загрузить назначенного пользователя и вернуть ошибку 404
, если она не найдена. Как и в случае с новым пользователем, мне нужно проверить, что поля username
и email
, предоставленные клиентом, не сталкиваются с другими пользователями, прежде чем я смогу их использовать, но в этом случае проверка немного сложнее. Прежде всего, эти поля являются необязательными в этом запросе, поэтому мне нужно проверить, что поле присутствует. Второе осложнение заключается в том, что клиент может предоставлять одно и то же значение, поэтому, прежде чем я проверю, берется ли имя пользователя или электронная почта, мне нужно убедиться, что они отличаются от текущих. Если какая-либо из этих проверок завершится ошибкой, я верну клиенту ошибку 400
, как и раньше.
После проверки данных я могу использовать метод From_dict()
пользовательской модели для импорта всех данных, предоставленных клиентом, а затем зафиксировать изменение в базе данных. Ответ на этот запрос возвращает пользователю обновленное представление пользователя с кодом состояния по умолчанию 200
.
Вот пример запроса, который редактирует поле about_me
с HTTPie:
(venv) $ http PUT http://localhost:5000/api/users/2 "about_me=Hi, I am Miguel"
API Аутентификация
Конечные точки API, которые я добавил в предыдущем разделе, в настоящее время открыты для любых клиентов. Очевидно, что они должны быть доступны только зарегистрированным пользователям, и для этого мне нужно добавить аутентификацию и авторизацию, или «AuthN» и «AuthZ» для краткости. Идея состоит в том, что запросы, отправленные клиентами, предоставляют некоторую идентификацию, так что сервер знает, какого пользователя представляет клиент, и может проверить, разрешено или нет запрошенное действие для этого пользователя.
Наиболее очевидным способом защиты этих конечных точек API является использование декоратора @login_required
из Flask-Login, но у этого подхода есть некоторые проблемы. Когда декоратор обнаруживает не аутентифицированного пользователя, он перенаправляет пользователя на HTML страницу входа в систему. В API нет концепции страниц HTML или входа в систему, если клиент отправляет запрос с недопустимыми или отсутствующими учетными данными, сервер должен отказаться от запроса, возвращающего код состояния 401. Сервер не может предположить, что клиент API является веб-браузером или он может обрабатывать переадресации или что он может отображать и обрабатывать формы входа в систему HTML. Когда API клиента получает код состояния 401, клиент знает, что ему нужно запросить у пользователя учетные данные, но как это происходит, на самом деле это не дело сервера.
Маркеры(токены) в пользовательской модели
Для проверки подлинности API я собираюсь использовать схему аутентификации с токеном. Когда клиент хочет начать взаимодействие с API, ему необходимо запросить временный токен, аутентифицироваться с именем пользователя и паролем. Затем клиент может отправлять запросы API, передающие токен в качестве аутентификации, до тех пор, пока токен действителен. По истечении срока действия токена необходимо запросить новый токен. Для поддержки токенов пользователей я собираюсь расширить модель User
:
app/models.py:
Поддержка пользовательских токенов.
import base64
from datetime import datetime, timedelta
import os
class User(UserMixin, PaginatedAPIMixin, db.Model):
# ...
token = db.Column(db.String(32), index=True, unique=True)
token_expiration = db.Column(db.DateTime)
# ...
def get_token(self, expires_in=3600):
now = datetime.utcnow()
if self.token and self.token_expiration > now + timedelta(seconds=60):
return self.token
self.token = base64.b64encode(os.urandom(24)).decode('utf-8')
self.token_expiration = now + timedelta(seconds=expires_in)
db.session.add(self)
return self.token
def revoke_token(self):
self.token_expiration = datetime.utcnow() - timedelta(seconds=1)
@staticmethod
def check_token(token):
user = User.query.filter_by(token=token).first()
if user is None or user.token_expiration < datetime.utcnow():
return None
return user
С этим изменением я добавляю атрибут token
в пользовательскую модель, и поскольку мне нужно будет искать базу данных по ней, я делаю ее уникальной и индексированной. Я также добавил token_expiration
, в котором есть дата и время истечения срока действия токена. Это делается для того, чтобы маркер не оставался действительным в течение длительного периода времени, что может стать угрозой безопасности.
Я создал три метода работы с этими токенами. Метод get_token()
возвращает токен для пользователя. Токен генерируется как случайная строка, закодированная в base64, так что все символы находятся в читаемом диапазоне. Перед созданием нового токена этот метод проверяет, есть ли у назначенного токена по крайней мере минута до истечения срока действия, и в этом случае возвращается существующий токен.
При работе с токенами всегда полезно иметь стратегию немедленного отзыва токена, а не полагаться только на дату истечения срока действия. Это лучшая практика в области безопасности, которую часто упускают из виду. Метод revoke_token()
делает маркер, назначенный пользователю, недействительным, просто установив дату истечения срока действия на одну секунду до текущего времени.
Метод check_token()
является статическим методом, который принимает токен в качестве входных данных и возвращает пользователя, которому этот токен принадлежит в качестве ответа. Если токен недействителен или истек, метод возвращает None.
Поскольку я внес изменения в базу данных, мне нужно создать новую миграцию базы данных, а затем обновить ее:
(venv) $ flask db migrate -m "user tokens"
(venv) $ flask db upgrade
Запросы Маркеров(токенов)
Когда вы пишете API, вы должны учитывать, что ваши клиенты не всегда будут веб-браузерами, подключенными к веб-приложению. Реальная сила API приходит, когда автономные клиенты, такие как приложения для смартфонов или даже браузерные одностраничные приложения могут иметь доступ к серверным службам. Когда эти специализированные клиенты нуждаются в доступе к службам API, они начинают с запроса маркера, который является аналогом формы входа в традиционное веб-приложение.
Чтобы упростить взаимодействие между клиентом и сервером при использовании аутентификации токенов, я собираюсь использовать расширение Flask под названием Flask-HTTPAuth. Flask-HTTPAuth устанавливается с pip:
(venv) $ pip install flask-httpauth
Flask-HTTPAuth поддерживает несколько различных механизмов аутентификации, все API дружественные. Для начала я собираюсь использовать HTTP Basic Authentication или тут 11.1, в которой клиент отправляет учетные данные пользователя в стандартном заголовке http авторизации. Для интеграции с Flask-HTTPAuth приложение должно предоставить две функции: одну, которая определяет логику для проверки имени пользователя и пароля, предоставленных пользователем, и другую, которая возвращает ответ об ошибке в случае сбоя аутентификации. Эти функции регистрируются в Flask-HTTPAuth через декораторы, а затем автоматически вызываются расширением по мере необходимости во время потока проверки подлинности. Вы можете увидеть реализацию:
app/api/auth.py:
Поддержка обычной проверки подлинности.
from flask import g
from flask_httpauth import HTTPBasicAuth
from app.models import User
from app.api.errors import error_response
basic_auth = HTTPBasicAuth()
@basic_auth.verify_password
def verify_password(username, password):
user = User.query.filter_by(username=username).first()
if user is None:
return False
g.current_user = user
return user.check_password(password)
@basic_auth.error_handler
def basic_auth_error():
return error_response(401)
Класс HTTPBasicAuth
из Flask-HTTPAuth-это класс, реализующий основной поток проверки подлинности. Две необходимые функции настраиваются с помощью декораторов verify_password
и error_handler
соответственно.
Функция проверки получает имя пользователя и пароль, предоставленные клиентом, и возвращает True
, если учетные данные действительны, или False
, если нет. Для проверки пароля я полагаюсь на метод check_password()
класса User
, который также используется Flask-Login при аутентификации для веб-приложения. Я сохраняю аутентифицированного пользователя в g.current_user
, так что я могу получить доступ к нему из функций представления API.
Функция обработчика ошибок просто возвращает ошибку 401, сгенерированную функцией error_response()
в app/api/errors.py. Ошибка 401 определяется в стандарте HTTP как «Unauthorized» ошибка («несанкционированного доступа»). Клиенты HTTP знают, что при получении этой ошибки отправленный ими запрос должен быть повторно отправлен с действительными учетными данными.
Теперь у меня есть базовая поддержка аутентификации, поэтому я могу добавить маршрут поиска токенов, который будут вызывать клиенты, когда им нужен этот самый токен:
app/api/tokens.py:
Generate user tokens.
from flask import jsonify, g
from app import db
from app.api import bp
from app.api.auth import basic_auth
@bp.route('/tokens', methods=['POST'])
@basic_auth.login_required
def get_token():
token = g.current_user.get_token()
db.session.commit()
return jsonify({'token': token})
Эта функция представления обернута декоратором @basic_auth.login_required
из экземпляра HTTPBasicAuth, который будет инструктировать Flask-HTTPAuth для проверки подлинности (через функцию проверки которую я определил выше) и разрешать функцию для запуска только, когда предоставленные учетные данные являются действительными. Реализация этой функции представления зависит от метода get_token()
пользовательской модели для создания маркера. Фиксация базы данных выполняется после создания маркера, чтобы гарантировать, что токен и его срок действия будут записаны обратно в базу данных.
При попытке отправить запрос POST на маршрут API маркеров происходит следующее:
(venv) $ http POST http://localhost:5000/api/tokens
HTTP/1.0 401 UNAUTHORIZED
Content-Length: 30
Content-Type: application/json
Date: Mon, 27 Nov 2017 20:01:00 GMT
Server: Werkzeug/0.12.2 Python/3.6.3
WWW-Authenticate: Basic realm="Authentication Required"
{
"error": "Unauthorized"
}
Ответ HTTP включает в себя код состояния 401 и полезную нагрузку, которую я определил в моей функции basic_auth_error()
. Вот тот же запрос, на этот раз включая базовые учетные данные:
(venv) $ http --auth <username>:<password> POST http://localhost:5000/api/tokens
HTTP/1.0 200 OK
Content-Length: 50
Content-Type: application/json
Date: Mon, 27 Nov 2017 20:01:22 GMT
Server: Werkzeug/0.12.2 Python/3.6.3
{
"token": "pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"
}
Теперь код состояния 200, который является кодом для успешного запроса, и полезные данные включают недавно созданный маркер для пользователя. Обратите внимание, что при отправке этого запроса необходимо заменить <username>:<password>
своими учетными данными. Имя пользователя и пароль должны быть снабжены двоеточием в качестве разделителя.
Защита маршрутов API с помощью токенов
Теперь клиенты могут запрашивать токен для использования с конечными точками API, поэтому осталось добавить проверку токена на эти конечные точки. Это то, что Flask-HTTPAuth также может обрабатывать для меня. Мне нужно создать второй экземпляр проверки подлинности на основе класса HTTPTokenAuth
и предоставить обратный вызов проверки токена:
app/api/auth.py:
Поддержка аутентификации Token.
# ...
from flask_httpauth import HTTPTokenAuth
# ...
token_auth = HTTPTokenAuth()
# ...
@token_auth.verify_token
def verify_token(token):
g.current_user = User.check_token(token) if token else None
return g.current_user is not None
@token_auth.error_handler
def token_auth_error():
return error_response(401)
При использовании аутентификации по токенам Flask-HTTPAuth использует функцию verify_token
, но кроме этого аутентификация токена работает так же, как и базовая аутентификация. Функция проверки токена использует User.check_token()
, чтобы найти пользователя, которому принадлежит предоставленный токен. Функция также обрабатывает случай отсутствующего токена, установив текущего пользователя в None
. Возвращаемое значение True
или False
определяет, может ли Flask-HTTPAuth разрешить выполнение функции просмотра или нет.
Чтобы защитить маршруты API с помощью токенов, необходимо добавить декоратор @token_auth.login_required
:
app/api/users.py:
Protect user routes with token authentication.
from app.api.auth import token_auth
@bp.route('/users/<int:id>', methods=['GET'])
@token_auth.login_required
def get_user(id):
# ...
@bp.route('/users', methods=['GET'])
@token_auth.login_required
def get_users():
# ...
@bp.route('/users/<int:id>/followers', methods=['GET'])
@token_auth.login_required
def get_followers(id):
# ...
@bp.route('/users/<int:id>/followed', methods=['GET'])
@token_auth.login_required
def get_followed(id):
# ...
@bp.route('/users', methods=['POST'])
def create_user():
# ...
@bp.route('/users/<int:id>', methods=['PUT'])
@token_auth.login_required
def update_user(id):
# ...
Обратите внимание, что декоратор добавляется ко всем функциям представления API, кроме create_user()
, который не может принять аутентификацию, так как пользователь, который запросит маркер, должен быть создан первым.
Если вы отправляете запрос на любую из этих конечных точек, как показано ранее, вы получите ответ об ошибке 401. Чтобы получить доступ, вам нужно добавить заголовок Authorization
с маркером, который вы получили от запроса в /api/tokens. Flask-HTTPAuth ожидает, что токен будет отправлен как токен-носитель, который напрямую не поддерживается HTTPie. Для базовой аутентификации с именем пользователя и паролем HTTPie предлагает параметр --auth
, но для токенов заголовок должен быть явно указан. Вот синтаксис для отправки токена-носителя:
(venv) $ http GET http://localhost:5000/api/users/1
"Authorization:Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"
Отмена токенов
Последняя функция, связанная с токеном, которую я собираюсь реализовать, — это отзыв токена, который Вы можете увидеть ниже:
app/api/tokens.py:
Revoke tokens.
from app.api.auth import token_auth
@bp.route('/tokens', methods=['DELETE'])
@token_auth.login_required
def revoke_token():
g.current_user.revoke_token()
db.session.commit()
return '', 204
Клиенты могут отправить запрос DELETE
на удаление по URL-адресу /tokens, чтобы аннулировать маркер. Проверка подлинности для этого маршрута основана на маркере, на самом деле маркер, отправляемый в заголовке Authorization
, является отозванным. Само аннулирование использует вспомогательный метод в классе User
, который сбрасывает дату истечения срока действия маркера. Сеанс базы данных фиксируется таким образом, что это изменение записывается в базу данных. Ответ от этого запроса не имеет тела, поэтому я могу вернуть пустую строку. Второе значение в инструкции return задает код состояния ответа 204, который используется для успешных запросов, не имеющих тела ответа.
Вот пример запроса на отзыв токена, отправленного из HTTPie:
(venv) $ http DELETE http://localhost:5000/api/tokens
Authorization:"Bearer pC1Nu9wwyNt8VCj1trWilFdFI276AcbS"
Сообщения об ошибках API
Вы помните, что произошло в начале этой главы, когда я попросил вас отправить запрос API из браузера с недействительным URL-адресом пользователя? Сервер вернул ошибку 404, но эта ошибка была отформатирована как стандартная страница ошибки 404 HTML. Многие ошибки, которые может потребоваться вернуть API, могут быть переопределены версиями JSON в схеме элементов API, но есть некоторые ошибки, обрабатываемые Flask, которые по-прежнему проходят через обработчики ошибок, глобально зарегистрированные для приложения, и они продолжают возвращать HTML.
Протокол HTTP поддерживает механизм, с помощью которого клиент и сервер могут согласовать оптимальный Формат ответа, называемый согласованием содержимого. Клиенту необходимо отправить Заголовок Accept
с запросом, указав предпочтения формата. Затем сервер смотрит на список и отвечает, используя лучший формат, который он поддерживает из списка, предлагаемого клиентом.
Я хочу изменить глобальные обработчики ошибок приложений, чтобы они использовали согласование содержимого для ответа в HTML или JSON в соответствии с предпочтениями клиента. Это можно сделать с помощью объекта Flask request.accept_mimetypes
:
app/errors/handlers.py:
Согласование содержимого для ответов об ошибках.
from flask import render_template, request
from app import db
from app.errors import bp
from app.api.errors import error_response as api_error_response
def wants_json_response():
return request.accept_mimetypes['application/json'] >=
request.accept_mimetypes['text/html']
@bp.app_errorhandler(404)
def not_found_error(error):
if wants_json_response():
return api_error_response(404)
return render_template('errors/404.html'), 404
@bp.app_errorhandler(500)
def internal_error(error):
db.session.rollback()
if wants_json_response():
return api_error_response(500)
return render_template('errors/500.html'), 500
Вспомогательная функция wants_json_response()
сравнивает предпочтения для JSON или HTML, выбранные клиентом в списке предпочтительных форматов. Если скорость JSON выше, чем HTML, то я возвращаю ответ JSON. В противном случае я верну исходные HTML-ответы на основе шаблонов. Для ответов JSON я собираюсь импортировать вспомогательную функцию error_response
из схемы элементов API, но здесь я собираюсь переименовать ее в api_error_response()
, чтобы было ясно, что она делает и откуда.
In my last Python Flask article, I walked you through the building of a simple application to take in a Threat Stack webhook and archive the alert to AWS S3. In this post, I’ll dive into Python exception handling and how to do it in a secure manner.
I wrote the code in the last article to be as simple and readable as possible, but what happens if something goes wrong in my application? I didn’t include any error or exception handling. If something goes wrong—for example, say you hit a bug or receive a bad dat—you can’t do anything about it in the application. Instead of returning a parseable JSON (JavaScript Object Notation) response, the app will just spit a backtrace embedded in an HTML document back. The entity sending the request to your service is then left trying to figure out what may have gone wrong.
What do you need to handle?
Some words of wisdom:
A distributed system is one in which the failure of a computer you didn’t even know existed can render your own computer unusable.
—Leslie Lamport, computer scientist and 2013 A.M. Turing Award winner.
You can start by placing «computer» with «service» in the preceding Lamport quotation. Your application talks to Threat Stack and AWS S3. A failure communicating with either of those can cause your own service to fail. A failure might be caused by the service being down, being unresponsive, or returning an unexpected response. Any number of issues can cause a communication failure between systems.
You also need to handle input validation. Your service has two different requests that take input:
- Sending alerts to the service requires a JSON document to be sent and parsed.
- Searching for alerts can take optional date parameters.
The input to your service might not be what you expect through a simple mistake, such as a typo or a misunderstanding of what’s required. Worse, some people will intentionally send bad data to see what happens. Fuzzing is a technique used in application penetration testing where malformed or semi-formed data is sent to a service to uncover bugs.
What is the worst that could happen?
Other than being an unreliable service that’s regularly breaking? I mentioned before that on an error the application will return a backtrace. Let’s see what happens when an unparseable date is sent to your service:
You’re returning your own code back to the requester. This code is reasonably benign, so take look at another example. If there was a Threat Stack communication issue: an issue that might happen completely at random (though hopefully not), this would appear:
You’re leaking the location of the service you’re talking to, and if a developer had used poor practices, you might have even leaked your API key to a random person.
Exception catching and handling
Now that you know why handling exceptions in your application is important, I’ll turn my focus on how to handle them properly. You want to accomplish the following when you start handling exceptions:
- Identify what could go wrong
- Return useful information to the client
- Don’t leak too much information
I’ll admit that until now I did many things dangerously or even incorrectly until I wrote this post and I finally made corrections. While searching for answers, I found that many other people had similar questions about how to do things correctly. Even if you think this is a trivial topic, why not take a refresher?
Catching exceptions in app.models.threatstack
I’ll walk through a part of this module to highlight a few different situations for you to handle. This is your function for getting alert detail from Threat Stack for a given alert ID:
def get_alert_by_id(alert_id):
'''
Retrieve an alert from Threat Stack by alert ID.
'''
alerts_url = '{}/alerts/{}'.format(THREATSTACK_BASE_URL, alert_id)
resp = requests.get(
alerts_url,
headers={'Authorization': THREATSTACK_API_KEY}
)
return resp.json()
The function is straightforward. It constructs a URL, makes a request to Threat Stack, and returns the response’s JSON content. So what can wrong? Of those three statements two can easily go wrong. When making a request to Threat Stack, a communication error that results in failure can occur. If you do get a response, you expect to parse a JSON document. What if there is no JSON document in the response?
Let’s start with a failed request to Threat Stack. Put request.get() into a try/except block that will catch the exception type requests.exceptions.RequestException:
try:
resp = requests.get(
alerts_url,
headers={'Authorization': THREATSTACK_API_KEY}
)
except requests.exceptions.RequestException as e:
` Pass
If you fail, this lets you perform any additional actions that you feel are necessary. If you were working with a database, you might roll back a transaction. You also might want to log the error for analysis later. (You would probably do that if you had already written the logging component for this application.) Notice that you’re specifying the exception type to catch. Do not blanket catch all exceptions. You may be tempted to do this to save time, but it will potentially make your life harder down the road as you find yourself unable to understand why your application is failing. Take the time now to understand why your application might fail and for what reasons.
What do you want to do if the app fails to communicate with Threat Stack? You’re going to raise a new exception. This is called catch and reraise. This technique makes organizing exception handling a little easier. You’re going to define a set of exception classes inside the app.models.threatstack module that describe what could go wrong. Doing this will make it easier later when you’ll add a handler to the application and tell it how to handle exceptions from the app.models.threatstack module.
You’ll start by adding two exception classes. The first is the base exception class, which inherits the base Python Exception class. Every subsequent exception class will inherit the new base exception class. At first this may just seem like extra work, but it will be useful down the road. The next class will be for request failures. You’ll even add a Threat Stack API error that you’ll use later. You want the class name to be descriptive, so that you’ll understand why your application failed just by reading it:
class ThreatStackError(Exception):
'''Base Threat Stack error.'''
class ThreatStackRequestError(ThreatStackError):
'''Threat Stack request error.'''
class ThreatStackAPIError(ThreatStackError):
'''Threat API Stack error.'''
With the Exception classes in place, you can catch and reraise an exception:
try:
resp = requests.get(
alerts_url,
headers={'Authorization': THREATSTACK_API_KEY}
)
except requests.exceptions.RequestException as e:
exc_info = sys.exc_info()
raise ThreatStackRequestError, ThreatStackRequestError(e), exc_info[2]
What’s going on after you catch the exception? Why didn’t you just do this?
except requests.exceptions.RequestException as e:
raise ThreatStackRequestError(e.args)
This mistake is very common when people catch and reraise exceptions. If you did the above, you lose the application backtrace. Inspecting the backtrace would show that you entered get_alert_by_id() and then you raised an error. You wouldn’t see the further context of why request.get() failed. The previous example is the correct way to catch and reraise errors in Python 2. Your code will throw an exception named for a class that you know, and it will give you the code trace that leads to the exception so you can better debug it.
You have made a request, communicated with Threat Stack correctly, and are ready to return the response at the end of this function:
return resp.json()
What can go wrong here? For one thing, the response may not have been a JSON body, which would cause you to throw an exception while attempting to parse it. The API is always supposed to return JSON, even on an error, but it is possible that something might still go unexpectedly wrong. Maybe an application issue spews a backtrace on error just as your application does right now. Maybe a load balancer has an issue and returns a 503 with a «Service Unavailable» page. API failures can also occur. You might have been sent back a JSON response that’s perfectly parseable only to tell you that your request failed for some reason. For example, when you’re trying to retrieve an alert that does not exist. Simply put, you need to make sure that your request returned a successful response. If you didn’t get a successful response, you raise an error. You might be returned a communication error or an API error, so depending on what you received, you’ll raise either ThreatStackRequestError or ThreatStackAPIError:
if not resp.ok:
if 'application/json' in resp.headers.get('Content-Type'):
raise ThreatStackAPIError(resp.reason,
resp.status_code,
resp.json()
)
else:
raise ThreatStackRequestError(resp.reason, resp.status_code)
return resp.json()
If the request was successful, resp.ok will be True. If it is not, then you’ll try to determine what sort of failure occurred: communication or API? You’ll use a very simple approach to figuring out the difference. If the response header indicates JSON, assume you were able to talk to the API and the API sent you an error. Otherwise assume that something else along the way failed and you never made it to the Threat Stack API, and that it is a communication error.
Handling exceptions
Thus far you’ve been catching exceptions only to reraise a new exception. It might feel that you’re not that much further from where you started. You’re just raising exceptions and returning a backtrace to the client, but with your own class name.
You’re still leaking code, potentially leaking secrets, and providing someone with greater intelligence about your environment than you really want to. Now you need to start handling these exceptions.
Flask’s documentation provides a good overview of handling exceptions. You’re just going to tweak it slightly due to the simplicity of our application. Start by associating HTTP status codes with your error classes. Let’s revisit your Threat Stack error classes in app.models.threatstack:
app.models.threatstack
class ThreatStackError(Exception):
'''Base Threat Stack error.'''
class ThreatStackRequestError(ThreatStackError):
'''Threat Stack request error.'''
class ThreatStackAPIError(ThreatStackError):
'''Threat API Stack error.'''
You raise these exceptions when your service attempts to talk to Threat Stack and something unexpected happens. These can arguably be considered 500 level server errors. (Note: You can make a case that an invalid alert ID passed to get_alert_by_id(), which raises a ThreatStackAPIError exception should actually be a 400 Bad Request, but I’m not that concerned. My own preference is to simply consider model level exceptions as 500 level and view level exceptions as 400 level.) Recall when I suggested creating a base ThreatStackError class? Here’s where you’ll first use it:
app.models.threatstack
class ThreatStackError(Exception):
'''Base Threat Stack error.'''
status_code = 500
class ThreatStackRequestError(ThreatStackError):
'''Threat Stack request error.'''
class ThreatStackAPIError(ThreatStackError):
'''Threat API Stack error.'''
Repeat this process for adding status_codes in app.models.s3 and app.views.s3, too.
Now that your error classes have an HTTP status code, you’ll add a handler for application exceptions. Flask’s documentation uses the errorhandler() decorator. You would add the decorator and a function to the app.view.s3 module just as if you were adding another endpoint to your application:
app.view.s3
@s3.route('/status', methods=['GET'])
def is_available():
# <SNIP>
@s3.errorhandler(Exception)
def handle_error(error):
# <SNIP>
This is great for larger apps, which perhaps require more organization and different views that require their own error handling, but let’s keep your code a little simpler. Instead you’ll add a single Flask blueprint for handling errors that will handle all application exceptions:
app.errors
'''Application error handlers.'''
from flask import Blueprint, jsonify
errors = Blueprint('errors', __name__)
@errors.app_errorhandler(Exception)
def handle_error(error):
message = [str(x) for x in error.args]
status_code = error.status_code
success = False
response = {
'success': success,
'error': {
'type': error.__class__.__name__,
'message': message
}
}
return jsonify(response), status_code
This is good to start with, but you’re going to make an additional tweak. We are assuming that all Exception objects have a status_code attribute, which is simply not true. We would like to think that we are prepared to catch every possible exception case in our code, but people make mistakes. For that reason, you’ll have two error handler functions. One will handle the error classes you know about (there’s our base exception classes again), and the other will be for unexpected errors.
Another important thing to notice is that the application blindly returns the message associated with errors you catch. You’re still at risk of potentially revealing information about your infrastructure, how your application works, or your secrets. In this particular application’s case, you don’t have to be as worried because you’re aware of the types of exceptions you catch and reraise along with the information those exceptions return. For those exceptions you didn’t anticipate, you always return the same error message as a precaution. I will revisit this in a later article when I discuss logging. Because this application currently has no logging, you’re relying on the error response to be highly descriptive.
When you’re returning API errors, ask yourself who will be using your service. Does the requester need to know as much as you’re returning? A developer might appreciate the added context to help them debug their own service. An external third party probably doesn’t need to know how your backend failed.
app.errors
'''Application error handlers.'''
from app.models.s3 import S3ClientError
from app.models.threatstack import ThreatStackError
from flask import Blueprint, jsonify
errors = Blueprint('errors', __name__)
@errors.app_errorhandler(S3ClientError)
@errors.app_errorhandler(ThreatStackError)
def handle_error(error):
message = [str(x) for x in error.args]
status_code = 500
success = False
response = {
'success': success,
'error': {
'type': error.__class__.__name__,
'message': message
}
}
return jsonify(response), status_code
@errors.app_errorhandler(Exception)
def handle_unexpected_error(error):
status_code = 500
success = False
response = {
'success': success,
'error': {
'type': 'UnexpectedException',
'message': 'An unexpected error has occurred.'
}
}
return jsonify(response), status_code
Finally, you’ll hook this blueprint up to the application in the app module. You add an additional function called _initialize_errorhandler(), which will import the blueprint and add it to your application:
app
def _initialize_errorhandlers(application):
'''
Initialize error handlers
'''
from app.errors import errors
application.register_blueprint(errors)
def create_app():
'''
Create an app by initializing components.
'''
application = Flask(__name__)
_initialize_errorhandlers(application)
_initialize_blueprints(application)
# Do it!
return application
Now you have functional error handling when the application throws an exception, so instead of throwing a backtrace and revealing code as well as potentially returning sensitive information, the app returns a JSON doc that describes the error.
Final thoughts
You have made your threatstack-to-s3 service far more resilient to failure, but you probably also see there being more for us to do. In an upcoming post, I’ll discuss logging.
View the finished product from this post.
This article originally appeared on the Threat Stack blog. Reposted with permission.
This work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.
Работа с ошибками приложения
Приложения отказывают,серверы отказывают.Рано или поздно вы увидите исключение в производстве.Даже если ваш код на 100% правильный,вы все равно будете время от времени видеть исключения.Почему? Потому что все остальное,что задействовано в коде,не работает.Вот несколько ситуаций,когда совершенно правильный код может привести к ошибкам сервера:
- клиент завершил запрос раньше времени,а приложение все еще считывало входящие данные
- сервер базы данных был перегружен и не смог обработать запрос
- файловая система заполнена
- разбился жесткий диск
- перегрузка внутреннего сервера
- программная ошибка в используемой вами библиотеке
- сетевое подключение сервера к другой системе не удалось
И это лишь небольшая часть проблем, с которыми вы можете столкнуться. Итак, как нам справиться с такой проблемой? По умолчанию, если ваше приложение работает в производственном режиме и возникает исключение, Flask отобразит для вас очень простую страницу и зарегистрирует исключение в logger
.
Но есть и другие возможности,и мы расскажем о некоторых лучших настройках для работы с ошибками,включая пользовательские исключения и инструменты сторонних производителей.
Инструменты протоколирования ошибок
Отправка сообщений об ошибках, даже если это критические сообщения, может стать чрезмерным, если достаточное количество пользователей сталкивается с ошибкой, а файлы журналов обычно никогда не просматриваются. Вот почему мы рекомендуем использовать Sentry для работы с ошибками приложений. Он доступен в виде проекта с исходным кодом на GitHub , а также доступен в виде размещенной версии , которую вы можете попробовать бесплатно. Sentry собирает повторяющиеся ошибки, фиксирует полную трассировку стека и локальные переменные для отладки и отправляет вам письма на основе новых ошибок или пороговых значений частоты.
Чтобы использовать Sentry, вам необходимо установить клиент sentry-sdk
с дополнительными зависимостями flask
.
$ pip install sentry-sdk[flask]
А затем добавьте это в ваше приложение Flask:
import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration sentry_sdk.init('YOUR_DSN_HERE', integrations=[FlaskIntegration()])
Значение YOUR_DSN_HERE
необходимо заменить значением DSN, полученным при установке Sentry.
После установки сбои,приводящие к внутренней ошибке сервера,автоматически сообщаются в Sentry,откуда вы можете получать уведомления об ошибках.
See also:
- Sentry также поддерживает перехват ошибок из рабочей очереди (RQ, Celery и т. Д.) Аналогичным образом. Дополнительную информацию см. В документации Python SDK .
- Начало работы с Sentry
- Flask-specific documentation
Error Handlers
Когда во Flask возникает ошибка, будет возвращен соответствующий код состояния HTTP . 400-499 указывают на ошибки в данных запроса клиента или в запрошенных данных. 500-599 указывают на ошибки сервера или самого приложения.
Вы можете захотеть показывать пользователю пользовательские страницы ошибок при возникновении ошибки.Это можно сделать,зарегистрировав обработчики ошибок.
Обработчик ошибок — это функция, которая возвращает ответ при возникновении ошибки определенного типа, подобно тому, как представление — это функция, возвращающая ответ при совпадении URL-адреса запроса. Ему передается экземпляр обрабатываемой ошибки, который, скорее всего, является HTTPException
.
Код состояния ответа не будет установлен на код обработчика.При возврате ответа от обработчика обязательно указывайте соответствующий код состояния HTTP.
Registering
Зарегистрируйте обработчики, украсив функцию errorhandler()
. Или используйте register_error_handler()
чтобы зарегистрировать функцию позже. Не забудьте установить код ошибки при возврате ответа.
@app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): return 'bad request!', 400 app.register_error_handler(400, handle_bad_request)
Подклассы werkzeug.exceptions.HTTPException
,такие как BadRequest
, и их HTTP-коды взаимозаменяемы при регистрации обработчиков. ( BadRequest.code == 400
)
Нестандартные коды HTTP не могут быть зарегистрированы с помощью кода, поскольку они неизвестны Werkzeug. Вместо этого определите подкласс HTTPException
с соответствующим кодом, зарегистрируйте и поднимите этот класс исключения.
class InsufficientStorage(werkzeug.exceptions.HTTPException): code = 507 description = 'Not enough storage space.' app.register_error_handler(InsufficientStorage, handle_507) raise InsufficientStorage()
Обработчики могут быть зарегистрированы для любого класса исключений, а не только для подклассов HTTPException
или кодов состояния HTTP. Обработчики могут быть зарегистрированы для определенного класса или для всех подклассов родительского класса.
Handling
При создании приложения Flask вы столкнетесь с исключениями. Если какая-то часть вашего кода сломается при обработке запроса (и у вас нет зарегистрированных обработчиков ошибок), по умолчанию будет возвращено «500 Internal Server Error» ( InternalServerError
).Точно так же ошибка «404 Not Found» ( NotFound
) возникает, если запрос отправляется на незарегистрированный маршрут. Если маршрут получает неразрешенный метод запроса, будет поднято «Метод 405 не разрешен» ( MethodNotAllowed
).Все они являются подклассами HTTPException
и по умолчанию предоставляются во Flask.
Flask дает вам возможность поднять любое исключение HTTP,зарегистрированное Werkzeug.Однако HTTP-исключения по умолчанию возвращают простые страницы исключений.Возможно,вы захотите показывать пользователю пользовательские страницы ошибок при возникновении ошибки.Это можно сделать,зарегистрировав обработчики ошибок.
Когда Flask перехватывает исключение при обработке запроса, оно сначала ищется по коду. Если для кода не зарегистрирован обработчик, Flask ищет ошибку по иерархии классов; выбирается наиболее конкретный обработчик. Если обработчик не зарегистрирован, подклассы HTTPException
отображают общее сообщение об их коде, в то время как другие исключения преобразуются в общее сообщение «500 Internal Server Error».
Например, если возникает экземпляр ConnectionRefusedError
и зарегистрирован обработчик для ConnectionError
и ConnectionRefusedError
, более конкретный обработчик ConnectionRefusedError
вызывается с экземпляром исключения для генерации ответа.
Обработчики,зарегистрированные на чертеже,имеют приоритет над обработчиками,зарегистрированными глобально в приложении,при условии,что чертеж обрабатывает запрос,вызвавший исключение.Однако блюпринт не может обрабатывать ошибки маршрутизации 404,поскольку 404 происходит на уровне маршрутизации до того,как можно определить блюпринт.
Общие обработчики исключений
Можно зарегистрировать обработчики ошибок для очень общих базовых классов, таких как HTTPException
или даже Exception
. Однако имейте в виду, что они поймают больше, чем вы могли ожидать.
Например, обработчик ошибок для HTTPException
может быть полезен для преобразования страниц ошибок HTML по умолчанию в формат JSON. Однако этот обработчик срабатывает для вещей, которые вы не вызываете напрямую, таких как ошибки 404 и 405 во время маршрутизации. Тщательно создавайте свой обработчик, чтобы не потерять информацию об ошибке HTTP.
from flask import json from werkzeug.exceptions import HTTPException @app.errorhandler(HTTPException) def handle_exception(e): """Return JSON instead of HTML for HTTP errors.""" response = e.get_response() response.data = json.dumps({ "code": e.code, "name": e.name, "description": e.description, }) response.content_type = "application/json" return response
Обработчик ошибок для Exception
может показаться полезным для изменения способа представления пользователю всех ошибок, даже необработанных. Однако это похоже на выполнение, except Exception:
в Python он фиксирует все необработанные в противном случае ошибки, включая все коды состояния HTTP.
В большинстве случаев будет безопаснее зарегистрировать обработчики для более конкретных исключений. Поскольку экземпляры HTTPException
являются действительными ответами WSGI, вы также можете передать их напрямую.
from werkzeug.exceptions import HTTPException @app.errorhandler(Exception) def handle_exception(e): if isinstance(e, HTTPException): return e return render_template("500_generic.html", e=e), 500
Обработчики ошибок по-прежнему соблюдают иерархию классов исключений. Если вы зарегистрируете обработчики как для HTTPException
,так и для Exception
, обработчик Exception
не будет обрабатывать подклассы HTTPException
, поскольку он является более конкретным обработчиком HTTPException
.
Unhandled Exceptions
Если для исключения не зарегистрирован обработчик ошибок, вместо этого будет возвращена ошибка 500 Internal Server Error. См. flask.Flask.handle_exception()
для получения информации об этом поведении.
Если для InternalServerError
зарегистрирован обработчик ошибок , он будет вызван. Начиная с Flask 1.1.0, этому обработчику ошибок всегда будет передаваться экземпляр InternalServerError
, а не исходная необработанная ошибка.
Исходная ошибка доступна как e.original_exception
.
В обработчик ошибки «500 Internal Server Error» будут передаваться не пойманные исключения в дополнение к явным 500 ошибкам.В режиме отладки обработчик для «500 Internal Server Error» не будет использоваться.Вместо этого будет показан интерактивный отладчик.
Пользовательские страницы ошибок
Иногда при создании приложения Flask вы можете захотеть создать исключение HTTPException
, чтобы сообщить пользователю, что с запросом что-то не так. К счастью, Flask поставляется с удобной функцией abort()
, которая прерывает запрос с ошибкой HTTP от werkzeug по желанию. Он также предоставит вам простую черно-белую страницу ошибки с основным описанием, но ничего особенного.
В зависимости от кода ошибки вероятность того,что пользователь действительно увидит такую ошибку,меньше или больше.
Рассмотрим код ниже,у нас может быть маршрут профиля пользователя,и если пользователь не передает имя пользователя,мы можем выдать сообщение «400 Bad Request».Если пользователь передает имя пользователя,но мы не можем его найти,мы выдаем сообщение «404 Not Found».
from flask import abort, render_template, request @app.route("/profile") def user_profile(): username = request.arg.get("username") if username is None: abort(400) user = get_user(username=username) if user is None: abort(404) return render_template("profile.html", user=user)
Вот еще один пример реализации исключения «404 Page Not Found»:
from flask import render_template @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404
При использовании фабрик приложений :
from flask import Flask, render_template def page_not_found(e): return render_template('404.html'), 404 def create_app(config_filename): app = Flask(__name__) app.register_error_handler(404, page_not_found) return app
Пример шаблона может быть следующим:
{% extends "layout.html" %} {% block title %}Page Not Found{% endblock %} {% block body %} <h1>Page Not Found</h1> <p>What you were looking for is just not there. <p><a href="{{ url_for('index') }}">go somewhere nice</a> {% endblock %}
Further Examples
Приведенные выше примеры на самом деле не являются улучшением стандартных страниц исключений.Мы можем создать пользовательский шаблон 500.html следующим образом:
{% extends "layout.html" %} {% block title %}Internal Server Error{% endblock %} {% block body %} <h1>Internal Server Error</h1> <p>Oops... we seem to have made a mistake, sorry!</p> <p><a href="{{ url_for('index') }}">Go somewhere nice instead</a> {% endblock %}
Это можно реализовать путем рендеринга шаблона при «500 Internal Server Error»:
from flask import render_template @app.errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500
При использовании фабрик приложений :
from flask import Flask, render_template def internal_server_error(e): return render_template('500.html'), 500 def create_app(): app = Flask(__name__) app.register_error_handler(500, internal_server_error) return app
При использовании модульных приложений с Blueprints :
from flask import Blueprint blog = Blueprint('blog', __name__) @blog.errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500 blog.register_error_handler(500, internal_server_error)
Обработчики ошибок чертежей
В Modular Applications with Blueprints большинство обработчиков ошибок будут работать должным образом. Однако есть предостережение относительно обработчиков исключений 404 и 405. Эти обработчики ошибок вызываются только из соответствующего оператора raise
или вызова abort
в другой функции представления схемы; они не вызываются, например, недопустимым доступом по URL-адресу.
Это связано с тем, что схема не «владеет» определенным пространством URL-адресов, поэтому экземпляр приложения не может узнать, какой обработчик ошибок схемы следует запустить, если указан недопустимый URL-адрес. Если вы хотите использовать разные стратегии обработки этих ошибок на основе префиксов URL, их можно определить на уровне приложения с помощью прокси-объекта request
from flask import jsonify, render_template @app.errorhandler(404) def page_not_found(e): if request.path.startswith('/blog/'): return render_template("blog/404.html"), 404 else: return render_template("404.html"), 404 @app.errorhandler(405) def method_not_allowed(e): if request.path.startswith('/api/'): return jsonify(message="Method Not Allowed"), 405 else: return render_template("405.html"), 405
Возврат ошибок API в формате JSON
При создании API-интерфейсов во Flask некоторые разработчики понимают, что встроенные исключения недостаточно выразительны для API-интерфейсов и что тип содержимого text / html, который они генерируют, не очень полезен для потребителей API.
Используя те же методы, что и выше, и jsonify()
, мы можем возвращать ответы JSON на ошибки API. abort()
вызывается с параметром description
. Обработчик ошибок будет использовать это как сообщение об ошибке JSON и установит код состояния 404.
from flask import abort, jsonify @app.errorhandler(404) def resource_not_found(e): return jsonify(error=str(e)), 404 @app.route("/cheese") def get_one_cheese(): resource = get_resource() if resource is None: abort(404, description="Resource not found") return jsonify(resource)
Мы также можем создавать пользовательские классы исключений.Например,мы можем создать новое пользовательское исключение для API,которое может принимать сообщение,читаемое человеком,код состояния ошибки и некоторую необязательную полезную нагрузку для создания дополнительного контекста ошибки.
Это простой пример:
from flask import jsonify, request class InvalidAPIUsage(Exception): status_code = 400 def __init__(self, message, status_code=None, payload=None): super().__init__() self.message = message if status_code is not None: self.status_code = status_code self.payload = payload def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message return rv @app.errorhandler(InvalidAPIUsage) def invalid_api_usage(e): return jsonify(e.to_dict()), e.status_code @app.route("/api/user") def user_api(user_id): user_id = request.arg.get("user_id") if not user_id: raise InvalidAPIUsage("No user id provided!") user = get_user(user_id=user_id) if not user: raise InvalidAPIUsage("No such user!", status_code=404) return jsonify(user.to_dict())
Теперь представление может вызвать это исключение с сообщением об ошибке. Кроме того, некоторая дополнительная полезная нагрузка может быть предоставлена в виде словаря через параметр payload
.
Logging
См. Ведение журнала для получения информации о том, как регистрировать исключения, например, отправляя их администраторам по электронной почте.
Debugging
См. Отладка ошибок приложений для получения информации о том, как отлаживать ошибки при разработке и производстве.
Flask
2.2
-
Waitress
-
Проектные решения во Flask
-
Extensions
-
Installation
Flask-ApiExceptions is a Flask extension that provides the basic
functionality for serializing uncaught exceptions as HTTP responses for
a JSON-based REST API.
Installation
You can install this extension with pip:
$ pip install flask_apiexceptions
Or, you can clone the repository:
$ git clone https://github.com/jperras/flask_apiexceptions.git
Running the Tests
Tox is used to run the tests,
which are written using PyTest.
To run them, clone the repository (indicated above), ensure tox is
installed and available, and run:
$ cd path/to/flask_apiexceptions
$ tox
Usage
This package includes an extension named JSONExceptionHandler, which
can be added to your application in the usual way:
from flask import Flask from flask_apiexceptions import JSONExceptionHandler app = Flask(__name__) exception_handler = JSONExceptionHandler(app)
The extension can also be initialized via deferred application init if
you’re using an application factory:
exception_handler = JSONExceptionHandler() exception_hander.init_app(app)
Once initialized, the extension doesn’t actually do anything by default.
You’ll have to configure it to handle Werkzeug HTTP error codes or
custom Exception classes.
Custom Exception Class Handling
An example showing how we can raise a custom exception within a view
method, and have that exception be transformed into a JSON response:
class MissingUserError(Exception):
status_code = 404
message = 'No such user exists.'
@app.route('/not-found')
def testing():
raise MissingUserError()
ext = JSONExceptionHandler(app)
ext.register(code_or_exception=MissingUserError)
with app.app_context():
with app.test_client() as c:
rv = c.get('/not-found')
assert rv.status_code == 404
assert rv.headers['content-type'] == 'application/json'
assert json.loads(rv.data)['message'] == 'No such user exists.'
This uses the JSONExceptionHandler.default_handler() to transform
the CustomError exception class into a suitable response. It
attempts to introspect the exception instance returned for a message
or description attribute, and also checks to see if there exists a
status_code attribute.
If any of those fields are found, the default handler will populate the
response data with the given message, and set the response status code.
If no message or status code is present, a default response of
{«message»: «An error occurred!»} with an
HTTP/1.1 500 Internal Server Error status code is set.
If you’d like to handle custom exception classes in a different manner,
say because you have more complex data captured within an exception
instance, or the attributes are not conveniently named message or
description, then you can specify a custom handler for the exception
type:
from flask_apiexceptions import JSONExceptionHandler
app = Flask(__name__)
ext = JSONExceptionHandler(app)
class CaffeineError(Exception):
teapot_code = 418
special = {'foo': 'bar'}
def caffeine_handler(error):
response = jsonify(data=error.special)
response.status_code = error.teapot_code
return response
@app.route('/testing')
def testing():
raise CaffeineError()
ext.register(code_or_exception=CaffeineError, handler=caffeine_handler)
with app.app_context():
with app.test_client() as c:
rv = c.get('/testing')
assert rv.status_code == 418
assert rv.headers['content-type'] == 'application/json'
assert json.loads(rv.data)['data'] == CaffeineError.special
This is also how, incidentally, you could use a response content type
other than application/json. Simply construct your own response
object isntead of using jsonify() within your handler, as long as it
produces a valid response as a return value.
Using ApiException and ApiError objects
Flask-ApiExceptions includes a few convenience classes and a handler
method for setting up structured API error responses. They are entirely
optional, but provide some sane defaults that should cover most
situatiosn.
An ApiException instance wraps one or more ApiError instances.
In this sense the ApiException is simply the container for the
actual error message. The ApiError instance accepts optional
code, message, and info attributes.
The idea is that the code should be an identifier for the type of
error, for example invalid-data or does-not-exist. The
message field should provide a more detailed and precise description
of the error. The info field can be used for any additional metadata
or unstructured information that may be required.
The info field, if utilized, should contain data that is JSON
serializable.
To use these constructs, you need to register the appropriate exception
class as well as an api_exception_handler that is provided for just
this purpose:
from flask_apiexceptions import (
JSONExceptionHandler, ApiException, ApiError, api_exception_handler)
app = Flask(__name__)
ext = JSONExceptionHandler(app)
ext.register(code_or_exception=ApiException, handler=api_exception_handler)
@app.route('/custom')
def testing():
error = ApiError(code='teapot', message='I am a little teapot.')
raise ApiException(status_code=418, error=error)
with app.app_context():
with app.test_client() as c:
rv = c.get('/custom')
# JSON response looks like...
# {"errors": [{"code": "teapot", "message": "I am a little teapot."}]}
assert rv.status_code == 418
assert rv.headers['content-type'] == 'application/json'
json_data = json.loads(rv.data)
assert json_data['errors'][0]['message'] == 'I am a little teapot.'
assert json_data['errors'][0]['code'] == 'teapot'
assert json_data['errors'][0]['info'] is None
Note that, when using the ApiException and ApiError classes, the
status code is set on the ApiException instance. This makes more
sense when you can set multiple ApiError objects to the same
ApiException:
from flask_apiexceptions import ApiException, ApiError
# ...
@app.route('/testing')
def testing():
exc = ApiException(status_code=400)
invalid_address_error = ApiError(code='invalid-data',
message='The address provided is invalid.')
invalid_phone_error = ApiError(code='invalid-data',
message='The phone number does not exist.',
info={'area_code': '555'})
exc.add_error(invalid_address_error)
exc.add_error(invalid_phone_error)
raise exc
# JSON response format:
# {"errors": [
# {"code": "invalid-data", "message": "The address provided is invalid."},
# {"code": "invalid-data", "message": "The phone number does not exist.", "info": {"area_code": "444"}}
# ]}
If you only want a single error to be instantiated within the
ApiException, this can be done via the constructor of the latter as a
shorthand:
exc = ApiException(
status_code=400,
code='invalid-data',
message='The address provided is invalid',
info={'zip_code': '90210'})
which is the equivalent of:
exc = ApiException(status_code=400)
error=ApiError(
code='invalid-data',
message='The address provided is invalid',
info={'zip_code': '90210'}))
exc.add_error(error)
A useful pattern is to subclass ApiException into distinctly useful
exception types, on which you can define default class-level attributes that
will be used to populate the correct error object on instantiation. For
example:
class MissingResourceError(ApiException):
status_code = 404
message = "No such resource exists."
code = 'not-found'
# ...
@app.route('/posts/<int:post_id>')
def post_by_id(post_id):
"""Fetch a single post by ID from the database."""
post = Post.query.filter(Post.id == post_id).one_or_none()
if post is None:
raise MissingResourceError()
# 404 response, wiht JSON body:
# {"errors": [
# {"code": "not-found", "message": "No such resource exists."}
# ]}
The nice thing about this particular pattern is that you can raise
semantically correct exceptions within your codebase, and can choose to
handle them in the call stack. If you don’t handle them, they simply bubble up
to the exception handler (if you’ve configured the
flask_apiexceptions.api_exception_handler or similar) registered with
Flask, and are then transformed into a useful response for the requesting client.
class MissingResourceError(ApiException):
status_code = 404
message = "No such resource exists."
code = 'not-found'
class Post(db.Model):
# ...
@classmethod
def query_by_id(cls, post_id):
"""Query Post by ID, raise exception if not found."""
result = cls.query.filter(cls.id == post_id).one_or_none()
if result is None:
raise MissingResourceError()
return result
@app.route('/posts/<int:post_id>')
def post_by_id(post_id):
"""Fetch a single post by ID from the database."""
try:
post = Post.query_by_id(post_id)
except MissingResourceError as e:
# We can do whatever we want now that we've caught the exception.
# For the sake of illustration, we're just going to log it.
app.logger.exception("Could not locate post!")
# Will bubble up the exception until it is rendered to JSON
# for the client.
raise e
Обработка HTTP-ошибок 404, 500 и т.д. во Flask.
Если какая-то часть кода кода сайта на Flask ломается при обработке запроса и нет зарегистрированных обработчиков ошибок, то по умолчанию будет возвращена ошибка 500 Internal Server Error
(InternalServerError
). Точно так же будет выводится стандартная страница с ошибкой 404 Not Found
, если запрос будет отправлен на незарегистрированный URL-адрес. Если маршрут получает недопустимый метод запроса, будет активирован HTTP-метод 405 Not Allowed
. Все это подклассы HTTPException
, которые по умолчанию предоставляются в Flask.
Фреймворк Flask дает возможность вызывать любое исключение HTTP, зарегистрированное Werkzeug
, но по умолчанию отдаются простые/стандартные страницы ошибок. Для удобства пользователя сайта, а так же повышения лояльности поисковых систем к сайту необходимо показывать настроенные страницы ошибок (вместо стандартных). Это можно сделать, зарегистрировав обработчики ошибок.
Обработчик ошибок — это функция, которая возвращает ответ при возникновении определенного типа ошибки, аналогично тому, как представление является функцией, которая возвращает ответ при совпадении URL-адреса запроса. Ему передается экземпляр обрабатываемой ошибки, который будет является исключением werkzeug.exceptions.HTTPException
.
Когда Flask перехватывает исключение при обработке запроса, сначала выполняется поиск по коду. Если в коде не зарегистрирован обработчик, то Flask ищет ошибку в иерархии классов и выбирает наиболее конкретный обработчик. В том случае, если обработчик не зарегистрирован, то подклассы HTTPException
показывают наиболее подходящую стандартную страницу с ошибкой, в то время как другие исключения преобразуются в общую страницу 500 Internal Server Error
.
Например, если возникает экземпляр ConnectionRefusedError
и зарегистрированы обработчики ConnectionError
и ConnectionRefusedError
, то для генерации ответа будет вызываться более конкретный обработчик ConnectionRefusedError
.
Содержание.
- Регистрация обработчика ошибок в веб-приложении на Flask;
- Универсальные обработчики исключений во Flask;
- Как Flask обрабатывает необработанные исключения?
- Создание собственной страницы с HTTP-ошибкой 404;
- Пример пользовательской страницы ошибки с кодом 500;
- Особенности обработки ошибок в схемах
blueprint
Flask; - Возврат ошибок API в формате JSON.
Регистрация обработчика ошибок в веб-приложении на Flask.
Зарегистрировать функцию-обработчик для модуля Flask, можно указав перед ней декоратор @app.errorhandler()
, или зарегистрировать обработчик, использовав функцию app.register_error_handler()
. Не забудьте установить код ошибки при возврате ответа.
# регистрируем обработчик `handle_bad_request()` декоратором @app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): return 'bad request!', 400 # регистрируем тот же обработчик без декоратора app.register_error_handler(400, handle_bad_request)
Подклассы HTTPException
, такие как BadRequest
и их HTTP-коды, взаимозаменяемы при регистрации обработчиков. (BadRequest.code == 400
)
Нестандартные HTTP-коды (такие как HTTP 507 Insufficient Storage
) нельзя зарегистрировать, так как они не известны модулю Werkzeug
. Для регистрации неизвестных HTTP-кодов определите подкласс werkzeug.exceptions.HTTPException
с соответствующим кодом, зарегистрируйте и где надо вернуть HTTP-код 507 Insufficient Storage
принудительно вызовите этот класс исключения при помощи инструкции raise
.
# создаем подкласс исключения HTTP 507 class InsufficientStorage(werkzeug.exceptions.HTTPException): code = 507 description = 'Not enough storage space.' # регистрируем HTTP 507 app.register_error_handler(InsufficientStorage, handle_507) # принудительно вызываем исключение `InsufficientStorage` raise InsufficientStorage()
Обработчики могут быть зарегистрированы для любого класса исключений, а не только для подклассов HTTPException
или кодов состояния HTTP. Обработчики могут быть зарегистрированы для определенного класса или для всех подклассов родительского класса.
Обработчики, зарегистрированные в blueprint
, имеют приоритет над обработчиками, зарегистрированными глобально в веб-приложении, при условии, что blueprint
(схема) обрабатывает запрос, вызывающий исключение. Однако blueprint
не может обрабатывать ошибки маршрутизации 404, так как ошибка 404 возникает на уровне маршрутизации до того, как можно определить схему blueprint
.
Универсальные обработчики исключений.
Можно зарегистрировать обработчики ошибок для очень общих базовых классов, таких как HTTPException
или даже Exception
, но имейте в виду, что они будут ловить все ошибки подряд (больше, чем можно ожидать) и в итоге получится одна страница ошибки на разные ситуации.
Например, обработчик ошибок для HTTPException
может быть полезен для преобразования страниц ошибок HTML по умолчанию в JSON. Но тогда этот обработчик будет запускать, например ошибки 404 и 405 во время маршрутизации. В общем будьте внимательны при создании универсальных обработчиков.
from flask import json from werkzeug.exceptions import HTTPException @app.errorhandler(HTTPException) def handle_exception(e): """Возвращает JSON вместо HTML для ошибок HTTP""" # сначала перехватываем ответ Flask для извлечения # правильных заголовков и кода состояния из ошибки response = e.get_response() # заменяем тело ответа сервера на JSON response.data = json.dumps({ "code": e.code, "name": e.name, "description": e.description, }) response.content_type = "application/json" # возвращаем ответ сервера return response
Обработчик ошибок для Exception
может !показаться! полезным для изменения способа представления пользователю всех ошибок, даже не перехваченных в коде. Другими словами: страница ошибки с одним и тем же HTTP-кодом для разных ситуаций (о чем говорилось выше). Исключение Exception
в Python фиксирует все необработанные ошибки, при этом будут включены все коды состояния HTTP.
Правильнее будет безопаснее зарегистрировать обработчики для более конкретных исключений, т.к. экземпляры HTTPException
являются действительными ответами WSGI.
from werkzeug.exceptions import HTTPException @app.errorhandler(Exception) def handle_exception(e): # исключаем ошибки HTTP if isinstance(e, HTTPException): # если это ошибка HTTP, то просто # возвращаем ее без изменений return e # в остальных случаях (ошибка кода веб-приложения) # генерируем страницу с ошибкой HTTP 500 return render_template("500_generic.html", e=e), 500
Обработчики ошибок по-прежнему соблюдают иерархию классов исключений. Если зарегистрировать обработчики как для HTTPException
, так и для Exception
, то обработчик Exception
не будет обрабатывать подклассы HTTPException
, т.к. он является более конкретным обработчиком HTTPException
.
Как Flask обрабатывает необработанные исключения?
Если код сайта на Flask во время работы ломается, то есть возникло исключение, для которого не зарегистрирован обработчик ошибок, то будет возвращена ошибка 500 Internal Server
Если для исключения InternalServerError
зарегистрирован обработчик ошибок, то будет вызван этот обработчик. Начиная с Flask 1.1.0, этому обработчику ошибок всегда будет передаваться экземпляр InternalServerError
, а не исходная не перехваченная ошибка. Исходная ошибка доступна как e.original_exception
.
Обработчику ошибок 500 Internal Server Error
будут передаваться неперехваченные исключения в дополнение к явным ошибкам 500. В режиме отладки обработчик 500 Internal Server Error
не используется, а показывается интерактивный отладчик.
Создание собственной страницы с HTTP-ошибкой 404.
Почти всегда при создании сайта на Flask необходимо потребоваться вызвать исключение HTTPException
, чтобы сообщить пользователю, что с запросом что-то не так. Фреймворк Flask поставляется с удобной функцией flask.abort()
, которая прерывает запрос со стандартной страницей HTTP-ошибки (только основное описание), зарегистрированной в модуле werkzeug
.
В зависимости от кода ошибки, вероятность того, что пользователь действительно увидит конкретную ошибку, меньше или больше.
Рассмотрим приведенный ниже код. Например, может быть маршрут профиля пользователя, и если пользователь не может передать имя пользователя, то можно выдать 400 Bad Request
. Если пользователь передает имя пользователя, а сайт не можем его найти, то выдаем сообщение 404 Not Found
.
from flask import abort, render_template, request # имя пользователя должно быть указано в параметрах запроса # успешный запрос будет похож на /profile?username=jack @app.route("/profile") def user_profile(): username = request.arg.get("username") # если имя пользователя не указано в запросе, # то вернем `400 Bad Request` if username is None: abort(400) user = get_user(username=username) # Если пользователь не наёден, то `404 not found` if user is None: abort(404) return render_template("profile.html", user=user)
Для того, что бы возвращалась страница 404 not found
с собственным дизайном, необходимо создать функцию обработчик:
from flask import render_template @app.errorhandler(404) def page_not_found(e): # в функцию `render_template()` передаем HTML-станицу с собственным # дизайном, а так же явно устанавливаем статус 404 return render_template('404.html'), 404
from flask import Flask, render_template # обработчик def page_not_found(e): return render_template('404.html'), 404 def create_app(config_filename): app = Flask(__name__) # регистрация обработчика app.register_error_handler(404, page_not_found) return app
Пример шаблона страницы с ошибкой 404.html
может быть таким:
{% extends "layout.html" %} {% block title %}Page Not Found{% endblock %} {% block body %} <h1>Page Not Found</h1> <h3>То, что вы искали, просто не существует.</h3> <p>Для продолжения перейдите <a href="{{ url_for('index') }}">на главную страницу сайта</a></p> {% endblock %}
Пример пользовательской страницы ошибки с кодом 500.
Приведенные выше примеры не на много улучшат страницы HTTP-ошибок по умолчанию. Так же можно создать собственный шаблон 500.html
следующим образом:
{% extends "layout.html" %} {% block title %}Internal Server Error{% endblock %} {% block body %} <h1>Internal Server Error</h1> <h3>Мы уже знаем об этой ошибке и делаем все возможное для ее устранения!</h3> <p>Приносим извинения за причлененные неудобства, скоро все заработает.</p> {% endblock %}
Создаем функцию обработчик HTTP-ошибок 500 Internal Server Error
:
from flask import render_template @app.errorhandler(500) def internal_server_error(e): # Обратите внимание, что необходимо # явно установить статус 500 return render_template('500.html'), 500
При использовании фабрик приложений:
from flask import Flask, render_template # обработчик def internal_server_error(e): return render_template('500.html'), 500 def create_app(): app = Flask(__name__) # регистрация обработчика app.register_error_handler(500, internal_server_error) return app
from flask import Blueprint blog = Blueprint('blog', __name__) # регистрация обработчика при помощи декоратора @blog.errorhandler(500) def internal_server_error(e): return render_template('500.html'), 500 # или с использованием метода `register_error_handler()` blog.register_error_handler(500, internal_server_error)
Особенности обработки ошибок в схемах blueprint
Flask.
В модульных приложениях с blueprint
большинство обработчиков ошибок будут работать должным образом, но есть предостережение относительно обработчиков исключений 404 и 405. Эти обработчики вызываются только из соответствующего оператора raise
или вызывают flask.abort()
в другой функции-представлении схемы blueprint
. Они не вызываются, например, из-за недействительного доступа к URL-адресу.
Это связано с тем, что blueprint
не принадлежит определенное пространство URL-адресов, поэтому экземпляр приложения не имеет возможности узнать, какой обработчик ошибок схемы (blueprint
) необходимо запустить, если указан недопустимый URL-адрес. Если необходимо использовать различные стратегии обработки этих ошибок на основе префиксов URL-адресов, то они могут быть определены на уровне приложения с помощью объекта прокси-сервера запроса flask.request
.
from flask import jsonify, render_template # на уровне всего веб-приложения # это не уровень определенной схемы blueprint @app.errorhandler(404) def page_not_found(e): # Если запрос находится в пространстве URL блога if request.path.startswith('/blog/'): # то возвращаем кастомную 404 ошибку для блога return render_template("blog/404.html"), 404 else: # в противном случае возвращаем # общую 404 ошибку для всего сайта return render_template("404.html"), 404 @app.errorhandler(405) def method_not_allowed(e): # Если в запросе указан неверный метод к API if request.path.startswith('/api/'): # возвращаем json с 405 HTTP-ошибкой return jsonify(message="Method Not Allowed"), 405 else: # в противном случае возвращаем # общую 405 ошибку для всего сайта return render_template("405.html"), 405
Возврат ошибок API в формате JSON
При создании API-интерфейсов во Flask некоторые разработчики понимают, что встроенные исключения недостаточно выразительны для API-интерфейсов и что тип содержимого text/html
, который они генерируют, не очень полезен для потребителей API.
Используя те же методы, что и выше плюс flask.jsonify()
, можно возвращать ответы JSON на ошибки API. Функция flask.abort()
вызывается с аргументом description
. Обработчик ошибок будет использовать это как сообщение об ошибке JSON и установит код состояния на 404.
from flask import abort, jsonify @app.errorhandler(404) def resource_not_found(e): return jsonify(error=str(e)), 404 @app.route("/cheese") def get_one_cheese(): resource = get_resource() if resource is None: abort(404, description="Resource not found") return jsonify(resource)
Можно создавать собственные классы исключений. Например, можно ввести новое настраиваемое исключение для API, которое будет принимать правильное удобочитаемое сообщение, код состояния для ошибки и некоторую дополнительную полезную информацию, чтобы дать больше конкретики для ошибки.
from flask import jsonify, request class InvalidAPIUsage(Exception): status_code = 400 def __init__(self, message, status_code=None, payload=None): super().__init__() self.message = message if status_code is not None: self.status_code = status_code self.payload = payload def to_dict(self): rv = dict(self.payload or ()) rv['message'] = self.message return rv @app.errorhandler(InvalidAPIUsage) def invalid_api_usage(e): return jsonify(e.to_dict()) # маршрут API для получения информации о пользователе # правильный запрос может быть /api/user?user_id=420 @app.route("/api/user") def user_api(user_id): user_id = request.arg.get("user_id") if not user_id: raise InvalidAPIUsage("No user id provided!") user = get_user(user_id=user_id) if not user: raise InvalidAPIUsage("No such user!", status_code=404) return jsonify(user.to_dict())
Теперь функция-представление может вызвать это исключение с сообщением об ошибке. Кроме того, дополнительная полезная информация может быть предоставлена в виде словаря через параметр payload
.