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)