Opened 4 weeks ago

Closed 2 weeks ago

#36975 closed Bug (duplicate)

SimpleUploadedFile cannot be re-opened

Reported by: Denis Washington Owned by: Vishy Algo
Component: File uploads/storage Version: 6.0
Severity: Normal Keywords:
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Summary

When a SimpleUploadedFile is closed, it calls close() on the underlying BytesIO, which means that the next call to open() fails (ValueError: I/O operation on closed file.). This is unlike the conceptually similar ContentFile, which explicitly overrides close() to do nothing, avoiding this issue.

How to Reproduce

The following script reproduces the issue:

from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile


def read_twice(file):
    with file.open() as f:
        print(f.read())
    with file.open() as f:
        print(f.read())


# Works as expected
read_twice(ContentFile(b"test data"))

# ValueError: I/O operation on closed file.
read_twice(SimpleUploadedFile("test.txt", b"test data"))

Expected Behavior

SimpleUploadedFile should follow ContentFile in making close() a no-op.

Ideally, the same should be done by its superclass InMemoryUploadedFile, at least if its file is a BytesIO or StringIO. (Skipping closing unconditionally could be risky here because the init method accepts any file-like object in principle.)

Change History (5)

comment:1 by Dinesh Thumma, 4 weeks ago

Owner: set to Dinesh Thumma
Status: newassigned

comment:2 by Vishy Algo, 3 weeks ago

This appears to be a fundamental file handling problem: once a BytesIO object is created and passed to parent classes, it cannot be reconstructed if abruptly closed. Since it's an InMemoryUploadedFile, we could save the content and recreate the file object via open(), allowing the context manager to clean it up automatically.

But, currently InMemoryUploadedFile accepts file object while construction, we have to add it to the SimpleUploadedFile somewhat similar to this,

  • django/core/files/uploadedfile.py

    diff --git a/django/core/files/uploadedfile.py b/django/core/files/uploadedfile.py
    index 1d006ede4f..8ff3c0a08a 100644
    a b class InMemoryUploadedFile(UploadedFile):  
    113113        self.field_name = field_name
    114114
    115115    def open(self, mode=None):
     116        if self.closed:
     117            self.file = BytesIO(self.content)
    116118        self.file.seek(0)
    117119        return self
    118120
    class SimpleUploadedFile(InMemoryUploadedFile):  
    132134    """
    133135
    134136    def __init__(self, name, content, content_type="text/plain"):
    135         content = content or b""
     137        self.content = content or b""
    136138        super().__init__(
    137             BytesIO(content), None, name, content_type, len(content), None, None
     139            BytesIO(self.content), None, name, content_type, len(self.content), None, None
    138140        )

I've verified this change with a unit test, and it works as expected. I believe using InMemoryUploadedFile would have been more appropriate than SimpleUploadedFile, as it enables easy reconstruction of the file from its content throughout the request lifecycle for all in-memory file wrappers.

I'd appreciate any second opinions or suggestions on this approach.

comment:3 by Vishy Algo, 3 weeks ago

Owner: changed from Dinesh Thumma to Vishy Algo

comment:4 by Vishy Algo, 3 weeks ago

Component: UncategorizedCore (Other)

comment:5 by Jacob Walls, 2 weeks ago

Component: Core (Other)File uploads/storage
Resolution: duplicate
Status: assignedclosed
Type: UncategorizedBug

Thanks, I'm a little sympathetic to your report, since it seems like a straightforward violation of the docs for File.open(), which claim it handles re-opening, and shouldn't be complicated to fix, but I'm going to respect the prior triage decision here in #33023. Feel free to start a forum discussion if you'd like to gather consensus to revisit.

Note: See TracTickets for help on using tickets.
Back to Top