[Django] #37191: FileBasedCache.touch() raises ValueError: I/O operation on closed file when the key is already expired

6 views
Skip to first unread message

Django

unread,
Jun 26, 2026, 7:21:58 AM (5 days ago) Jun 26
to django-...@googlegroups.com
#37191: FileBasedCache.touch() raises ValueError: I/O operation on closed file when
the key is already expired
-------------------------------------+-------------------------------------
Reporter: dkr13 | Type: Bug
Status: new | Component: Core
| (Cache system)
Version: 6.0 | Severity: Normal
Keywords: cache, | Triage Stage:
FileBasedCache, touch | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------------+-------------------------------------
FileBasedCache.touch() raises ValueError: I/O operation on closed file
when called on a key whose timeout has already elapsed (but whose file
hasn't been lazily cleaned up yet).

**Steps to reproduce:**

Use the following settings

{{{
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache",
},
}
}}}

Run the following (e.g. from the django shell)
{{{
from django.core.cache import cache

cache.add("key", "value", timeout=1)
time.sleep(2)
cache.touch("k", 60)
}}}

**Result:** The last `cache.touch` call raises ValueError: I/O operation
on closed file

**Expected:** touch() to return False

**Stacktrace:**

{{{
Traceback (most recent call last):
File "<console>", line 1, in <module>
File ".../django_cache_test/.venv/lib/python3.13/site-
packages/django/core/cache/backends/filebased.py", line 76, in touch
locks.unlock(f)
~~~~~~~~~~~~^^^
File ".../django_cache_test/.venv/lib/python3.13/site-
packages/django/core/files/locks.py", line 127, in unlock
fcntl.flock(_fd(f), fcntl.LOCK_UN)
~~~^^^
File ".../django_cache_test/.venv/lib/python3.13/site-
packages/django/core/files/locks.py", line 27, in _fd
return f.fileno() if hasattr(f, "fileno") else f
~~~~~~~~^^
ValueError: I/O operation on closed file
}}}


----


I assume the issue lies in the fileBasedCache class in
`django/core/cache/backends/filebased.py`

{{{
try:
locks.lock(f, locks.LOCK_EX)
if self._is_expired(f):
return False
else:
previous_value = pickle.loads(zlib.decompress(f.read()))
f.seek(0)
self._write_content(f, timeout, previous_value)
return True
finally:
locks.unlock(f)
}}}

`self._is_expired()` closes the file when it detects that the cache
expired and returns False. The finally block is still executed and tries
to access the previously closed file.
--
Ticket URL: <https://code.djangoproject.com/ticket/37191>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

Django

unread,
Jun 26, 2026, 8:07:30 AM (5 days ago) Jun 26
to django-...@googlegroups.com
#37191: FileBasedCache.touch() raises ValueError: I/O operation on closed file when
the key is already expired
-------------------------------------+-------------------------------------
Reporter: Daniel | Owner: (none)
Type: Bug | Status: new
Component: Core (Cache system) | Version: 6.0
Severity: Normal | Resolution:
Keywords: cache, | Triage Stage:
FileBasedCache, touch | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------------+-------------------------------------
Comment (by Ayoub Bouaik):

The issue stems from how the **finally** block interacts with
{{{locks.unlock(f)}}} .When the file is expired, {{{_is_expired(f)}}}
closes the file descriptor.

Then, the finally block executes {{{locks.unlock(f)}}} which routes
through the internal {{{_fd(f)}}} helper:

{{{
def _fd(f):
"""Get a filedescriptor from something which could be a file or an
fd."""
return f.fileno() if hasattr(f, "fileno") else f

}}}


Because f has the {{{fileno}}} attribute but is already closed, calling
{{{f.fileno() throws the ValueError: I/O operation on closed file.}}}

We can safely fix this by wrapping the unlock utility in a check to ensure
the file descriptor is still active:
{{{
finally:
if not f.closed:
locks.unlock(f)
}}}

I have reproduced the issue locally and confirmed this fix stops the crash
and correctly returns **False**. I'd be happy to open a pull request with
this fix and a regression test if that works
--
Ticket URL: <https://code.djangoproject.com/ticket/37191#comment:1>

