Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0915761

Browse files
committed
Fix unicode issues in HttpError and BatchHttpRequest
1) HttpError parses http response by json.loads. It accepts only unicode on PY3 while response is bytes. So decode response first. 2) BatchHttpRequest uses email.parser.FeedParser. It accepts only unicode on PY3, too. So encode parsed response to emulate normal http response.
1 parent edf2d2e commit 0915761

File tree

7 files changed

+28
-17
lines changed

7 files changed

+28
-17
lines changed

googleapiclient/errors.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,16 @@ class HttpError(Error):
3737
@util.positional(3)
3838
def __init__(self, resp, content, uri=None):
3939
self.resp = resp
40+
if not isinstance(content, bytes):
41+
raise TypeError("HTTP content should be bytes")
4042
self.content = content
4143
self.uri = uri
4244

4345
def _get_reason(self):
4446
"""Calculate the reason for the error from the response content."""
4547
reason = self.resp.reason
4648
try:
47-
data = json.loads(self.content)
49+
data = json.loads(self.content.decode('utf-8'))
4850
reason = data['error']['message']
4951
except (ValueError, KeyError):
5052
pass

googleapiclient/http.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,10 @@ def _execute(self, http, order, requests):
12571257

12581258
# Prepend with a content-type header so FeedParser can handle it.
12591259
header = 'content-type: %s\r\n\r\n' % resp['content-type']
1260+
# PY3's FeedParser only accepts unicode. So we should decode content
1261+
# here, and encode each payload again.
1262+
if six.PY3:
1263+
content = content.decode('utf-8')
12601264
for_parser = header + content
12611265

12621266
parser = FeedParser()
@@ -1270,6 +1274,9 @@ def _execute(self, http, order, requests):
12701274
for part in mime_response.get_payload():
12711275
request_id = self._header_to_id(part['Content-ID'])
12721276
response, content = self._deserialize_response(part.get_payload())
1277+
# We encode content here to emulate normal http response.
1278+
if isinstance(content, six.text_type):
1279+
content = content.encode('utf-8')
12731280
self._responses[request_id] = (response, content)
12741281

12751282
@util.positional(1)
@@ -1536,6 +1543,8 @@ def request(self, uri,
15361543
content = body
15371544
elif content == 'echo_request_uri':
15381545
content = uri
1546+
if isinstance(content, six.text_type):
1547+
content = content.encode('utf-8')
15391548
return httplib2.Response(resp), content
15401549

15411550

tests/test_discovery.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,7 +1048,7 @@ def test_resumable_media_handle_resume_of_upload_of_unknown_size(self):
10481048
'Content-Range': 'bytes */14',
10491049
'content-length': '0'
10501050
}
1051-
self.assertEqual(expected, json.loads(e.content),
1051+
self.assertEqual(expected, json.loads(e.content.decode('utf-8')),
10521052
'Should send an empty body when requesting the current upload status.')
10531053

10541054
def test_pickle(self):
@@ -1186,7 +1186,7 @@ def test_get_media(self):
11861186
({'status': '200'}, 'standing in for media'),
11871187
])
11881188
response = request.execute(http=http)
1189-
self.assertEqual('standing in for media', response)
1189+
self.assertEqual(b'standing in for media', response)
11901190

11911191

11921192
if __name__ == '__main__':

tests/test_errors.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from googleapiclient.errors import HttpError
2929

3030

