Skip to content

Commit 3cf5e60

Browse files
authored
Under Python 3; fix bug that caused invalid Content-ID header when custom request_id supplied (#545)
Fixes bug under Python 3 caused by incorrect RFC2822 Content-ID header folding (see #164, #536) Allows custom request ids to contain the '+' character also. Closes #164 Closes #536
1 parent bbaad28 commit 3cf5e60

File tree

2 files changed

+47
-11
lines changed

2 files changed

+47
-11
lines changed

googleapiclient/http.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,7 +1171,10 @@ def _id_to_header(self, id_):
11711171
if self._base_id is None:
11721172
self._base_id = uuid.uuid4()
11731173

1174-
return '<%s+%s>' % (self._base_id, quote(id_))
1174+
# NB: we intentionally leave whitespace between base/id and '+', so RFC2822
1175+
# line folding works properly on Python 3; see
1176+
# https://github.com/google/google-api-python-client/issues/164
1177+
return '<%s + %s>' % (self._base_id, quote(id_))
11751178

11761179
def _header_to_id(self, header):
11771180
"""Convert a Content-ID header value to an id.
@@ -1192,7 +1195,7 @@ def _header_to_id(self, header):
11921195
raise BatchError("Invalid value for Content-ID: %s" % header)
11931196
if '+' not in header:
11941197
raise BatchError("Invalid value for Content-ID: %s" % header)
1195-
base, id_ = header[1:-1].rsplit('+', 1)
1198+
base, id_ = header[1:-1].split(' + ', 1)
11961199

11971200
return unquote(id_)
11981201

@@ -1302,8 +1305,8 @@ def add(self, request, callback=None, request_id=None):
13021305
request id, and the second is the deserialized response object. The
13031306
third is an googleapiclient.errors.HttpError exception object if an HTTP error
13041307
occurred while processing the request, or None if no errors occurred.
1305-
request_id: string, A unique id for the request. The id will be passed to
1306-
the callback with the response.
1308+
request_id: string, A unique id for the request. The id will be passed
1309+
to the callback with the response.
13071310
13081311
Returns:
13091312
None

tests/test_http.py

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ def test_media_io_base_download_unknown_media_size(self):
635635
BATCH_RESPONSE = b"""--batch_foobarbaz
636636
Content-Type: application/http
637637
Content-Transfer-Encoding: binary
638-
Content-ID: <randomness+1>
638+
Content-ID: <randomness + 1>
639639
640640
HTTP/1.1 200 OK
641641
Content-Type: application/json
@@ -645,7 +645,7 @@ def test_media_io_base_download_unknown_media_size(self):
645645
--batch_foobarbaz
646646
Content-Type: application/http
647647
Content-Transfer-Encoding: binary
648-
Content-ID: <randomness+2>
648+
Content-ID: <randomness + 2>
649649
650650
HTTP/1.1 200 OK
651651
Content-Type: application/json
@@ -657,7 +657,7 @@ def test_media_io_base_download_unknown_media_size(self):
657657
BATCH_ERROR_RESPONSE = b"""--batch_foobarbaz
658658
Content-Type: application/http
659659
Content-Transfer-Encoding: binary
660-
Content-ID: <randomness+1>
660+
Content-ID: <randomness + 1>
661661
662662
HTTP/1.1 200 OK
663663
Content-Type: application/json
@@ -667,7 +667,7 @@ def test_media_io_base_download_unknown_media_size(self):
667667
--batch_foobarbaz
668668
Content-Type: application/http
669669
Content-Transfer-Encoding: binary
670-
Content-ID: <randomness+2>
670+
Content-ID: <randomness + 2>
671671
672672
HTTP/1.1 403 Access Not Configured
673673
Content-Type: application/json
@@ -693,7 +693,7 @@ def test_media_io_base_download_unknown_media_size(self):
693693
BATCH_RESPONSE_WITH_401 = b"""--batch_foobarbaz
694694
Content-Type: application/http
695695
Content-Transfer-Encoding: binary
696-
Content-ID: <randomness+1>
696+
Content-ID: <randomness + 1>
697697
698698
HTTP/1.1 401 Authorization Required
699699
Content-Type: application/json
@@ -704,7 +704,7 @@ def test_media_io_base_download_unknown_media_size(self):
704704
--batch_foobarbaz
705705
Content-Type: application/http
706706
Content-Transfer-Encoding: binary
707-
Content-ID: <randomness+2>
707+
Content-ID: <randomness + 2>
708708
709709
HTTP/1.1 200 OK
710710
Content-Type: application/json
@@ -716,7 +716,7 @@ def test_media_io_base_download_unknown_media_size(self):
716716
BATCH_SINGLE_RESPONSE = b"""--batch_foobarbaz
717717
Content-Type: application/http
718718
Content-Transfer-Encoding: binary
719-
Content-ID: <randomness+1>
719+
Content-ID: <randomness + 1>
720720
721721
HTTP/1.1 200 OK
722722
Content-Type: application/json
@@ -1211,6 +1211,39 @@ def test_execute_request_body(self):
12111211
header = parts[1].splitlines()[1]
12121212
self.assertEqual('Content-Type: application/http', header)
12131213

1214+
def test_execute_request_body_with_custom_long_request_ids(self):
1215+
batch = BatchHttpRequest()
1216+
1217+
batch.add(self.request1, request_id='abc'*20)
1218+
batch.add(self.request2, request_id='def'*20)
1219+
http = HttpMockSequence([
1220+
({'status': '200',
1221+
'content-type': 'multipart/mixed; boundary="batch_foobarbaz"'},
1222+
'echo_request_body'),
1223+
])
1224+
try:
1225+
batch.execute(http=http)
1226+
self.fail('Should raise exception')
1227+
except BatchError as e:
1228+
boundary, _ = e.content.split(None, 1)
1229+
self.assertEqual('--', boundary[:2])
1230+
parts = e.content.split(boundary)
1231+
self.assertEqual(4, len(parts))
1232+
self.assertEqual('', parts[0])
1233+
self.assertEqual('--', parts[3].rstrip())
1234+
for partindex, request_id in ((1, 'abc'*20), (2, 'def'*20)):
1235+
lines = parts[partindex].splitlines()
1236+
for n, line in enumerate(lines):
1237+
if line.startswith('Content-ID:'):
1238+
# assert correct header folding
1239+
self.assertTrue(line.endswith('+'), line)
1240+
header_continuation = lines[n+1]
1241+
self.assertEqual(
1242+
header_continuation,
1243+
' %s>' % request_id,
1244+
header_continuation
1245+
)
1246+
12141247
def test_execute_initial_refresh_oauth2(self):
12151248
batch = BatchHttpRequest()
12161249
callbacks = Callbacks()

0 commit comments

Comments
 (0)