Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ResourceWarning: unclosed file with request.POST['file'].file #3599

Closed
hyperknot opened this issue Jun 29, 2020 · 13 comments
Closed

ResourceWarning: unclosed file with request.POST['file'].file #3599

hyperknot opened this issue Jun 29, 2020 · 13 comments

Comments

@hyperknot
Copy link
Contributor

hyperknot commented Jun 29, 2020

Describe the bug
When I POST to the following endpoint snippet, after a few tries I get a ResourceWarning: unclosed file.

@view_config(route_name='file_upload', renderer='json')
def map_upload(request):
    request.POST['file'].file

    with open('/some/path/on/local/disk/some.json') as infile:
        infile.read()
    
    return {}

Output with
PYTHONTRACEMALLOC=25

Output 1:

..../venv/lib/python3.7/site-packages/pyramid/urldispatch.py:211: ResourceWarning: unclosed file <_io.FileIO name=24 mode='rb+' closefd=True>
  for k, v in m.groupdict().items():
Object allocated at (most recent call last):
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 890
    self._bootstrap_inner()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 926
    self.run()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 870
    self._target(*self._args, **self._kwargs)
  File "..../venv/lib/python3.7/site-packages/waitress/task.py", lineno 84
    task.service()
  File "..../venv/lib/python3.7/site-packages/waitress/channel.py", lineno 349
    task.service()
  File "..../venv/lib/python3.7/site-packages/waitress/task.py", lineno 169
    self.execute()
  File "..../venv/lib/python3.7/site-packages/waitress/task.py", lineno 439
    app_iter = self.channel.server.application(environ, start_response)
  File "..../venv/lib/python3.7/site-packages/paste/translogger.py", lineno 69
    return self.application(environ, replacement_start_response)
  File "..../venv/lib/python3.7/site-packages/pyramid/router.py", lineno 270
    response = self.execution_policy(environ, self)
  File "..../venv/lib/python3.7/site-packages/pyramid/router.py", lineno 277
    return router.invoke_request(request)
  File "..../venv/lib/python3.7/site-packages/pyramid/router.py", lineno 249
    response = handle_request(request)
  File "..../venv/lib/python3.7/site-packages/pyramid_tm/__init__.py", lineno 143
    response = handler(request)
  File "..../venv/lib/python3.7/site-packages/pyramid/tweens.py", lineno 41
    response = handler(request)
  File "..../venv/lib/python3.7/site-packages/pyramid/router.py", lineno 148
    registry, request, context, context_iface, view_name
  File "..../venv/lib/python3.7/site-packages/pyramid/view.py", lineno 667
    response = view_callable(context, request)
  File "..../venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 514
    return view(context, request)
  File "/Users/user/nonsync/dev/maphub/web/server/maphub_web/security.py", lineno 94
    response = view(context, request)
  File "..../venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 436
    result = view(context, request)
  File "..../venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 144
    response = view(request)
  File "/Users/user/nonsync/dev/maphub/web/server/maphub_web/views/ajax/upload_download.py", lineno 36
    request.POST['file'].file
  File "..../venv/lib/python3.7/site-packages/webob/request.py", lineno 776
    self.make_body_seekable()
  File "..../venv/lib/python3.7/site-packages/webob/request.py", lineno 929
    self.copy_body()
  File "..../venv/lib/python3.7/site-packages/webob/request.py", lineno 979
    fileobj = self.make_tempfile()
  File "..../venv/lib/python3.7/site-packages/webob/request.py", lineno 1019
    return tempfile.TemporaryFile()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/tempfile.py", lineno 622
    newline=newline, encoding=encoding)

In a less minimal form (closer to real world logic), the output was:

/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/json/decoder.py:353: ResourceWarning: unclosed file <_io.BufferedRandom name=18>
  obj, end = self.scan_once(s, idx)