31-
JSON_ERROR_CONTENT = """
31+
JSON_ERROR_CONTENT = b"""
3232
{
3333
"error": {
3434
"errors": [
@@ -65,36 +65,36 @@ def test_json_body(self):
6565

6666
def test_bad_json_body(self):
6767
"""Test handling of bodies with invalid json."""
68-
resp, content = fake_response('{',
68+
resp, content = fake_response(b'{',
6969
{ 'status':'400', 'content-type': 'application/json'},
7070
reason='Failed')
7171
error = HttpError(resp, content)
7272
self.assertEqual(str(error), '<HttpError 400 "Failed">')
7373

7474
def test_with_uri(self):
7575
"""Test handling of passing in the request uri."""
76-
resp, content = fake_response('{',
76+
resp, content = fake_response(b'{',
7777
{'status':'400', 'content-type': 'application/json'},
7878
reason='Failure')
7979
error = HttpError(resp, content, uri='http://example.org')
8080
self.assertEqual(str(error), '<HttpError 400 when requesting http://example.org returned "Failure">')
8181

8282
def test_missing_message_json_body(self):
8383
"""Test handling of bodies with missing expected 'message' element."""
84-
resp, content = fake_response('{}',
84+
resp, content = fake_response(b'{}',
8585
{'status':'400', 'content-type': 'application/json'},
8686
reason='Failed')
8787
error = HttpError(resp, content)
8888
self.assertEqual(str(error), '<HttpError 400 "Failed">')
8989

9090
def test_non_json(self):
9191
"""Test handling of non-JSON bodies"""
92-
resp, content = fake_response('}NOT OK', {'status':'400'})
92+
resp, content = fake_response(b'}NOT OK', {'status':'400'})
9393
error = HttpError(resp, content)
9494
self.assertEqual(str(error), '<HttpError 400 "Ok">')
9595

9696
def test_missing_reason(self):
9797
"""Test an empty dict with a missing resp.reason."""
98-
resp, content = fake_response('}NOT OK', {'status': '400'}, reason=None)
98+
resp, content = fake_response(b'}NOT OK', {'status': '400'}, reason=None)
9999
error = HttpError(resp, content)
100100
self.assertEqual(str(error), '<HttpError 400 "">')

tests/test_http.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ def test_media_io_base_download_retries_5xx(self):
460460
ETag: "etag/pony"\r\n\r\n{"answer": 42}"""
461461

462462

463-
BATCH_RESPONSE = """--batch_foobarbaz
463+
BATCH_RESPONSE = b"""--batch_foobarbaz
464464
Content-Type: application/http
465465
Content-Transfer-Encoding: binary
466466
Content-ID: <randomness+1>
@@ -482,7 +482,7 @@ def test_media_io_base_download_retries_5xx(self):
482482
--batch_foobarbaz--"""
483483

484484

485-
BATCH_ERROR_RESPONSE = """--batch_foobarbaz
485+
BATCH_ERROR_RESPONSE = b"""--batch_foobarbaz
486486
Content-Type: application/http
487487
Content-Transfer-Encoding: binary
488488
Content-ID: <randomness+1>
@@ -518,7 +518,7 @@ def test_media_io_base_download_retries_5xx(self):
518518
--batch_foobarbaz--"""
519519

520520

521-
BATCH_RESPONSE_WITH_401 = """--batch_foobarbaz
521+
BATCH_RESPONSE_WITH_401 = b"""--batch_foobarbaz
522522
Content-Type: application/http
523523
Content-Transfer-Encoding: binary
524524
Content-ID: <randomness+1>
@@ -541,7 +541,7 @@ def test_media_io_base_download_retries_5xx(self):
541541
--batch_foobarbaz--"""
542542

543543

544-
BATCH_SINGLE_RESPONSE = """--batch_foobarbaz
544+
BATCH_SINGLE_RESPONSE = b"""--batch_foobarbaz
545545
Content-Type: application/http
546546
Content-Transfer-Encoding: binary
547547
Content-ID: <randomness+1>
@@ -928,7 +928,7 @@ def _postproc(resp, content):
928928

929929
# Query parameters should be sent in the body.
930930
response = req.execute()
931-
self.assertEqual('q=' + 'a' * MAX_URI_LENGTH + '%3F%26', response)
931+
self.assertEqual(b'q=' + b'a' * MAX_URI_LENGTH + b'%3F%26', response)
932932

933933
# Extra headers should be set.
934934
response = req.execute()

tests/test_json_model.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def test_bad_response(self):
150150
model = JsonModel(data_wrapper=False)
151151
resp = httplib2.Response({'status': '401'})
152152
resp.reason = 'Unauthorized'
153-
content = '{"error": {"message": "not authorized"}}'
153+
content = b'{"error": {"message": "not authorized"}}'
154154

155155
try:
156156
content = model.response(resp, content)

tests/test_mocks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,15 @@ def test_simple_matching_dict_body(self):
134134
def test_errors(self):
135135
errorResponse = httplib2.Response({'status': 500, 'reason': 'Server Error'})
136136
requestBuilder = RequestMockBuilder({
137-
'plus.activities.list': (errorResponse, '{}')
137+
'plus.activities.list': (errorResponse, b'{}')
138138
})
139139
plus = build('plus', 'v1', http=self.http, requestBuilder=requestBuilder)
140140

141141
try:
142142
activity = plus.activities().list(collection='public', userId='me').execute()
143143
self.fail('An exception should have been thrown')
144144
except HttpError as e:
145-
self.assertEqual('{}', e.content)
145+
self.assertEqual(b'{}', e.content)
146146
self.assertEqual(500, e.resp.status)
147147
self.assertEqual('Server Error', e.resp.reason)
148148

0 commit comments

Comments
 (0)