Django

unread,
Jun 26, 2026, 8:08:14 AM (5 days ago) Jun 26
to django-...@googlegroups.com
#37191: FileBasedCache.touch() raises ValueError: I/O operation on closed file when
the key is already expired
-------------------------------------+-------------------------------------
Reporter: Daniel | Owner: Ayoub
| Bouaik
Type: Bug | Status: assigned
Component: Core (Cache system) | Version: 6.0
Severity: Normal | Resolution:
Keywords: cache, | Triage Stage:
FileBasedCache, touch | Unreviewed
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Ayoub Bouaik):

* owner: (none) => Ayoub Bouaik
* status: new => assigned

--
Ticket URL: <https://code.djangoproject.com/ticket/37191#comment:2>

Django

unread,
Jun 26, 2026, 8:46:54 AM (5 days ago) Jun 26
to django-...@googlegroups.com
#37191: FileBasedCache.touch() raises ValueError: I/O operation on closed file when
the key is already expired
-------------------------------------+-------------------------------------
Reporter: Daniel | Owner: Ayoub
| Bouaik
Type: Bug | Status: assigned
Component: Core (Cache system) | Version: 6.0
Severity: Normal | Resolution:
Keywords: cache, | Triage Stage: Accepted
FileBasedCache, touch |
Has patch: 0 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls):

* stage: Unreviewed => Accepted


Old description:

> FileBasedCache.touch() raises ValueError: I/O operation on closed file
> when called on a key whose timeout has already elapsed (but whose file
> hasn't been lazily cleaned up yet).
>
> **Steps to reproduce:**
>
> Use the following settings
>
> {{{
> CACHES = {
> "default": {
> "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
> "LOCATION": "/var/tmp/django_cache",
> },
> }
> }}}
>
> Run the following (e.g. from the django shell)
> {{{
> from django.core.cache import cache
>
> cache.add("key", "value", timeout=1)
> time.sleep(2)
> cache.touch("k", 60)
> }}}
>
> **Result:** The last `cache.touch` call raises ValueError: I/O operation
> on closed file
>
> **Expected:** touch() to return False
>
> **Stacktrace:**
>
> {{{
> Traceback (most recent call last):
> File "<console>", line 1, in <module>
> File ".../django_cache_test/.venv/lib/python3.13/site-
> packages/django/core/cache/backends/filebased.py", line 76, in touch
> locks.unlock(f)
> ~~~~~~~~~~~~^^^
> File ".../django_cache_test/.venv/lib/python3.13/site-
> packages/django/core/files/locks.py", line 127, in unlock
> fcntl.flock(_fd(f), fcntl.LOCK_UN)
> ~~~^^^
> File ".../django_cache_test/.venv/lib/python3.13/site-
> packages/django/core/files/locks.py", line 27, in _fd
> return f.fileno() if hasattr(f, "fileno") else f
> ~~~~~~~~^^
> ValueError: I/O operation on closed file
> }}}
>

> ----
>

> I assume the issue lies in the fileBasedCache class in
> `django/core/cache/backends/filebased.py`
>
> {{{
> try:
> locks.lock(f, locks.LOCK_EX)
> if self._is_expired(f):
> return False
> else:
> previous_value = pickle.loads(zlib.decompress(f.read()))
> f.seek(0)
> self._write_content(f, timeout, previous_value)
> return True
> finally:
> locks.unlock(f)
> }}}
>
> `self._is_expired()` closes the file when it detects that the cache
> expired and returns False. The finally block is still executed and tries
> to access the previously closed file.

New description:

FileBasedCache.touch() raises ValueError: I/O operation on closed file
when called on a key whose timeout has already elapsed (but whose file
hasn't been lazily cleaned up yet).

**Steps to reproduce:**

Use the following settings

{{{
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
"LOCATION": "/var/tmp/django_cache",
},
}
}}}

Run the following (e.g. from the django shell)
{{{
from django.core.cache import cache

cache.add("key", "value", timeout=1)
time.sleep(2)
cache.touch("key", 60)
}}}

**Result:** The last `cache.touch` call raises ValueError: I/O operation
on closed file

