Question

I inherited the following Django view code, used by another webservice to serve downloadable versions of output data:

def index(request):
    # ... (snip) ...
    data = base64.decodestring(request.POST['data'])
    filename = request.POST['filename']

    wrapper = FileWrapper(StringIO(data))

    response = HttpResponse(wrapper, content_type=guess_type(str(filename))[0])

    response['Content-Length'] = len(data)
    response['Content-Disposition'] = "attachment; filename=" + filename

    return response

The function itself — written against Django 1.0 — still works fine after upgrading to 1.5. Unfortunately, the test that covers this view is now failing:

    def testDownload(self):
        self.client.login(username='test', password='test')

        real = 'abc' * 100
        data = base64.encodestring(real)
        response = self.client.post("/api/v1/download/", {'data': data, 'filename': 'out.jpg'})

        self.assertEqual(real, response.content)
        self.assertEqual(response['Content-Disposition'], 'attachment; filename=out.jpg')

and the error:

Traceback (most recent call last):
  File "/home/fred/.secret_projects/final/gerbils/tests/amf.py", line 548, in testDownload
    self.assertEqual(real, response.content)
  File "/home/fred/.virtualenvs/cunning_plot/lib/python2.7/site-packages/django/http/response.py", line 282, in content
    self._consume_content()
  File "/home/carl/.virtualenvs/cunning_plot/lib/python2.7/site-packages/django/http/response.py", line 278, in _consume_content
    self.content = b''.join(self.make_bytes(e) for e in self._container)
  File "/home/carl/.virtualenvs/cunning_plot/lib/python2.7/site-packages/django/http/response.py", line 278, in <genexpr>
    self.content = b''.join(self.make_bytes(e) for e in self._container)
  File "/usr/lib64/python2.7/wsgiref/util.py", line 30, in next
    data = self.filelike.read(self.blksize)
  File "/usr/lib64/python2.7/StringIO.py", line 127, in read
    _complain_ifclosed(self.closed)
  File "/usr/lib64/python2.7/StringIO.py", line 40, in _complain_ifclosed
    raise ValueError, "I/O operation on closed file"
ValueError: I/O operation on closed file

So.. any ideas? I can't see anything in testDownload() or index() which would necessarily "close" the StringIO before it needs to be read. And if there were something, wouldn't it affect the non-test situation as well?

Very confused. Help appreciated.

Was it helpful?

Solution

An easy way to see where close is being called is to just subclass StringIO and put a breakpoint in the close function.

class CustomStringIO(StringIO):
    def close(self):
        import pdb; pdb.set_trace()
        super(CustomStringIO, self).close()

The stack for this is

-> response = self.client.post("/test/", {'data': data, 'filename': 'out.jpg'})
  ...\venv\lib\site-packages\django\test\client.py(463)post()
-> response = super(Client, self).post(path, data=data, content_type=content_type, **extra)
  ...\venv\lib\site-packages\django\test\client.py(297)post()
-> return self.request(**r)
  ...\venv\lib\site-packages\django\test\client.py(406)request()
-> response = self.handler(environ)
  ...\venv\lib\site-packages\django\test\client.py(119)__call__()
-> response.close()                    # will fire request_finished
  ...\venv\lib\site-packages\django\http\response.py(233)close()
-> closable.close()
> \testapp\views.py(11)close()
-> super(CustomStringIO, self).close()

It looks like the test client is closing the response, which in turn calls close on FileWrapper which then calls close on StringIO. This is all before you actually get to response.content.

Is there a reason why you need FileWrapper? As HttpResponse takes in string content and base64.decodestring returns a binary string, it seems you could just pass data directly to HttpResponse instead of having to create a StringIO and FileWrapper.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top