From 55f860da2fce863a58d6ec9e78fd39ecec72d76a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Thu, 22 Jun 2017 18:11:23 +0200 Subject: [PATCH 01/10] Added support for 'Authorization: Bearer ' header in `Request.token` property. Also added a test case for that kind of header. --- sanic/request.py | 12 ++++++++---- tests/test_requests.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 3cc9c10b..29cb83f6 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -86,11 +86,15 @@ class Request(dict): :return: token related to request """ + prefixes = ('Token ', 'Bearer ') auth_header = self.headers.get('Authorization') - if auth_header is not None and 'Token ' in auth_header: - return auth_header.partition('Token ')[-1] - else: - return auth_header + + if auth_header is not None: + for prefix in prefixes: + if prefix in auth_header: + return auth_header.partition(prefix)[-1] + + return auth_header @property def form(self): diff --git a/tests/test_requests.py b/tests/test_requests.py index 2351a3b0..671febeb 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -180,6 +180,16 @@ def test_token(): request, response = app.test_client.get('/', headers=headers) + assert request.token == token + + token = 'a1d895e0-553a-421a-8e22-5ff8ecb48cbf' + headers = { + 'content-type': 'application/json', + 'Authorization': 'Bearer {}'.format(token) + } + + request, response = app.test_client.get('/', headers=headers) + assert request.token == token # no Authorization headers From f049a4ca678f21d5f03535b4353e1cf5ffa47b6c Mon Sep 17 00:00:00 2001 From: 7 Date: Thu, 22 Jun 2017 13:26:50 -0700 Subject: [PATCH 02/10] Recycling gunicorn worker (#800) * add recycling feature to gunicorn worker * add unit tests * add more unit tests, and remove redundant trigger_events call * fixed up unit tests * make flake8 happy * address feedbacks * make flake8 happy * add doc --- docs/sanic/deploying.md | 6 +++ sanic/server.py | 18 ++++++--- sanic/worker.py | 22 +++++++++-- tests/test_worker.py | 82 ++++++++++++++++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md index 391c10da..d3652d0d 100644 --- a/docs/sanic/deploying.md +++ b/docs/sanic/deploying.md @@ -57,6 +57,12 @@ for Gunicorn `worker-class` argument: gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker ``` +If your application suffers from memory leaks, you can configure Gunicorn to gracefully restart a worker +after it has processed a given number of requests. This can be a convenient way to help limit the effects +of the memory leak. + +See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information. + ## Asynchronous support This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`. However be advised that this method does not support using multiple processes, and is not the preferred way diff --git a/sanic/server.py b/sanic/server.py index f3106226..369e790e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -75,7 +75,7 @@ class HttpProtocol(asyncio.Protocol): signal=Signal(), connections=set(), request_timeout=60, request_max_size=None, request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, - **kwargs): + state=None, **kwargs): self.loop = loop self.transport = None self.request = None @@ -99,6 +99,9 @@ class HttpProtocol(asyncio.Protocol): self._request_handler_task = None self._request_stream_task = None self._keep_alive = keep_alive + self.state = state if state else {} + if 'requests_count' not in self.state: + self.state['requests_count'] = 0 @property def keep_alive(self): @@ -154,6 +157,9 @@ class HttpProtocol(asyncio.Protocol): self.headers = [] self.parser = HttpRequestParser(self) + # requests count + self.state['requests_count'] = self.state['requests_count'] + 1 + # Parse request chunk or close connection try: self.parser.feed_data(data) @@ -389,7 +395,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, register_sys_signals=True, run_async=False, connections=None, signal=Signal(), request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, websocket_max_size=None, - websocket_max_queue=None): + websocket_max_queue=None, state=None): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -427,8 +433,6 @@ def serve(host, port, request_handler, error_handler, before_start=None, if debug: loop.set_debug(debug) - trigger_events(before_start, loop) - connections = connections if connections is not None else set() server = partial( protocol, @@ -445,7 +449,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, is_request_stream=is_request_stream, router=router, websocket_max_size=websocket_max_size, - websocket_max_queue=websocket_max_queue + websocket_max_queue=websocket_max_queue, + state=state ) server_coroutine = loop.create_server( @@ -457,6 +462,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, sock=sock, backlog=backlog ) + # Instead of pulling time at the end of every request, # pull it once per minute loop.call_soon(partial(update_current_time, loop)) @@ -464,6 +470,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, if run_async: return server_coroutine + trigger_events(before_start, loop) + try: http_server = loop.run_until_complete(server_coroutine) except: diff --git a/sanic/worker.py b/sanic/worker.py index 1d3e384b..876354ce 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -29,7 +29,7 @@ class GunicornWorker(base.Worker): self.ssl_context = self._create_ssl_context(cfg) else: self.ssl_context = None - self.servers = [] + self.servers = {} self.connections = set() self.exit_code = 0 self.signal = Signal() @@ -96,11 +96,16 @@ class GunicornWorker(base.Worker): async def _run(self): for sock in self.sockets: - self.servers.append(await serve( + state = dict(requests_count=0) + self._server_settings["host"] = None + self._server_settings["port"] = None + server = await serve( sock=sock, connections=self.connections, + state=state, **self._server_settings - )) + ) + self.servers[server] = state async def _check_alive(self): # If our parent changed then we shut down. @@ -109,7 +114,15 @@ class GunicornWorker(base.Worker): while self.alive: self.notify() - if pid == os.getpid() and self.ppid != os.getppid(): + req_count = sum( + self.servers[srv]["requests_count"] for srv in self.servers + ) + if self.max_requests and req_count > self.max_requests: + self.alive = False + self.log.info( + "Max requests exceeded, shutting down: %s", self + ) + elif pid == os.getpid() and self.ppid != os.getppid(): self.alive = False self.log.info("Parent changed, shutting down: %s", self) else: @@ -166,3 +179,4 @@ class GunicornWorker(base.Worker): self.alive = False self.exit_code = 1 self.cfg.worker_abort(self) + sys.exit(1) diff --git a/tests/test_worker.py b/tests/test_worker.py index 2c1a0123..e1a13368 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -3,7 +3,11 @@ import json import shlex import subprocess import urllib.request - +from unittest import mock +from sanic.worker import GunicornWorker +from sanic.app import Sanic +import asyncio +import logging import pytest @@ -20,3 +24,79 @@ def test_gunicorn_worker(gunicorn_worker): with urllib.request.urlopen('http://localhost:1337/') as f: res = json.loads(f.read(100).decode()) assert res['test'] + + +class GunicornTestWorker(GunicornWorker): + + def __init__(self): + self.app = mock.Mock() + self.app.callable = Sanic("test_gunicorn_worker") + self.servers = {} + self.exit_code = 0 + self.cfg = mock.Mock() + self.notify = mock.Mock() + + +@pytest.fixture +def worker(): + return GunicornTestWorker() + + +def test_worker_init_process(worker): + with mock.patch('sanic.worker.asyncio') as mock_asyncio: + try: + worker.init_process() + except TypeError: + pass + + assert mock_asyncio.get_event_loop.return_value.close.called + assert mock_asyncio.new_event_loop.called + assert mock_asyncio.set_event_loop.called + + +def test_worker_init_signals(worker): + worker.loop = mock.Mock() + worker.init_signals() + assert worker.loop.add_signal_handler.called + + +def test_handle_abort(worker): + with mock.patch('sanic.worker.sys') as mock_sys: + worker.handle_abort(object(), object()) + assert not worker.alive + assert worker.exit_code == 1 + mock_sys.exit.assert_called_with(1) + + +def test_handle_quit(worker): + worker.handle_quit(object(), object()) + assert not worker.alive + assert worker.exit_code == 0 + + +def test_run_max_requests_exceeded(worker): + loop = asyncio.new_event_loop() + worker.ppid = 1 + worker.alive = True + sock = mock.Mock() + sock.cfg_addr = ('localhost', 8080) + worker.sockets = [sock] + worker.wsgi = mock.Mock() + worker.connections = set() + worker.log = mock.Mock() + worker.loop = loop + worker.servers = { + "server1": {"requests_count": 14}, + "server2": {"requests_count": 15}, + } + worker.max_requests = 10 + worker._run = mock.Mock(wraps=asyncio.coroutine(lambda *a, **kw: None)) + + # exceeding request count + _runner = asyncio.ensure_future(worker._check_alive(), loop=loop) + loop.run_until_complete(_runner) + + assert worker.alive == False + worker.notify.assert_called_with() + worker.log.info.assert_called_with("Max requests exceeded, shutting down: %s", + worker) From cf1713b08561629080537309f7394ad1572226e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 16:12:15 +0200 Subject: [PATCH 03/10] Added a Unauthorized exception. Also added a few tests related to this new exception. --- sanic/exceptions.py | 28 ++++++++++++++++++++++++++++ tests/test_exceptions.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index e1136dd1..1cf28a8e 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -198,6 +198,34 @@ class InvalidRangeType(ContentRangeError): pass +@add_status_code(401) +class Unauthorized(SanicException): + """ + Unauthorized exception (401 HTTP status code). + + :param scheme: Name of the authentication scheme to be used. + :param realm: Description of the protected area. (optional) + :param others: A dict containing values to add to the WWW-Authenticate + header that is generated. This is especially useful when dealing with the + Digest scheme. (optional) + """ + pass + + def __init__(self, message, scheme, realm="", others=None): + super().__init__(message) + + adds = "" + + if others is not None: + values = ["{!s}={!r}".format(k, v) for k, v in others.items()] + adds = ', '.join(values) + adds = ', {}'.format(adds) + + self.headers = { + "WWW-Authenticate": "{} realm='{}'{}".format(scheme, realm, adds) + } + + def abort(status_code, message=None): """ Raise an exception based on SanicException. Returns the HTTP response diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index a2b8dc71..4330ef57 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -3,7 +3,8 @@ from bs4 import BeautifulSoup from sanic import Sanic from sanic.response import text -from sanic.exceptions import InvalidUsage, ServerError, NotFound, abort +from sanic.exceptions import InvalidUsage, ServerError, NotFound, Unauthorized +from sanic.exceptions import abort class SanicExceptionTestException(Exception): @@ -26,6 +27,20 @@ def exception_app(): def handler_404(request): raise NotFound("OK") + @app.route('/401/basic') + def handler_401_basic(request): + raise Unauthorized("Unauthorized", "Basic", "Sanic") + + @app.route('/401/digest') + def handler_401_digest(request): + challenge = { + "qop": "auth, auth-int", + "algorithm": "MD5", + "nonce": "abcdef", + "opaque": "zyxwvu", + } + raise Unauthorized("Unauthorized", "Digest", "Sanic", challenge) + @app.route('/invalid') def handler_invalid(request): raise InvalidUsage("OK") @@ -49,8 +64,10 @@ def exception_app(): return app + def test_catch_exception_list(): app = Sanic('exception_list') + @app.exception([SanicExceptionTestException, NotFound]) def exception_list(request, exception): return text("ok") @@ -91,6 +108,24 @@ def test_not_found_exception(exception_app): assert response.status == 404 +def test_unauthorized_exception(exception_app): + """Test the built-in Unauthorized exception""" + request, response = exception_app.test_client.get('/401/basic') + assert response.status == 401 + assert response.headers.get('WWW-Authenticate') is not None + assert response.headers.get('WWW-Authenticate') == "Basic realm='Sanic'" + + request, response = exception_app.test_client.get('/401/digest') + assert response.status == 401 + + auth_header = response.headers.get('WWW-Authenticate') + expected = ("Digest realm='Sanic', qop='auth, auth-int', algorithm='MD5', " + "nonce='abcdef', opaque='zyxwvu'") + + assert auth_header is not None + assert auth_header == expected + + def test_handled_unhandled_exception(exception_app): """Test that an exception not built into sanic is handled""" request, response = exception_app.test_client.get('/divide_by_zero') From 9fcdacb62405cc2e1ae63a73a3be74d3fa926083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 16:29:04 +0200 Subject: [PATCH 04/10] Modified the name of an argument. --- sanic/exceptions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 1cf28a8e..95e41b4a 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -205,19 +205,19 @@ class Unauthorized(SanicException): :param scheme: Name of the authentication scheme to be used. :param realm: Description of the protected area. (optional) - :param others: A dict containing values to add to the WWW-Authenticate + :param challenge: A dict containing values to add to the WWW-Authenticate header that is generated. This is especially useful when dealing with the Digest scheme. (optional) """ pass - def __init__(self, message, scheme, realm="", others=None): + def __init__(self, message, scheme, realm="", challenge=None): super().__init__(message) adds = "" - if others is not None: - values = ["{!s}={!r}".format(k, v) for k, v in others.items()] + if challenge is not None: + values = ["{!s}={!r}".format(k, v) for k, v in challenge.items()] adds = ', '.join(values) adds = ', {}'.format(adds) From 60aa60f48ea2536ac95ad61276dfa4ab64195a92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20KUBLER?= Date: Fri, 23 Jun 2017 17:16:31 +0200 Subject: [PATCH 05/10] Fixed the test for the new Unauthorized exception. --- tests/test_exceptions.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 4330ef57..dcdecabd 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -119,11 +119,12 @@ def test_unauthorized_exception(exception_app): assert response.status == 401 auth_header = response.headers.get('WWW-Authenticate') - expected = ("Digest realm='Sanic', qop='auth, auth-int', algorithm='MD5', " - "nonce='abcdef', opaque='zyxwvu'") - assert auth_header is not None - assert auth_header == expected + assert auth_header.startswith('Digest') + assert "qop='auth, auth-int'" in auth_header + assert "algorithm='MD5'" in auth_header + assert "nonce='abcdef'" in auth_header + assert "opaque='zyxwvu'" in auth_header def test_handled_unhandled_exception(exception_app): From dc5a70b0de2abdbb1943450112c7f7607068806a Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Mon, 26 Jun 2017 21:05:23 +0900 Subject: [PATCH 06/10] Introduce debug mode for HTTP protocol --- sanic/server.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 369e790e..fd31680e 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -75,7 +75,7 @@ class HttpProtocol(asyncio.Protocol): signal=Signal(), connections=set(), request_timeout=60, request_max_size=None, request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, - state=None, **kwargs): + state=None, debug=False, **kwargs): self.loop = loop self.transport = None self.request = None @@ -102,12 +102,14 @@ class HttpProtocol(asyncio.Protocol): self.state = state if state else {} if 'requests_count' not in self.state: self.state['requests_count'] = 0 + self._debug = debug @property def keep_alive(self): - return (self._keep_alive - and not self.signal.stopped - and self.parser.should_keep_alive()) + return ( + self._keep_alive and + not self.signal.stopped and + self.parser.should_keep_alive()) # -------------------------------------------- # # Connection @@ -164,7 +166,10 @@ class HttpProtocol(asyncio.Protocol): try: self.parser.feed_data(data) except HttpParserError: - exception = InvalidUsage('Bad Request') + message = 'Bad Request' + if self._debug: + message += '\n' + traceback.format_exc() + exception = InvalidUsage(message) self.write_error(exception) def on_url(self, url): @@ -450,7 +455,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, router=router, websocket_max_size=websocket_max_size, websocket_max_queue=websocket_max_queue, - state=state + state=state, + debug=debug, ) server_coroutine = loop.create_server( From ad8e1cbf62db2f6d3b45e520dcb8a6c6b8933b3f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 20:49:41 -0700 Subject: [PATCH 07/10] convert environment vars to int if digits --- sanic/config.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/config.py b/sanic/config.py index e3563bc1..b51f4d0c 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,4 +201,7 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - self[config_key] = v + if v.isdigit(): + self[config_key] = int(v) + else: + self[config_key] = v From 4379a4b0670c9172a3a6af63aa7d0132142c1989 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 20:58:31 -0700 Subject: [PATCH 08/10] float logic --- sanic/config.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index b51f4d0c..ec4f9bf3 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,7 +201,11 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - if v.isdigit(): - self[config_key] = int(v) + # This is a float or an int + if v.replace('.', '').isdigit(): + if '.' in v: + self[config_key] = float(v) + else: + self[config_key] = int(v) else: self[config_key] = v From 395d85a12f9b2be24dce112a6a4b0e873374de7f Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Jun 2017 21:26:34 -0700 Subject: [PATCH 09/10] use try/except --- sanic/config.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index ec4f9bf3..f5649cfe 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -201,11 +201,10 @@ class Config(dict): for k, v in os.environ.items(): if k.startswith(SANIC_PREFIX): _, config_key = k.split(SANIC_PREFIX, 1) - # This is a float or an int - if v.replace('.', '').isdigit(): - if '.' in v: + try: + self[config_key] = int(v) + except ValueError: + try: self[config_key] = float(v) - else: - self[config_key] = int(v) - else: - self[config_key] = v + except ValueError: + self[config_key] = v From 412ffd15923bad98d36d2bc344bf08656cacb303 Mon Sep 17 00:00:00 2001 From: Jonas Obrist Date: Wed, 28 Jun 2017 11:05:59 +0900 Subject: [PATCH 10/10] Added a warning to the cookies documentation about security --- docs/sanic/cookies.md | 75 ------------------------------------ docs/sanic/cookies.rst | 87 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 75 deletions(-) delete mode 100644 docs/sanic/cookies.md create mode 100644 docs/sanic/cookies.rst diff --git a/docs/sanic/cookies.md b/docs/sanic/cookies.md deleted file mode 100644 index e71bcc47..00000000 --- a/docs/sanic/cookies.md +++ /dev/null @@ -1,75 +0,0 @@ -# Cookies - -Cookies are pieces of data which persist inside a user's browser. Sanic can -both read and write cookies, which are stored as key-value pairs. - -## Reading cookies - -A user's cookies can be accessed via the `Request` object's `cookies` dictionary. - -```python -from sanic.response import text - -@app.route("/cookie") -async def test(request): - test_cookie = request.cookies.get('test') - return text("Test cookie set to: {}".format(test_cookie)) -``` - -## Writing cookies - -When returning a response, cookies can be set on the `Response` object. - -```python -from sanic.response import text - -@app.route("/cookie") -async def test(request): - response = text("There's a cookie up in this response") - response.cookies['test'] = 'It worked!' - response.cookies['test']['domain'] = '.gotta-go-fast.com' - response.cookies['test']['httponly'] = True - return response -``` - -## Deleting cookies - -Cookies can be removed semantically or explicitly. - -```python -from sanic.response import text - -@app.route("/cookie") -async def test(request): - response = text("Time to eat some cookies muahaha") - - # This cookie will be set to expire in 0 seconds - del response.cookies['kill_me'] - - # This cookie will self destruct in 5 seconds - response.cookies['short_life'] = 'Glad to be here' - response.cookies['short_life']['max-age'] = 5 - del response.cookies['favorite_color'] - - # This cookie will remain unchanged - response.cookies['favorite_color'] = 'blue' - response.cookies['favorite_color'] = 'pink' - del response.cookies['favorite_color'] - - return response -``` - -Response cookies can be set like dictionary values and have the following -parameters available: - -- `expires` (datetime): The time for the cookie to expire on the - client's browser. -- `path` (string): The subset of URLs to which this cookie applies. Defaults to /. -- `comment` (string): A comment (metadata). -- `domain` (string): Specifies the domain for which the cookie is valid. An - explicitly specified domain must always start with a dot. -- `max-age` (number): Number of seconds the cookie should live for. -- `secure` (boolean): Specifies whether the cookie will only be sent via - HTTPS. -- `httponly` (boolean): Specifies whether the cookie cannot be read by - Javascript. diff --git a/docs/sanic/cookies.rst b/docs/sanic/cookies.rst new file mode 100644 index 00000000..c4e0c0a1 --- /dev/null +++ b/docs/sanic/cookies.rst @@ -0,0 +1,87 @@ +Cookies +======= + +Cookies are pieces of data which persist inside a user's browser. Sanic can +both read and write cookies, which are stored as key-value pairs. + +.. warning:: + + Cookies can be freely altered by the client. Therefore you cannot just store + data such as login information in cookies as-is, as they can be freely altered + by the client. To ensure data you store in cookies is not forged or tampered + with by the client, use something like `itsdangerous`_ to cryptographically + sign the data. + + +Reading cookies +--------------- + +A user's cookies can be accessed via the ``Request`` object's ``cookies`` dictionary. + +.. code-block:: python + + from sanic.response import text + + @app.route("/cookie") + async def test(request): + test_cookie = request.cookies.get('test') + return text("Test cookie set to: {}".format(test_cookie)) + +Writing cookies +--------------- + +When returning a response, cookies can be set on the ``Response`` object. + +.. code-block:: python + + from sanic.response import text + + @app.route("/cookie") + async def test(request): + response = text("There's a cookie up in this response") + response.cookies['test'] = 'It worked!' + response.cookies['test']['domain'] = '.gotta-go-fast.com' + response.cookies['test']['httponly'] = True + return response + +Deleting cookies +---------------- + +Cookies can be removed semantically or explicitly. + +.. code-block:: python + + from sanic.response import text + + @app.route("/cookie") + async def test(request): + response = text("Time to eat some cookies muahaha") + + # This cookie will be set to expire in 0 seconds + del response.cookies['kill_me'] + + # This cookie will self destruct in 5 seconds + response.cookies['short_life'] = 'Glad to be here' + response.cookies['short_life']['max-age'] = 5 + del response.cookies['favorite_color'] + + # This cookie will remain unchanged + response.cookies['favorite_color'] = 'blue' + response.cookies['favorite_color'] = 'pink' + del response.cookies['favorite_color'] + + return response + +Response cookies can be set like dictionary values and have the following +parameters available: + +- ``expires`` (datetime): The time for the cookie to expire on the client's browser. +- ``path`` (string): The subset of URLs to which this cookie applies. Defaults to /. +- ``comment`` (string): A comment (metadata). +- ``domain`` (string): Specifies the domain for which the cookie is valid. An + explicitly specified domain must always start with a dot. +- ``max-age`` (number): Number of seconds the cookie should live for. +- ``secure`` (boolean): Specifies whether the cookie will only be sent via HTTPS. +- ``httponly`` (boolean): Specifies whether the cookie cannot be read by Javascript. + +.. _itsdangerous: https://pythonhosted.org/itsdangerous/ \ No newline at end of file