Skip to content

Commit

Permalink
feat(responders): raise a specialized subclass of HTTPNotFound (#1647)
Browse files Browse the repository at this point in the history
* feat(responders): Raise a specialized subclass of HTTPNotFound

When no route matches a request, raise a subclass of HTTPNotFound so that
a custom error handler can distinguish that specific case if desired.

* doc(faq): Remove extra spaces
  • Loading branch information
kgriffs authored and vytas7 committed Jan 25, 2020
1 parent a5652d9 commit c817a42
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 38 deletions.
@@ -0,0 +1,4 @@
When no route matches a request, the framework will now raise a
specialized subclass of :class:`~.falcon.HTTPNotFound`
(:class:`~.falcon.HTTPRouteNotFound`) so that
a custom error handler can distinguish that specific case if desired.
121 changes: 104 additions & 17 deletions docs/api/errors.rst
Expand Up @@ -87,20 +87,107 @@ Mixins
Predefined Errors
-----------------

.. automodule:: falcon
:noindex:
:members: HTTPBadRequest,
HTTPInvalidHeader, HTTPMissingHeader,
HTTPInvalidParam, HTTPMissingParam,
HTTPUnauthorized, HTTPForbidden, HTTPNotFound, HTTPMethodNotAllowed,
HTTPNotAcceptable, HTTPConflict, HTTPGone, HTTPLengthRequired,
HTTPPreconditionFailed, HTTPPayloadTooLarge, HTTPUriTooLong,
HTTPUnsupportedMediaType, HTTPRangeNotSatisfiable,
HTTPUnprocessableEntity, HTTPLocked, HTTPFailedDependency,
HTTPPreconditionRequired, HTTPTooManyRequests,
HTTPRequestHeaderFieldsTooLarge,
HTTPUnavailableForLegalReasons,
HTTPInternalServerError, HTTPNotImplemented, HTTPBadGateway,
HTTPServiceUnavailable, HTTPGatewayTimeout, HTTPVersionNotSupported,
HTTPInsufficientStorage, HTTPLoopDetected,
HTTPNetworkAuthenticationRequired
.. autoclass:: falcon.HTTPBadRequest
:members:

.. autoclass:: falcon.HTTPInvalidHeader
:members:

.. autoclass:: falcon.HTTPMissingHeader
:members:

.. autoclass:: falcon.HTTPInvalidParam
:members:

.. autoclass:: falcon.HTTPMissingParam
:members:

.. autoclass:: falcon.HTTPUnauthorized
:members:

.. autoclass:: falcon.HTTPForbidden
:members:

.. autoclass:: falcon.HTTPNotFound
:members:

.. autoclass:: falcon.HTTPRouteNotFound
:members:

.. autoclass:: falcon.HTTPMethodNotAllowed
:members:

.. autoclass:: falcon.HTTPNotAcceptable
:members:

.. autoclass:: falcon.HTTPConflict
:members:

.. autoclass:: falcon.HTTPGone
:members:

.. autoclass:: falcon.HTTPLengthRequired
:members:

.. autoclass:: falcon.HTTPPreconditionFailed
:members:

.. autoclass:: falcon.HTTPPayloadTooLarge
:members:

.. autoclass:: falcon.HTTPUriTooLong
:members:

.. autoclass:: falcon.HTTPUnsupportedMediaType
:members:

.. autoclass:: falcon.HTTPRangeNotSatisfiable
:members:

.. autoclass:: falcon.HTTPUnprocessableEntity
:members:

.. autoclass:: falcon.HTTPLocked
:members:

.. autoclass:: falcon.HTTPFailedDependency
:members:

.. autoclass:: falcon.HTTPPreconditionRequired
:members:

.. autoclass:: falcon.HTTPTooManyRequests
:members:

.. autoclass:: falcon.HTTPRequestHeaderFieldsTooLarge
:members:

.. autoclass:: falcon.HTTPUnavailableForLegalReasons
:members:

.. autoclass:: falcon.HTTPInternalServerError
:members:

.. autoclass:: falcon.HTTPNotImplemented
:members:

.. autoclass:: falcon.HTTPBadGateway
:members:

.. autoclass:: falcon.HTTPServiceUnavailable
:members:

.. autoclass:: falcon.HTTPGatewayTimeout
:members:

.. autoclass:: falcon.HTTPVersionNotSupported
:members:

.. autoclass:: falcon.HTTPInsufficientStorage
:members:

.. autoclass:: falcon.HTTPLoopDetected
:members:

.. autoclass:: falcon.HTTPNetworkAuthenticationRequired
:members:
16 changes: 11 additions & 5 deletions docs/api/routing.rst
Expand Up @@ -8,11 +8,6 @@ templates. If the path requested by the client matches the template for
a given route, the request is then passed on to the associated resource
for processing.

If no route matches the request, control then passes to a default
responder that simply raises an instance of :class:`~.HTTPNotFound`.
Normally this will result in sending a 404 response back to the
client.

Here's a quick example to show how all the pieces fit together:

.. code:: python
Expand Down Expand Up @@ -45,6 +40,17 @@ Here's a quick example to show how all the pieces fit together:
images = ImagesResource()
app.add_route('/images', images)
If no route matches the request, control then passes to a default
responder that simply raises an instance of
:class:`~.HTTPRouteNotFound`. By default, this error will be
rendered as a 404 response, but this behavior can be modified by
adding a custom error handler (see also
:ref:`this FAQ topic <faq_override_404_500_handlers>`).

On the other hand, if a route is matched but the resource does not
implement a responder for the requested HTTP method, the framework
invokes a default responder that raises an instance of
:class:`~.HTTPMethodNotAllowed`.

Default Router
--------------
Expand Down
17 changes: 9 additions & 8 deletions docs/user/faq.rst
Expand Up @@ -436,22 +436,23 @@ around:
def on_post(self, req, resp):
pass
.. _faq_override_404_500_handlers:

How can I write a custom handler for 404 and 500 pages in falcon?
------------------------------------------------------------------
When a route can not be found for an incoming request, Falcon uses a default
responder that simply raises an instance of :attr:`falcon.HTTPNotFound`. You
can use :meth:`falcon.App.add_error_handler` to register a custom error handler
for this exception type. Alternatively, you may be able to configure your web
server to transform the response for you (e.g., using Nginx's ``error_page``
directive).
responder that simply raises an instance of :class:`~.HTTPRouteNotFound`, which
the framework will in turn render as a 404 response. You can use
:meth:`falcon.App.add_error_handler` to override the default handler for this
exception type (or for its parent type, :class:`~.HTTPNotFound`).
Alternatively, you may be able to configure your web server to transform the
response for you (e.g., using nginx's ``error_page`` directive).

By default, non-system-exiting exceptions that do not inherit from
:class:`~.HTTPError` or :class:`~.HTTPStatus` are handled by Falcon with a
plain HTTP 500 error. To provide your own 500 logic, you can add a custom error
handler for Python's base :class:`Exception` type, though be aware that doing
so will also override the default handlers for :class:`~.HTTPError` and
:class:`~.HTTPStatus`.
handler for Python's base :class:`Exception` type. This will not affect the
default handlers for :class:`~.HTTPError` and :class:`~.HTTPStatus`.

See :ref:`errors` and the :meth:`falcon.API.add_error_handler` docs for more
details.
Expand Down
16 changes: 13 additions & 3 deletions falcon/app.py
Expand Up @@ -422,9 +422,19 @@ def add_route(self, uri_template, resource, **kwargs):
template for a given route, the request is then passed on to the
associated resource for processing.
If no route matches the request, control then passes to a
default responder that simply raises an instance of
:class:`~.HTTPNotFound`.
Note:
If no route matches the request, control then passes to a default
responder that simply raises an instance of
:class:`~.HTTPRouteNotFound`. By default, this error will be
rendered as a 404 response, but this behavior can be modified by
adding a custom error handler (see also
:ref:`this FAQ topic <faq_override_404_500_handlers>`).
On the other hand, if a route is matched but the resource does not
implement a responder for the requested HTTP method, the framework
invokes a default responder that raises an instance of
:class:`~.HTTPMethodNotAllowed`.
This method delegates to the configured router's ``add_route()``
method. To override the default behavior, pass a custom router
Expand Down
44 changes: 44 additions & 0 deletions falcon/errors.py
Expand Up @@ -282,6 +282,50 @@ def __init__(self, headers=None, **kwargs):
super(HTTPNotFound, self).__init__(status.HTTP_404, headers=headers, **kwargs)


class HTTPRouteNotFound(HTTPNotFound):
"""404 Not Found.
The request did not match any routes configured for the application.
This subclass of :class:`~.HTTPNotFound` is raised by the framework to
provide a default 404 response when no route matches the request. This
behavior can be customized by registering a custom error handler for
:class:`~.HTTPRouteNotFound`.
Keyword Args:
title (str): Human-friendly error title. If not provided, and
`description` is also not provided, no body will be included
in the response.
description (str): Human-friendly description of the error, along with
a helpful suggestion or two (default ``None``).
headers (dict or list): A ``dict`` of header names and values
to set, or a ``list`` of (*name*, *value*) tuples. Both *name* and
*value* must be of type ``str`` or ``StringType``, and only
character values 0x00 through 0xFF may be used on platforms that
use wide characters.
Note:
The Content-Type header, if present, will be overridden. If
you wish to return custom error messages, you can create
your own HTTP error class, and install an error handler
to convert it into an appropriate HTTP response for the
client
Note:
Falcon can process a list of ``tuple`` slightly faster
than a ``dict``.
href (str): A URL someone can visit to find out more information
(default ``None``). Unicode characters are percent-encoded.
href_text (str): If href is given, use this as the friendly
title/description for the link (default 'API documentation
for this error').
code (int): An internal code that customers can reference in their
support request or to help them when searching for knowledge
base articles related to this error (default ``None``).
"""
pass


class HTTPMethodNotAllowed(OptionalRepresentation, HTTPError):
"""405 Method Not Allowed.
Expand Down
10 changes: 5 additions & 5 deletions falcon/responders.py
Expand Up @@ -16,18 +16,18 @@

from falcon.errors import HTTPBadRequest
from falcon.errors import HTTPMethodNotAllowed
from falcon.errors import HTTPNotFound
from falcon.errors import HTTPRouteNotFound
from falcon.status_codes import HTTP_200


def path_not_found(req, resp, **kwargs):
"""Raise 404 HTTPNotFound error"""
raise HTTPNotFound()
"""Raise 404 HTTPRouteNotFound error"""
raise HTTPRouteNotFound()


async def path_not_found_async(req, resp, **kwargs):
"""Raise 404 HTTPNotFound error"""
raise HTTPNotFound()
"""Raise 404 HTTPRouteNotFound error"""
raise HTTPRouteNotFound()


def bad_request(req, resp, **kwargs):
Expand Down
23 changes: 23 additions & 0 deletions tests/test_error_handlers.py
Expand Up @@ -236,3 +236,26 @@ async def legacy_handler(err, rq, rs, prms):
with disable_asgi_non_coroutine_wrapping():
with pytest.raises(ValueError):
app.add_error_handler(Exception, capture_error)

def test_catch_http_no_route_error(self, asgi):
class Resource:
def on_get(self, req, resp):
raise falcon.HTTPNotFound()

def capture_error(req, resp, ex, params):
resp.body = ex.__class__.__name__
raise ex

app = create_app(asgi)
app.add_route('/', Resource())
app.add_error_handler(falcon.HTTPError, capture_error)

client = testing.TestClient(app)

result = client.simulate_get('/')
assert result.status_code == 404
assert result.text == 'HTTPNotFound'

result = client.simulate_get('/404')
assert result.status_code == 404
assert result.text == 'HTTPRouteNotFound'

0 comments on commit c817a42

Please sign in to comment.