Repository: sanic-org/sanic

Issue:
using AGSI the Set-Cookie header is send twice, one correct, one wrong
**Describe the bug**
When using a AGSI server (unicorn) the Set-Cookie header is send twice, one time correctly formatted, one time formatted as a json dict. See curl response below.

```
HTTP/1.1 200 OK
date: Sat, 06 Jul 2019 20:01:13 GMT
server: uvicorn
set-cookie: {'path': '/', 'httponly': True}
content-length: 36
set-cookie: test="It worked!"; Path=/; HttpOnly
```


**Code snippet**
```
import logging

from sanic import Sanic
from sanic.response import json, redirect, text, html, stream, raw

logger = logging.getLogger(__name__)
app = Sanic(__name__)

@app.get("/test", name='api')
async def cookie_test(request):
    response = text("There's a cookie up in this response")
    response.cookies['test'] = 'It worked!'
    response.cookies['test']['httponly'] = True
    
    logger.info(response.headers)
    
    return response

def main():
    uvicorn.run(app, host="0.0.0.0", port=5000, debug=True, access_log=False)

```


**Expected behavior**
The first Set-Cookie header should not be send.
```
HTTP/1.1 200 OK
date: Sat, 06 Jul 2019 20:01:13 GMT
server: uvicorn
content-length: 36
set-cookie: test="It worked!"; Path=/; HttpOnly
```


**Environment (please complete the following information):**
 - OS: Lunix
 - Version: 19.6.0


**Additional context**
The problem seems to happen when the headers are turned into  bytes in the sanic AGSI module. The Cookies are already in de response headers because the CookieJar is adding the cookies to the headers. When `str()` is called on the Cookie object the object in converted to a json string.

