Skip to content

Commit 0270f09

Browse files
committed
Make responding with binary data work under PY3.
The code as previously written would unconditionally encode parts as though they contained text - this went unnoticed on PY2 because strings and bytes are one and the same thing but blew up on PY3 where a string is explicitly of type unicode while a binary file would be raw bytes. Explicitly check the output_format and if instructed to serve a file do so without touching the chunks of file content bytes being yielded.
1 parent 38b00ac commit 0270f09

File tree

4 files changed

+90
-9
lines changed

4 files changed

+90
-9
lines changed

mig/wsgi-bin/migwsgi.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ def _set_os_environ(value):
206206
return _application(None, environ, start_response, _set_environ=_set_os_environ, _wrap_wsgi_errors=wrap_wsgi_errors)
207207

208208

209-
def _application(configuration, environ, start_response, _set_environ, _format_output=format_output, _retrieve_handler=_import_backend, _wrap_wsgi_errors=True, _config_file=None, _skip_log=False):
209+
def _application(configuration, environ, start_response, _set_environ, _fieldstorage_to_dict=fieldstorage_to_dict, _format_output=format_output, _retrieve_handler=_import_backend, _wrap_wsgi_errors=True, _config_file=None, _skip_log=False):
210210

211211
# NOTE: pass app environ including apache and query args on to sub handlers
212212
# through the usual 'os.environ' channel expected in functionality
@@ -319,7 +319,7 @@ def _application(configuration, environ, start_response, _set_environ, _format_o
319319
# (backend, script_name))
320320
fieldstorage = cgi.FieldStorage(fp=environ['wsgi.input'],
321321
environ=environ)
322-
user_arguments_dict = fieldstorage_to_dict(fieldstorage)
322+
user_arguments_dict = _fieldstorage_to_dict(fieldstorage)
323323
if 'output_format' in user_arguments_dict:
324324
output_format = user_arguments_dict['output_format'][0]
325325

@@ -437,7 +437,10 @@ def _application(configuration, environ, start_response, _set_environ, _format_o
437437
# (backend, i+1, chunk_parts))
438438
# end index may be after end of content - but no problem
439439
part = output[i*download_block_size:(i+1)*download_block_size]
440-
yield force_utf8(part)
440+
if output_format == 'file':
441+
yield part
442+
else:
443+
yield force_utf8(part)
441444
if chunk_parts > 1:
442445
_logger.info("WSGI %s finished yielding all %d output parts" %
443446
(backend, chunk_parts))

tests/support/wsgibinsupp.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,22 @@ def _is_return_value(return_value):
3737
return return_value in defined_return_values
3838

3939

40+
def create_instrumented_fieldstorage_to_dict():
41+
def _instrumented_fieldstorage_to_dict(fieldstorage):
42+
return _instrumented_fieldstorage_to_dict._result
43+
44+
_instrumented_fieldstorage_to_dict._result = {
45+
'output_format': ('html',)
46+
}
47+
48+
def set_result(result):
49+
_instrumented_fieldstorage_to_dict._result = result
50+
51+
_instrumented_fieldstorage_to_dict.set_result = set_result
52+
53+
return _instrumented_fieldstorage_to_dict
54+
55+
4056
def create_instrumented_format_output():
4157
def _instrumented_format_output(
4258
configuration,
@@ -53,6 +69,16 @@ def _instrumented_format_output(
5369
call_args_out_obj, outputformat,)
5470
_instrumented_format_output.calls.append({'args': call_args})
5571

72+
if _instrumented_format_output._file:
73+
return format_output(
74+
configuration,
75+
backend,
76+
ret_val,
77+
ret_msg,
78+
out_obj,
79+
outputformat,
80+
)
81+
5682
# FIXME: the following is a workaround for a bug that exists between the WSGI wrapper
5783
# and the output formatter - specifically, the latter adds default header and
5884
# title if start does not exist, but the former ensures that start always exists
@@ -88,11 +114,17 @@ def _instrumented_format_output(
88114
outputformat,
89115
)
90116
_instrumented_format_output.calls = []
117+
_instrumented_format_output._file = False
91118
_instrumented_format_output.values = dict(
92119
title_text='',
93120
header_text='',
94121
)
95122

123+
def _set_file(is_enabled):
124+
_instrumented_format_output._file = is_enabled
125+
126+
setattr(_instrumented_format_output, 'set_file', _set_file)
127+
96128
def _program_values(**kwargs):
97129
_instrumented_format_output.values.update(kwargs)
98130

@@ -126,12 +158,33 @@ def _instrumented_retrieve_handler(*args):
126158

127159
class WsgibinInstrumentation:
128160
def __init__(self):
161+
self.fieldstorage_to_dict = create_instrumented_fieldstorage_to_dict()
129162
self.format_output = create_instrumented_format_output()
130163
self.retrieve_handler = create_instrumented_retrieve_handler()
131164

132-
def set_response(self, content, returnvalue):
165+
def _set_response_content(self, content, returnvalue):
133166
self.retrieve_handler.program(content, returnvalue)
134167

168+
def _set_response_file(self, returnbytes, returnvalue):
169+
self.fieldstorage_to_dict.set_result({
170+
'output_format': ('file',)
171+
})
172+
self.format_output.set_file(True)
173+
file_obj = {'object_type': 'binary', 'data': returnbytes}
174+
self.set_response([file_obj], returnvalue)
175+
176+
def set_response(self, content, returnvalue, responding_with='objects'):
177+
assert not (content is not None and file is not None)
178+
179+
if responding_with == 'file':
180+
assert isinstance(
181+
returnvalue, bytes), "file response demands bytes"
182+
self._set_response_file(content, returnvalue)
183+
elif responding_with == 'objects':
184+
self._set_response_content(content, returnvalue)
185+
else:
186+
raise NotImplementedError()
187+
135188

136189
class WsgibinAssertMixin:
137190
def assertWsgibinInstrumentation(self, instrumentation=None):

tests/support/wsgisupp.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,21 +114,27 @@ def prepare_wsgi(configuration, url, **kwargs):
114114
)
115115

116116

117-
def _trigger_and_unpack_result(wsgi_result):
117+
def _trigger_and_unpack_result(wsgi_result, content_kind='textual'):
118+
assert content_kind in ('textual', 'binary')
119+
118120
chunks = list(wsgi_result)
119121
assert len(chunks) > 0, "invocation returned no output"
120122
complete_value = b''.join(chunks)
121-
decoded_value = codecs.decode(complete_value, 'utf8')
123+
if content_kind == 'binary':
124+
decoded_value = complete_value
125+
else:
126+
decoded_value = codecs.decode(complete_value, 'utf8')
122127
return decoded_value
123128

124129

125130
class WsgiAssertMixin:
126131
"""Custom assertions for verifying server code executed under test."""
127132

128-
def assertWsgiResponse(self, wsgi_result, fake_wsgi, expected_status_code):
133+
def assertWsgiResponse(self, wsgi_result, fake_wsgi, expected_status_code,
134+
content_kind='textual'):
129135
assert isinstance(fake_wsgi, _PreparedWsgi)
130136

131-
content = _trigger_and_unpack_result(wsgi_result)
137+
content = _trigger_and_unpack_result(wsgi_result, content_kind=content_kind)
132138

133139
def called_once(fake):
134140
assert hasattr(fake, 'calls')

tests/test_mig_wsgi-bin.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
import stat
3434
import sys
3535

36-
from tests.support import PY2, MIG_BASE, MigTestCase, testmain, is_path_within
36+
from tests.support import PY2, MIG_BASE, TEST_DATA_DIR, \
37+
MigTestCase, testmain, is_path_within
3738
from tests.support.htmlsupp import HtmlAssertMixin
3839
from tests.support.wsgisupp import prepare_wsgi, WsgiAssertMixin
3940
from tests.support.wsgibinsupp import WsgibinInstrumentation, WsgibinAssertMixin
@@ -79,6 +80,7 @@ def before_each(self):
7980
)
8081
self.application_kwargs = dict(
8182
_wrap_wsgi_errors=noop,
83+
_fieldstorage_to_dict=self.wsgibin_instrumentation.fieldstorage_to_dict,
8284
_format_output=self.wsgibin_instrumentation.format_output,
8385
_retrieve_handler=self.wsgibin_instrumentation.retrieve_handler,
8486
_set_environ=noop,
@@ -122,6 +124,23 @@ def test_return_value_ok_returns_expected_title(self):
122124
self.assertHtmlElementTextContent(
123125
output, 'title', 'TEST', trim_newlines=True)
124126

127+
def test_return_value_ok_serving_a_binary_file(self):
128+
test_binary_file = os.path.join(TEST_DATA_DIR, 'loading.gif')
129+
with open(test_binary_file, 'rb') as f:
130+
test_binary_data = f.read()
131+
self.wsgibin_instrumentation.set_response(
132+
test_binary_data, returnvalues.OK, responding_with='file')
133+
134+
wsgi_result = migwsgi._application(
135+
*self.application_args,
136+
**self.application_kwargs
137+
)
138+
139+
output, _ = self.assertWsgiResponse(
140+
wsgi_result, self.fake_wsgi, 200, content_kind='binary')
141+
self.assertWsgibinInstrumentation()
142+
self.assertEqual(output, test_binary_data)
143+
125144

126145
if __name__ == '__main__':
127146
testmain()

0 commit comments

Comments
 (0)