Skip to content

Commit 60e7d69

Browse files
YunchuWangpeterstone2017
and
peterstone2017
authored
Fix for GH#107: Enable support for multiple Set-Cookie response headers (native & wsgi/asgi) (#115)
* initial multi cookie attempt * address pr comments Co-authored-by: peterstone2017 <[email protected]>
1 parent 990bb95 commit 60e7d69

File tree

5 files changed

+71
-14
lines changed

5 files changed

+71
-14
lines changed

azure/functions/_abc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import io
77
import typing
88

9+
from azure.functions._thirdparty.werkzeug.datastructures import Headers
910

1011
T = typing.TypeVar('T')
1112

@@ -193,7 +194,7 @@ def charset(self):
193194

194195
@property
195196
@abc.abstractmethod
196-
def headers(self) -> typing.MutableMapping[str, str]:
197+
def headers(self) -> Headers:
197198
pass
198199

199200
@abc.abstractmethod

azure/functions/_http.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
import collections.abc
55
import io
66
import json
7-
import typing
87
import types
8+
import typing
99

1010
from . import _abc
11-
1211
from ._thirdparty.werkzeug import datastructures as _wk_datastructures
1312
from ._thirdparty.werkzeug import formparser as _wk_parser
1413
from ._thirdparty.werkzeug import http as _wk_http
14+
from ._thirdparty.werkzeug.datastructures import Headers
1515

1616

1717
class BaseHeaders(collections.abc.Mapping):
@@ -40,13 +40,8 @@ class HttpRequestHeaders(BaseHeaders):
4040
pass
4141

4242

43-
class HttpResponseHeaders(BaseHeaders, collections.abc.MutableMapping):
44-
45-
def __setitem__(self, key: str, value: str):
46-
self.__http_headers__[key.lower()] = value
47-
48-
def __delitem__(self, key: str):
49-
del self.__http_headers__[key.lower()]
43+
class HttpResponseHeaders(Headers):
44+
pass
5045

5146

