diff --git a/docs/cookies.md b/docs/cookies.md index c29a1f32..5ec54a03 100644 --- a/docs/cookies.md +++ b/docs/cookies.md @@ -32,12 +32,39 @@ async def test(request): 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. +- `path` (string): The subset of URLs to which this cookie applies. Defaults to 0. - `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. diff --git a/sanic/cookies.py b/sanic/cookies.py index b7669e76..27b85bc9 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -42,8 +42,9 @@ class CookieJar(dict): """ CookieJar dynamically writes headers as cookies are added and removed It gets around the limitation of one header per name by using the - MultiHeader class to provide a unique key that encodes to Set-Cookie + MultiHeader class to provide a unique key that encodes to Set-Cookie. """ + def __init__(self, headers): super().__init__() self.headers = headers @@ -54,6 +55,7 @@ class CookieJar(dict): cookie_header = self.cookie_headers.get(key) if not cookie_header: cookie = Cookie(key, value) + cookie['path'] = '/' cookie_header = MultiHeader("Set-Cookie") self.cookie_headers[key] = cookie_header self.headers[cookie_header] = cookie @@ -62,8 +64,14 @@ class CookieJar(dict): self[key].value = value def __delitem__(self, key): - del self.cookie_headers[key] - return super().__delitem__(key) + if key not in self.cookie_headers: + self[key] = '' + self[key]['max-age'] = 0 + else: + cookie_header = self.cookie_headers[key] + del self.headers[cookie_header] + del self.cookie_headers[key] + return super().__delitem__(key) class Cookie(dict): diff --git a/tests/test_cookies.py b/tests/test_cookies.py index cf6a4259..571c01a3 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -3,6 +3,7 @@ from http.cookies import SimpleCookie from sanic import Sanic from sanic.response import json, text from sanic.utils import sanic_endpoint_test +import pytest # ------------------------------------------------------------ # @@ -54,4 +55,23 @@ def test_cookie_options(): response_cookies.load(response.headers.get('Set-Cookie', {})) assert response_cookies['test'].value == 'at you' - assert response_cookies['test']['httponly'] == True \ No newline at end of file + assert response_cookies['test']['httponly'] == True + +def test_cookie_deletion(): + app = Sanic('test_text') + + @app.route('/') + def handler(request): + response = text("OK") + del response.cookies['i_want_to_die'] + response.cookies['i_never_existed'] = 'testing' + del response.cookies['i_never_existed'] + return response + + request, response = sanic_endpoint_test(app) + response_cookies = SimpleCookie() + response_cookies.load(response.headers.get('Set-Cookie', {})) + + assert int(response_cookies['i_want_to_die']['max-age']) == 0 + with pytest.raises(KeyError): + hold_my_beer = response.cookies['i_never_existed'] \ No newline at end of file