[L293-L296](https://github.com/huge-success/sanic/blob/master/sanic/asgi.py#L293-L296)

**A possible solution**
First asume that the header value is a string or a other object with a `encode` function if not convert it to a string. This is similair to the `_parse_headers` function in the response class. 

[_parse_headers (L32-L46)](https://github.com/huge-success/sanic/blob/master/sanic/response.py#L32-L46)

If this is implemented it won't be needed to add the cookies after the headers are converted to bytes. the lines below can be removed.

[L321-L327](https://github.com/huge-success/sanic/blob/master/sanic/asgi.py#L321-L327)


Discussion:
@loek17 Agreed. This is an issue that needs to be fixed. Let me open a PR to address the same. 

Resolving patch:
diff --git a/sanic/asgi.py b/sanic/asgi.py
--- a/sanic/asgi.py
+++ b/sanic/asgi.py
@@ -1,7 +1,6 @@
 import asyncio
 import warnings
 
-from http.cookies import SimpleCookie
 from inspect import isawaitable
 from typing import Any, Awaitable, Callable, MutableMapping, Union
 from urllib.parse import quote
@@ -288,11 +287,22 @@ async def stream_callback(self, response: HTTPResponse) -> None:
         """
         Write the response.
         """
-
+        headers = []
+        cookies = {}
         try:
-            headers = [
+            cookies = {
+                v.key: v
+                for _, v in list(
+                    filter(
+                        lambda item: item[0].lower() == "set-cookie",
+                        response.headers.items(),
+                    )
+                )
+            }
+            headers += [
                 (str(name).encode("latin-1"), str(value).encode("latin-1"))
                 for name, value in response.headers.items()
+                if name.lower() not in ["set-cookie"]
             ]
         except AttributeError:
             logger.error(
@@ -319,12 +329,18 @@ async def stream_callback(self, response: HTTPResponse) -> None:
             ]
 
         if response.cookies:
-            cookies = SimpleCookie()
-            cookies.load(response.cookies)
-            headers += [
-                (b"set-cookie", cookie.encode("utf-8"))
-                for name, cookie in response.cookies.items()
-            ]
+            cookies.update(
+                {
+                    v.key: v
+                    for _, v in response.cookies.items()
+                    if v.key not in cookies.keys()
+                }
+            )
+
+        headers += [
+            (b"set-cookie", cookie.encode("utf-8"))
+            for k, cookie in cookies.items()
+        ]
 
         await self.transport.send(
             {


Test patch:
diff --git a/tests/test_asgi.py b/tests/test_asgi.py
--- a/tests/test_asgi.py
+++ b/tests/test_asgi.py
@@ -229,3 +229,30 @@ def custom_request(request):
 
     _, response = await app.asgi_client.get("/custom")
     assert response.body == b"MyCustomRequest"
+
+
+@pytest.mark.asyncio
+async def test_cookie_customization(app):
+    @app.get("/cookie")
+    def get_cookie(request):
+        response = text("There's a cookie up in this response")
+        response.cookies["test"] = "Cookie1"
+        response.cookies["test"]["httponly"] = True
+
+        response.cookies["c2"] = "Cookie2"
+        response.cookies["c2"]["httponly"] = False
+
+        return response
+
+    _, response = await app.asgi_client.get("/cookie")
+    cookie_map = {
+        "test": {"value": "Cookie1", "HttpOnly": True},
+        "c2": {"value": "Cookie2", "HttpOnly": False},
+    }
+
+    for k, v in (
+        response.cookies._cookies.get("mockserver.local").get("/").items()
+    ):
+        assert cookie_map.get(k).get("value") == v.value
+        if cookie_map.get(k).get("HttpOnly"):
+            assert "HttpOnly" in v._rest.keys()
diff --git a/tests/test_requests.py b/tests/test_requests.py
--- a/tests/test_requests.py
+++ b/tests/test_requests.py
@@ -635,13 +635,15 @@ async def handler(request):
         return text(request.remote_addr)
 
     request, response = app.test_client.get("/")
-    assert request.scheme == 'http'
+    assert request.scheme == "http"
 
-    request, response = app.test_client.get("/", headers={'X-Forwarded-Proto': 'https'})
-    assert request.scheme == 'https'
+    request, response = app.test_client.get(
+        "/", headers={"X-Forwarded-Proto": "https"}
+    )
+    assert request.scheme == "https"
 
-    request, response = app.test_client.get("/", headers={'X-Scheme': 'https'})
-    assert request.scheme == 'https'
+    request, response = app.test_client.get("/", headers={"X-Scheme": "https"})
+    assert request.scheme == "https"
 
 
 def test_match_info(app):
@@ -1677,7 +1679,7 @@ def handler(request):
         return text("OK")
 
     request, response = app.test_client.get("/")
-    assert request.server_name == '127.0.0.1'
+    assert request.server_name == "127.0.0.1"
 
 
 def test_request_server_name_in_host_header(app):
@@ -1685,8 +1687,10 @@ def test_request_server_name_in_host_header(app):
     def handler(request):
         return text("OK")
 
-    request, response = app.test_client.get("/", headers={'Host': 'my_server:5555'})
-    assert request.server_name == 'my_server'
+    request, response = app.test_client.get(
+        "/", headers={"Host": "my_server:5555"}
+    )
+    assert request.server_name == "my_server"
 
 
 def test_request_server_name_forwarded(app):
@@ -1694,11 +1698,11 @@ def test_request_server_name_forwarded(app):
     def handler(request):
         return text("OK")
 
-    request, response = app.test_client.get("/", headers={
-        'Host': 'my_server:5555',
-        'X-Forwarded-Host': 'your_server'
-    })
-    assert request.server_name == 'your_server'
+    request, response = app.test_client.get(
+        "/",
+        headers={"Host": "my_server:5555", "X-Forwarded-Host": "your_server"},
+    )
+    assert request.server_name == "your_server"
 
 
 def test_request_server_port(app):
@@ -1706,9 +1710,7 @@ def test_request_server_port(app):
     def handler(request):
         return text("OK")
 
-    request, response = app.test_client.get("/", headers={
-        'Host': 'my_server'
-    })
+    request, response = app.test_client.get("/", headers={"Host": "my_server"})
     assert request.server_port == app.test_client.port
 
 
@@ -1717,9 +1719,9 @@ def test_request_server_port_in_host_header(app):
     def handler(request):
         return text("OK")
 
-    request, response = app.test_client.get("/", headers={
-        'Host': 'my_server:5555'
-    })
+    request, response = app.test_client.get(
+        "/", headers={"Host": "my_server:5555"}
+    )
     assert request.server_port == 5555
 
 
@@ -1728,10 +1730,9 @@ def test_request_server_port_forwarded(app):
     def handler(request):
         return text("OK")
 
-    request, response = app.test_client.get("/", headers={
-        'Host': 'my_server:5555',
-        'X-Forwarded-Port': '4444'
-    })
+    request, response = app.test_client.get(
+        "/", headers={"Host": "my_server:5555", "X-Forwarded-Port": "4444"}
+    )
     assert request.server_port == 4444
 
 
@@ -1754,29 +1755,34 @@ def handler(request):
     def view_name(request):
         return text("OK")
 
-    request, response = app.test_client.get("/", headers={
-        'X-Forwarded-Proto': 'https',
-    })
-    assert app.url_for('view_name') == '/another_view'
-    assert app.url_for('view_name', _external=True) == 'http:///another_view'
-    assert request.url_for('view_name') == 'https://127.0.0.1:{}/another_view'.format(app.test_client.port)
+    request, response = app.test_client.get(
+        "/", headers={"X-Forwarded-Proto": "https"}
+    )
+    assert app.url_for("view_name") == "/another_view"
+    assert app.url_for("view_name", _external=True) == "http:///another_view"
+    assert request.url_for(
+        "view_name"
+    ) == "https://127.0.0.1:{}/another_view".format(app.test_client.port)
 
     app.config.SERVER_NAME = "my_server"
-    request, response = app.test_client.get("/", headers={
-        'X-Forwarded-Proto': 'https',
-        'X-Forwarded-Port': '6789',
-    })
-    assert app.url_for('view_name') == '/another_view'
-    assert app.url_for('view_name', _external=True) == 'http://my_server/another_view'
-    assert request.url_for('view_name') == 'https://my_server:6789/another_view'
-
-    request, response = app.test_client.get("/", headers={
-        'X-Forwarded-Proto': 'https',
-        'X-Forwarded-Port': '443',
-    })
-    assert request.url_for('view_name') == 'https://my_server/another_view'
-
-    
+    request, response = app.test_client.get(
+        "/", headers={"X-Forwarded-Proto": "https", "X-Forwarded-Port": "6789"}
+    )
+    assert app.url_for("view_name") == "/another_view"
+    assert (
+        app.url_for("view_name", _external=True)
+        == "http://my_server/another_view"
+    )
+    assert (
+        request.url_for("view_name") == "https://my_server:6789/another_view"
+    )
+
+    request, response = app.test_client.get(
+        "/", headers={"X-Forwarded-Proto": "https", "X-Forwarded-Port": "443"}
+    )
+    assert request.url_for("view_name") == "https://my_server/another_view"
+
+
 @pytest.mark.asyncio
 async def test_request_form_invalid_content_type_asgi(app):
     @app.route("/", methods=["POST"])
@@ -1787,7 +1793,7 @@ async def post(request):
 
     assert request.form == {}
 
-    
+
 def test_endpoint_basic():
     app = Sanic()