**Expected:** touch() to return False

**Stacktrace:**

{{{
Traceback (most recent call last):
File "<console>", line 1, in <module>
File ".../django_cache_test/.venv/lib/python3.13/site-
packages/django/core/cache/backends/filebased.py", line 76, in touch
locks.unlock(f)
~~~~~~~~~~~~^^^
File ".../django_cache_test/.venv/lib/python3.13/site-
packages/django/core/files/locks.py", line 127, in unlock
fcntl.flock(_fd(f), fcntl.LOCK_UN)
~~~^^^
File ".../django_cache_test/.venv/lib/python3.13/site-
packages/django/core/files/locks.py", line 27, in _fd
return f.fileno() if hasattr(f, "fileno") else f
~~~~~~~~^^
ValueError: I/O operation on closed file
}}}


----


I assume the issue lies in the fileBasedCache class in
`django/core/cache/backends/filebased.py`

{{{
try:
locks.lock(f, locks.LOCK_EX)
if self._is_expired(f):
return False
else:
previous_value = pickle.loads(zlib.decompress(f.read()))
f.seek(0)
self._write_content(f, timeout, previous_value)
return True
finally:
locks.unlock(f)
}}}

`self._is_expired()` closes the file when it detects that the cache
expired and returns False. The finally block is still executed and tries
to access the previously closed file.

--
--
Ticket URL: <https://code.djangoproject.com/ticket/37191#comment:3>

Django

unread,
Jun 26, 2026, 8:56:03 AM (4 days ago) Jun 26
to django-...@googlegroups.com
#37191: FileBasedCache.touch() raises ValueError: I/O operation on closed file when
the key is already expired
-------------------------------------+-------------------------------------
Reporter: Daniel | Owner: Ayoub
| Bouaik
Type: Bug | Status: assigned
Component: Core (Cache system) | Version: 6.0
Severity: Normal | Resolution:
Keywords: cache, | Triage Stage: Accepted
FileBasedCache, touch |
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Ayoub Bouaik):

* has_patch: 0 => 1

--
Ticket URL: <https://code.djangoproject.com/ticket/37191#comment:4>

Django

unread,
Jun 26, 2026, 12:48:11 PM (4 days ago) Jun 26
to django-...@googlegroups.com
#37191: FileBasedCache.touch() raises ValueError: I/O operation on closed file when
the key is already expired
-------------------------------------+-------------------------------------
Reporter: Daniel | Owner: Ayoub
| Bouaik
Type: Bug | Status: assigned
Component: Core (Cache system) | Version: 6.0
Severity: Normal | Resolution:
Keywords: cache, | Triage Stage: Ready for
FileBasedCache, touch | checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls):

* stage: Accepted => Ready for checkin

--
Ticket URL: <https://code.djangoproject.com/ticket/37191#comment:5>

Django

unread,
Jun 26, 2026, 1:28:20 PM (4 days ago) Jun 26
to django-...@googlegroups.com
#37191: FileBasedCache.touch() raises ValueError: I/O operation on closed file when
the key is already expired
-------------------------------------+-------------------------------------
Reporter: Daniel | Owner: Ayoub
| Bouaik
Type: Bug | Status: closed
Component: Core (Cache system) | Version: 6.0
Severity: Normal | Resolution: fixed
Keywords: cache, | Triage Stage: Ready for
FileBasedCache, touch | checkin
Has patch: 1 | Needs documentation: 0
Needs tests: 0 | Patch needs improvement: 0
Easy pickings: 1 | UI/UX: 0
-------------------------------------+-------------------------------------
Changes (by Jacob Walls <jacobtylerwalls@…>):

* resolution: => fixed
* status: assigned => closed

Comment:

In [changeset:"d058058976496cedf3f36fa303a0327af38cdeaa" d0580589]:
{{{#!CommitTicketReference repository=""
revision="d058058976496cedf3f36fa303a0327af38cdeaa"
Fixed #37191 -- Prevented ValueError in FileBasedCache.touch() for expired
keys.
}}}
--
Ticket URL: <https://code.djangoproject.com/ticket/37191#comment:6>
Reply all
Reply to author
Forward
0 new messages