From b213399473339c7ca4000a4dffeabb05947cc66a Mon Sep 17 00:00:00 2001 From: Kurt Griffiths Date: Tue, 1 Sep 2015 22:10:22 -0500 Subject: [PATCH] feat: Add redirection support via raising subclasses of HTTPStatus Define a set of HTTPStatus subclasses that can be raised to perform various types of HTTP redirects. This should avoid the problem of hooks and responder methods possibly overriding the redirect. Raising an instance of one of these classes and will short-circuit request processing similar to raising an instance of HTTPError. Specifically, if raised in a before hook, it will skip any remaining hooks and the responder method, but will not skip any process_response middleware methods. If raised within a responder, it will skip the rest of the responder and all after hooks. If raised in an after hook, it would skip remaining after hooks but not middleware methods. And finally, if raised within a middleware method, execution would perceive as described at the bottom of [1]. The above behavior is inherited from HTTPStatus and so is not re-tested in the subclasses. [1]: https://falcon.readthedocs.org/en/stable/api/middleware.html Closes #406 --- doc/api/index.rst | 3 +- doc/api/redirects.rst | 25 +++++++ falcon/__init__.py | 1 + falcon/http_status.py | 5 +- falcon/redirects.py | 146 ++++++++++++++++++++++++++++++++++++++++ falcon/status_codes.py | 2 + tests/test_redirects.py | 49 ++++++++++++++ 7 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 doc/api/redirects.rst create mode 100644 falcon/redirects.py create mode 100644 tests/test_redirects.py diff --git a/doc/api/index.rst b/doc/api/index.rst index 3d07c75..b0f4b20 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -9,7 +9,8 @@ Classes and Functions cookies status errors + redirects middleware hooks routing - util \ No newline at end of file + util diff --git a/doc/api/redirects.rst b/doc/api/redirects.rst new file mode 100644 index 0000000..bae8a9e --- /dev/null +++ b/doc/api/redirects.rst @@ -0,0 +1,25 @@ +.. _redirects: + +Redirection +=========== + +Falcon defines a set of exceptions that can be raised within a +middleware method, hook, or responder in order to trigger +a 3xx (Redirection) response to the client. Raising one of these +classes short-circuits request processing in a manner similar to +raising an instance or subclass of :py:class:`~.HTTPError` + + +Base Class +---------- + +.. autoclass:: falcon.http_status.HTTPStatus + :members: + + +Redirects +--------- + +.. automodule:: falcon + :members: HTTPMovedPermanently, HTTPFound, HTTPSeeOther, + HTTPTemporaryRedirect, HTTPPermanentRedirect diff --git a/falcon/__init__.py b/falcon/__init__.py index 530e666..92ecb20 100644 --- a/falcon/__init__.py +++ b/falcon/__init__.py @@ -32,6 +32,7 @@ from falcon.version import __version__ # NOQA from falcon.api import API, DEFAULT_MEDIA_TYPE # NOQA from falcon.status_codes import * # NOQA from falcon.errors import * # NOQA +from falcon.redirects import * # NOQA from falcon.http_error import HTTPError # NOQA from falcon.util import * # NOQA from falcon.hooks import before, after # NOQA diff --git a/falcon/http_status.py b/falcon/http_status.py index fb2733a..dd75fc7 100644 --- a/falcon/http_status.py +++ b/falcon/http_status.py @@ -16,8 +16,9 @@ class HTTPStatus(Exception): """Represents a generic HTTP status. - Raise this class from a hook, middleware, or a responder to stop handling - the request and skip to the response handling. + Raise an instance of this class from a hook, middleware, or + responder to short-circuit request processing in a manner similar + to ``falcon.HTTPError``, but for non-error status codes. Attributes: status (str): HTTP status line, e.g. '748 Confounded by Ponies'. diff --git a/falcon/redirects.py b/falcon/redirects.py new file mode 100644 index 0000000..d6e8bed --- /dev/null +++ b/falcon/redirects.py @@ -0,0 +1,146 @@ +# Copyright 2015 by Kurt Griffiths +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import falcon +from falcon.http_status import HTTPStatus + + +class HTTPMovedPermanently(HTTPStatus): + """301 Moved Permanently. + + The 301 (Moved Permanently) status code indicates that the target + resource has been assigned a new permanent URI. + + Note: + For historical reasons, a user agent MAY change the request + method from POST to GET for the subsequent request. If this + behavior is undesired, the 308 (Permanent Redirect) status code + can be used instead. + + See also: https://tools.ietf.org/html/rfc7231#section-6.4.2 + + Args: + location (str): URI to provide as the Location header in the + response. + """ + + def __init__(self, location): + super(HTTPMovedPermanently, self).__init__( + falcon.HTTP_301, {'location': location}) + + +class HTTPFound(HTTPStatus): + """302 Found. + + The 302 (Found) status code indicates that the target resource + resides temporarily under a different URI. Since the redirection + might be altered on occasion, the client ought to continue to use the + effective request URI for future requests. + + Note: + For historical reasons, a user agent MAY change the request + method from POST to GET for the subsequent request. If this + behavior is undesired, the 307 (Temporary Redirect) status code + can be used instead. + + See also: https://tools.ietf.org/html/rfc7231#section-6.4.3 + + Args: + location (str): URI to provide as the Location header in the + response. + """ + + def __init__(self, location): + super(HTTPFound, self).__init__( + falcon.HTTP_302, {'location': location}) + + +class HTTPSeeOther(HTTPStatus): + """303 See Other. + + The 303 (See Other) status code indicates that the server is + redirecting the user agent to a *different* resource, as indicated + by a URI in the Location header field, which is intended to provide + an indirect response to the original request. + + A 303 response to a GET request indicates that the origin server + does not have a representation of the target resource that can be + transferred over HTTP. However, the Location header in the response + may be dereferenced to obtain a representation for an alternative + resource. The recipient may find this alternative useful, even + though it does not represent the original target resource. + + Note: + The new URI in the Location header field is not considered + equivalent to the effective request URI. + + See also: https://tools.ietf.org/html/rfc7231#section-6.4.4 + + Args: + location (str): URI to provide as the Location header in the + response. + """ + + def __init__(self, location): + super(HTTPSeeOther, self).__init__( + falcon.HTTP_303, {'location': location}) + + +class HTTPTemporaryRedirect(HTTPStatus): + """307 Temporary Redirect. + + The 307 (Temporary Redirect) status code indicates that the target + resource resides temporarily under a different URI and the user + agent MUST NOT change the request method if it performs an automatic + redirection to that URI. Since the redirection can change over + time, the client ought to continue using the original effective + request URI for future requests. + + Note: + This status code is similar to 302 (Found), except that it + does not allow changing the request method from POST to GET. + + See also: https://tools.ietf.org/html/rfc7231#section-6.4.7 + + Args: + location (str): URI to provide as the Location header in the + response. + """ + + def __init__(self, location): + super(HTTPTemporaryRedirect, self).__init__( + falcon.HTTP_307, {'location': location}) + + +class HTTPPermanentRedirect(HTTPStatus): + """308 Permanent Redirect. + + The 308 (Permanent Redirect) status code indicates that the target + resource has been assigned a new permanent URI. + + Note: + This status code is similar to 301 (Moved Permanently), except + that it does not allow changing the request method from POST to + GET. + + See also: https://tools.ietf.org/html/rfc7238#section-3 + + Args: + location (str): URI to provide as the Location header in the + response. + """ + + def __init__(self, location): + super(HTTPPermanentRedirect, self).__init__( + falcon.HTTP_308, {'location': location}) diff --git a/falcon/status_codes.py b/falcon/status_codes.py index 6b9dda2..f0c87a4 100644 --- a/falcon/status_codes.py +++ b/falcon/status_codes.py @@ -49,6 +49,8 @@ HTTP_305 = '305 Use Proxy' HTTP_USE_PROXY = HTTP_305 HTTP_307 = '307 Temporary Redirect' HTTP_TEMPORARY_REDIRECT = HTTP_307 +HTTP_308 = '308 Permanent Redirect' +HTTP_PERMANENT_REDIRECT = HTTP_308 HTTP_400 = '400 Bad Request' HTTP_BAD_REQUEST = HTTP_400 diff --git a/tests/test_redirects.py b/tests/test_redirects.py new file mode 100644 index 0000000..13291ec --- /dev/null +++ b/tests/test_redirects.py @@ -0,0 +1,49 @@ +import ddt + +import falcon.testing as testing +import falcon + + +class RedirectingResource(object): + + # NOTE(kgriffs): You wouldn't necessarily use these types of + # http methods with these types of redirects; this is only + # done to simplify testing. + + def on_get(self, req, resp): + raise falcon.HTTPMovedPermanently('/moved/perm') + + def on_post(self, req, resp): + raise falcon.HTTPFound('/found') + + def on_put(self, req, resp): + raise falcon.HTTPSeeOther('/see/other') + + def on_delete(self, req, resp): + raise falcon.HTTPTemporaryRedirect('/tmp/redirect') + + def on_head(self, req, resp): + raise falcon.HTTPPermanentRedirect('/perm/redirect') + + +@ddt.ddt +class TestRedirects(testing.TestBase): + + def before(self): + self.api.add_route('/', RedirectingResource()) + + @ddt.data( + ('GET', falcon.HTTP_301, '/moved/perm'), + ('POST', falcon.HTTP_302, '/found'), + ('PUT', falcon.HTTP_303, '/see/other'), + ('DELETE', falcon.HTTP_307, '/tmp/redirect'), + ('HEAD', falcon.HTTP_308, '/perm/redirect'), + ) + @ddt.unpack + def test_redirect(self, method, expected_status, expected_location): + result = self.simulate_request('/', method=method) + + self.assertEqual(result, []) + self.assertEqual(self.srmock.status, expected_status) + self.assertEqual(self.srmock.headers_dict['location'], + expected_location)