Object allocated at (most recent call last):
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 890
    self._bootstrap_inner()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 926
    self.run()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 870
    self._target(*self._args, **self._kwargs)
  File "venv/lib/python3.7/site-packages/waitress/task.py", lineno 84
    task.service()
  File "venv/lib/python3.7/site-packages/waitress/channel.py", lineno 349
    task.service()
  File "venv/lib/python3.7/site-packages/waitress/task.py", lineno 169
    self.execute()
  File "venv/lib/python3.7/site-packages/waitress/task.py", lineno 439
    app_iter = self.channel.server.application(environ, start_response)
  File "venv/lib/python3.7/site-packages/paste/translogger.py", lineno 69
    return self.application(environ, replacement_start_response)
  File "venv/lib/python3.7/site-packages/pyramid/router.py", lineno 270
    response = self.execution_policy(environ, self)
  File "venv/lib/python3.7/site-packages/pyramid/router.py", lineno 277
    return router.invoke_request(request)
  File "venv/lib/python3.7/site-packages/pyramid/router.py", lineno 249
    response = handle_request(request)
  File "venv/lib/python3.7/site-packages/pyramid_tm/__init__.py", lineno 143
    response = handler(request)
  File "venv/lib/python3.7/site-packages/pyramid/tweens.py", lineno 41
    response = handler(request)
  File "venv/lib/python3.7/site-packages/pyramid/router.py", lineno 148
    registry, request, context, context_iface, view_name
  File "venv/lib/python3.7/site-packages/pyramid/view.py", lineno 667
    response = view_callable(context, request)
  File "venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 514
    return view(context, request)
  File "/Users/user/nonsync/dev/maphub/web/server/maphub_web/security.py", lineno 94
    response = view(context, request)
  File "venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 436
    result = view(context, request)
  File "venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 144
    response = view(request)
  File "/Users/user/nonsync/dev/maphub/web/server/maphub_web/views/ajax/upload_download.py", lineno 37
    file_obj = request.POST['file'].file
  File "venv/lib/python3.7/site-packages/webob/request.py", lineno 776
    self.make_body_seekable()
  File "venv/lib/python3.7/site-packages/webob/request.py", lineno 929
    self.copy_body()
  File "venv/lib/python3.7/site-packages/webob/request.py", lineno 979
    fileobj = self.make_tempfile()
  File "venv/lib/python3.7/site-packages/webob/request.py", lineno 1019
    return tempfile.TemporaryFile()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/tempfile.py", lineno 622
    newline=newline, encoding=encoding)

To Reproduce
Steps to reproduce the behavior:

  1. Try to POST the above snippet endpoint multiple times.
  2. The first upload is always good. The error happens on the 2nd .. 5th upload usually.

Expected behavior
Nothing like this should happen.

Additional context
This is really strange and it took me a while to reproduce this minimal case, it honestly makes no sense to me. But it's 100% reproducible, so I'm sharing it here as it is. The file I'm uploading is 2.8 MB in size.

Python 3.7 / macOS
pyramid==1.10.4
WebOb==1.8.6
waitress==1.4.4

@mmerickel
Copy link
Member

Opening a file without closing it... yep that'll trigger.

@view_config(route_name='file_upload', renderer='json')
def map_upload(request):
   with request.POST['file'].file as fp:
      pass

   with open('/some/path/on/local/disk/some.json') as infile:
        infile.read()
        return {}

@mmerickel
Copy link
Member

I opened this issue on webob which could help in the future but the behavior you're seeing is expected right now (unfortunately).

Pylons/webob#415

@hyperknot
Copy link
Contributor Author

hyperknot commented Jun 29, 2020

Sorry that return was obviously a mistake in my pasted snippet. The problem is the same when return is outside the with block.

@luhn
Copy link
Contributor

luhn commented Jun 29, 2020

The issue issue isn't the return, it's the request.POST['file'].file. That opens up a file handler, but it's never closed, hence the bug.

@mmerickel sample puts that in a with statement, which will close the file handler properly.

@hyperknot
Copy link
Contributor Author

hyperknot commented Jun 29, 2020

I tried it with @mmerickel's with statement, it didn't help either. It took a lot of tries, like 6-8 but it triggered the warning:

    with request.POST['file'].file as fp:
        pass

    with open(
        'path.json',
        'rb',
    ) as infile:
        infile.read()

    return {}
.../venv/lib/python3.7/site-packages/redis/_compat.py:72: ResourceWarning: unclosed file <_io.FileIO name=32 mode='rb+' closefd=True>
  return sock.recv(*args, **kwargs)
