Skip to content

Commit 04b5245

Browse files
committed
Tolerate an http response object with no headers
1 parent 9156a30 commit 04b5245

File tree

2 files changed

+28
-5
lines changed

2 files changed

+28
-5
lines changed

msal/throttled_http_client.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
DEVICE_AUTH_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
1212

1313

14+
def _get_headers(response):
15+
# MSAL's HttpResponse did not have headers until 1.23.0
16+
# https://github.com/AzureAD/microsoft-authentication-library-for-python/pull/581/files#diff-28866b706bc3830cd20485685f20fe79d45b58dce7050e68032e9d9372d68654R61
17+
# This helper ensures graceful degradation to {} without exception
18+
return getattr(response, "headers", {})
19+
20+
1421
class RetryAfterParser(object):
1522
FIELD_NAME_LOWER = "Retry-After".lower()
1623
def __init__(self, default_value=None):
@@ -19,9 +26,7 @@ def __init__(self, default_value=None):
1926
def parse(self, *, result, **ignored):
2027
"""Return seconds to throttle"""
2128
response = result
22-
lowercase_headers = {k.lower(): v for k, v in getattr(
23-
# Historically, MSAL's HttpResponse does not always have headers
24-
response, "headers", {}).items()}
29+
lowercase_headers = {k.lower(): v for k, v in _get_headers(response).items()}
2530
if not (response.status_code == 429 or response.status_code >= 500
2631
or self.FIELD_NAME_LOWER in lowercase_headers):
2732
return 0 # Quick exit
@@ -49,7 +54,7 @@ def __init__(self, raw_response):
4954
self.status_code = raw_response.status_code
5055
self.text = raw_response.text
5156
self.headers = { # Only keep the headers which ThrottledHttpClient cares about
52-
k: v for k, v in raw_response.headers.items()
57+
k: v for k, v in _get_headers(raw_response).items()
5358
if k.lower() == RetryAfterParser.FIELD_NAME_LOWER
5459
}
5560

@@ -153,7 +158,7 @@ def __init__(self, *args, default_throttle_time=None, **kwargs):
153158
and kwargs["data"].get("grant_type") == DEVICE_AUTH_GRANT
154159
)
155160
and RetryAfterParser.FIELD_NAME_LOWER not in set( # Otherwise leave it to the Retry-After decorator
156-
h.lower() for h in getattr(result, "headers", {}).keys())
161+
h.lower() for h in _get_headers(result))
157162
else 0,
158163
)(self.post)
159164

tests/test_throttled_http_client.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ def close(self):
4949
raise CloseMethodCalled("Not used by MSAL, but our customers may use it")
5050

5151

52+
class DummyHttpClientWithoutResponseHeaders(DummyHttpClient):
53+
def post(self, url, params=None, data=None, headers=None, **kwargs):
54+
response = super().post(url, params, data, headers, **kwargs)
55+
del response.headers # Early versions of MSAL did not require http client to return headers
56+
return response
57+
58+
def get(self, url, params=None, headers=None, **kwargs):
59+
response = super().get(url, params, headers, **kwargs)
60+
del response.headers # Early versions of MSAL did not require http client to return headers
61+
return response
62+
63+
5264
class CloseMethodCalled(Exception):
5365
pass
5466

@@ -69,6 +81,12 @@ def test_pickled_minimal_response_should_contain_signature(self):
6981
self.assertIn(MinimalResponse.SIGNATURE, pickle.dumps(MinimalResponse(
7082
status_code=200, headers={}, text="foo")))
7183

84+
def test_throttled_http_client_base_response_should_tolerate_headerless_response(self):
85+
http_client = ThrottledHttpClientBase(DummyHttpClientWithoutResponseHeaders(
86+
status_code=200, response_text="foo"))
87+
response = http_client.post("https://example.com")
88+
self.assertEqual(response.text, "foo", "Should return the same response text")
89+
7290
def test_throttled_http_client_base_response_should_not_contain_signature(self):
7391
http_client = ThrottledHttpClientBase(DummyHttpClient(status_code=200))
7492
response = http_client.post("https://example.com")

0 commit comments

Comments
 (0)