5247
class HttpResponse(_abc.HttpResponse):
@@ -90,7 +85,10 @@ def __init__(self,
9085

9186
if headers is None:
9287
headers = {}
93-
self.__headers = HttpResponseHeaders(headers)
88+
89+
self.__headers = HttpResponseHeaders([])
90+
for k, v in headers.items():
91+
self.__headers.add_header(k, v)
9492

9593
if body is not None:
9694
self.__set_body(body)

azure/functions/http.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33

44
import json
55
import typing
6+
from http.cookies import SimpleCookie
67

78
from azure.functions import _abc as azf_abc
89
from azure.functions import _http as azf_http
9-
1010
from . import meta
11+
from ._thirdparty.werkzeug.datastructures import Headers
1112

1213

1314
class HttpRequest(azf_http.HttpRequest):
@@ -79,7 +80,8 @@ def encode(cls, obj: typing.Any, *,
7980

8081
if isinstance(obj, azf_abc.HttpResponse):
8182
status = obj.status_code
82-
headers = dict(obj.headers)
83+
headers: Headers = obj.headers
84+
8385
if 'content-type' not in headers:
8486
if obj.mimetype.startswith('text/'):
8587
ct = f'{obj.mimetype}; charset={obj.charset}'
@@ -93,6 +95,12 @@ def encode(cls, obj: typing.Any, *,
9395
else:
9496
datum_body = meta.Datum(type='bytes', value=b'')
9597

98+
cookies = None
99+
if "Set-Cookie" in headers:
100+
cookies = [SimpleCookie(cookie) for cookie in
101+
headers.get_all('Set-Cookie')]
102+
headers.pop("Set-Cookie")
103+
96104
return meta.Datum(
97105
type='http',
98106
value=dict(
@@ -101,6 +109,7 @@ def encode(cls, obj: typing.Any, *,
101109
n: meta.Datum(type='string', value=h)
102110
for n, h in headers.items()
103111
},
112+
cookies=cookies,
104113
body=datum_body,
105114
)
106115
)

tests/test_http.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import azure.functions as func
77
import azure.functions.http as http
8+
from azure.functions._http import HttpResponseHeaders
89

910

1011
class TestHTTP(unittest.TestCase):
@@ -88,6 +89,53 @@ def test_http_output_type(self):
8889
self.assertTrue(check_output_type(func.HttpResponse))
8990
self.assertTrue(check_output_type(str))
9091

92+
def test_http_response_encode_to_datum_no_cookie(self):
93+
resp = func.HttpResponse()
94+
datum = http.HttpResponseConverter.encode(resp, expected_type=None)
95+
96+
self.assertEqual(datum.value["cookies"], None)
97+
98+
def test_http_response_encode_to_datum_with_cookies(self):
99+
headers = HttpResponseHeaders()
100+
headers.add("Set-Cookie",
101+
'foo3=42; Domain=example.com; Expires=Thu, '
102+
'12-Jan-2017 13:55:08 GMT; Path=/; Max-Age=10000000')
103+
headers.add("Set-Cookie",
104+
'foo3=43; Domain=example.com; Expires=Thu, 12-Jan-2018 '
105+
'13:55:09 GMT; Path=/; Max-Age=10000000')
106+
resp = func.HttpResponse(headers=headers)
107+
datum = http.HttpResponseConverter.encode(resp, expected_type=None)
108+
109+
actual_cookies = datum.value['cookies']
110+
self.assertIsNotNone(actual_cookies)
111+
self.assertTrue(isinstance(actual_cookies, list))
112+
self.assertTrue(len(actual_cookies), 2)
113+
self.assertEqual(str(actual_cookies[0]),
114+
"Set-Cookie: foo3=42; Domain=example.com; "
115+
"expires=Thu, 12-Jan-2017 13:55:08 GMT; "
116+
"Max-Age=10000000; Path=/")
117+
self.assertEqual(str(actual_cookies[1]),
118+
"Set-Cookie: foo3=43; Domain=example.com; "
119+
"expires=Thu, 12-Jan-2018 13:55:09 GMT; "
120+
"Max-Age=10000000; Path=/")
121+
122+
self.assertTrue("Set-Cookie" not in resp.headers)
123+
124+
def test_http_response_encode_to_datum_with_cookies_lower_case(self):
125+
headers = HttpResponseHeaders()
126+
headers.add("set-cookie",
127+
'foo3=42; Domain=example.com; Path=/; Max-Age=10000.0')
128+
resp = func.HttpResponse(headers=headers)
129+
datum = http.HttpResponseConverter.encode(resp, expected_type=None)
130+
131+
actual_cookies = datum.value['cookies']
132+
self.assertIsNotNone(actual_cookies)
133+
self.assertTrue(isinstance(actual_cookies, list))
134+
self.assertTrue(len(actual_cookies), 1)
135+
self.assertEqual(str(actual_cookies[0]),
136+
"Set-Cookie: foo3=42; Domain=example.com; "
137+
"Max-Age=10000.0; Path=/")
138+
91139
def test_http_request_should_not_have_implicit_output(self):
92140
self.assertFalse(http.HttpRequestConverter.has_implicit_output())
93141

tests/test_http_wsgi.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import azure.functions as func
88
from azure.functions._abc import TraceContext, RetryContext
9+
from azure.functions._http import HttpResponseHeaders
910
from azure.functions._http_wsgi import (
1011
WsgiRequest,
1112
WsgiResponse,
@@ -153,7 +154,7 @@ def test_response_no_headers(self):
153154

154155
wsgi_response: WsgiResponse = WsgiResponse.from_app(app, environ)
155156
func_response: func.HttpResponse = wsgi_response.to_func_response()
156-
self.assertEqual(func_response.headers, {})
157+
self.assertEqual(func_response.headers, HttpResponseHeaders([]))
157158

158159
def test_response_with_exception(self):
159160
app = self._generate_wsgi_app(

0 commit comments

Comments
 (0)