Object allocated at (most recent call last):
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 890
    self._bootstrap_inner()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 926
    self.run()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/threading.py", lineno 870
    self._target(*self._args, **self._kwargs)
  File ".../venv/lib/python3.7/site-packages/waitress/task.py", lineno 84
    task.service()
  File ".../venv/lib/python3.7/site-packages/waitress/channel.py", lineno 349
    task.service()
  File ".../venv/lib/python3.7/site-packages/waitress/task.py", lineno 169
    self.execute()
  File ".../venv/lib/python3.7/site-packages/waitress/task.py", lineno 439
    app_iter = self.channel.server.application(environ, start_response)
  File ".../venv/lib/python3.7/site-packages/paste/translogger.py", lineno 69
    return self.application(environ, replacement_start_response)
  File ".../venv/lib/python3.7/site-packages/pyramid/router.py", lineno 270
    response = self.execution_policy(environ, self)
  File ".../venv/lib/python3.7/site-packages/pyramid/router.py", lineno 277
    return router.invoke_request(request)
  File ".../venv/lib/python3.7/site-packages/pyramid/router.py", lineno 249
    response = handle_request(request)
  File ".../venv/lib/python3.7/site-packages/pyramid_tm/__init__.py", lineno 143
    response = handler(request)
  File ".../venv/lib/python3.7/site-packages/pyramid/tweens.py", lineno 41
    response = handler(request)
  File ".../venv/lib/python3.7/site-packages/pyramid/router.py", lineno 148
    registry, request, context, context_iface, view_name
  File ".../venv/lib/python3.7/site-packages/pyramid/view.py", lineno 667
    response = view_callable(context, request)
  File ".../venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 514
    return view(context, request)
  File "maphub_web/security.py", lineno 94
    response = view(context, request)
  File ".../venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 436
    result = view(context, request)
  File ".../venv/lib/python3.7/site-packages/pyramid/viewderivers.py", lineno 144
    response = view(request)
  File "maphub_web/views/ajax/upload_download.py", lineno 36
    with request.POST['file'].file as fp:
  File ".../venv/lib/python3.7/site-packages/webob/request.py", lineno 776
    self.make_body_seekable()
  File ".../venv/lib/python3.7/site-packages/webob/request.py", lineno 929
    self.copy_body()
  File ".../venv/lib/python3.7/site-packages/webob/request.py", lineno 979
    fileobj = self.make_tempfile()
  File ".../venv/lib/python3.7/site-packages/webob/request.py", lineno 1019
    return tempfile.TemporaryFile()
  File "/usr/local/Cellar/python/3.7.7/Frameworks/Python.framework/Versions/3.7/lib/python3.7/tempfile.py", lineno 622
    newline=newline, encoding=encoding)
200 POST /ajax/map_upload

@mmerickel
Copy link
Member

mmerickel commented Jun 29, 2020

Sorry the context manager is actually on the cgi.FieldStorage object which is file-like and supports reading/writing. This may work better:

@view_config(route_name='file_upload', renderer='json')
def map_upload(request):
   with request.POST['file'] as field:
      data = field.value

   with open('/some/path/on/local/disk/some.json') as infile:
        infile.read()
   return {}

If you want to consume the file directly without loading it into a full memory buffer then you can read field.file directly as well. The important thing is to make sure you close the field when you're done with it via field.file.close() which the context manager will do. From there, no one should try to access the raw file again in that request.

@hyperknot
Copy link
Contributor Author

hyperknot commented Jun 29, 2020

The following still doesn't work (actually this was what I tried as a very first step):

    fp = request.POST['file'].file

    with open('disk.file', 'wb') as dst_file:
        shutil.copyfileobj(fp, dst_file)

    fp.close()

    with open(some_path, 'rb') as infile:
        infile.read()

    return {}

@mmerickel
Copy link
Member

Apologies I’ll have to try to find a repro to offer more help. Closing the field file is usually enough.

@archoversight
Copy link

archoversight commented Jun 30, 2020

Closing the file returned from the FieldStorage is good enough. It's a red herring, looking at the traceback the temporary file that is the culprit is the one created by WebOb when it makes sure that the body of the request is seekable (so that it may be rewound to the beginning of the stream).

It looks like your WSGI server does not already give Pyramid a wsgi.input that is seekable, so WebOb does it for you, but it doesn't clean up after itself.

Edit: Actually WebOb doesn't check if the provided wsgi.input is seekable or not, only trusting itself, so always copying to make seekable.

@mmerickel
Copy link
Member

Ah I missed that it was due to the tempfile created from make_body_seekable which is a separate issue. Unfortunately there's not a good way to clean that up automatically at this time.

@hyperknot
Copy link
Contributor Author

It looks like your WSGI server does not already give Pyramid a wsgi.input that is seekable, so WebOb does it for you, but it doesn't clean up after itself.

This is in local development setup with Waitress.

@digitalresistor
Copy link
Member

@hyperknot and waitress does, but WebOb doesn't adequately check for it. See the edit above by @archoversight.

@mmerickel
Copy link
Member

Moving this issue to Pylons/webob#416.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants