Summary
User.get_thumbnail_url() raises FileNotFoundError instead of degrading
gracefully when a user's profile_image row points at a file that is not
present in storage. Any page that renders that user's avatar returns a 500
rather than falling back to the initials circle.
Where it raises
users/models.py:359
def get_thumbnail_url(self):
# convenience method for templates
if self.profile_image and self.image_thumbnail:
with suppress(AttributeError, MissingSource, FileNotFoundError, OSError):
return getattr(self.image_thumbnail, "url", None)
The exception escapes from the if condition, before the suppress context is
ever entered.
image_thumbnail is a django-imagekit ImageSpecField. Evaluating it for
truthiness calls ImageCacheFile.__bool__, which fires the
existence_required signal, which under the default JustInTime strategy calls
generate(), which opens the source file. When the source is missing that
raises FileNotFoundError while the condition is still being evaluated.
profile_image being truthy only means the field holds a name. It says nothing
about whether the underlying file exists, so the first half of the condition
does not protect the second.
Impact
get_thumbnail_url() is reached from get_avatar_url(), which is called from
at least:
ak/homepage.py community post cards, via news/models.py to_v3_post_card_dict
core/views.py:2319
libraries/models.py:620
news/services.py:49
users/views.py:237 and users/profile_cards.py:11
users/templatetags/avatar_tags.py:110 and :125
A single missing media object therefore takes down the homepage, library pages,
and profile pages. This is reachable in production whenever a stored object goes
missing, for example a partially failed upload or an object deleted out of the
bucket. It is hit reliably in local and review environments, where a database is
restored from a remote environment but the corresponding media files are not.
The same pattern is used by get_hq_image_url() for the hq_image field, which
users/templatetags/avatar_tags.py calls on the same render paths, so that
field is expected to fail the same way.
The docstring on get_avatar_url() already states the intended behaviour, so
the current handling contradicts the documented contract:
Returns empty string when no image is available so the avatar template
falls back to a colored initials circle.
Steps to reproduce
The setup requires a local database change, which is reverted at the end.
1. Find a user whose avatar the homepage renders
docker compose exec web python manage.py shell -c "
from news.models import Entry
qs = Entry.objects.ranked().filter(deleted_at__isnull=True, published=True)
for e in qs.select_related('author')[:5]:
print(e.author_id, e.author.email, repr(e.author.profile_image.name))
"
Pick any listed author and note their id as AUTHOR_ID.
2. Record the current value so it can be restored
SELECT profile_image FROM users_user WHERE id = AUTHOR_ID;
3. Point the row at a file that does not exist
UPDATE users_user
SET profile_image = 'profile-images/does-not-exist.png'
WHERE id = AUTHOR_ID;
4. Observe the failure
Method level:
docker compose exec web python manage.py shell -c "
from users.models import User
print(User.objects.get(pk=AUTHOR_ID).get_thumbnail_url())
"
Raises FileNotFoundError: [Errno 2] No such file or directory: '/code/media/profile-images/does-not-exist.png', with users/models.py line
361 in the traceback and imagekit/cachefiles/__init__.py __bool__ further
down.
Page level:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/
Returns 500.
5. Restore the row
UPDATE users_user SET profile_image = '<value from step 2>' WHERE id = AUTHOR_ID;
Expected behaviour
get_thumbnail_url() returns None when the source file cannot be opened.
get_avatar_url() then continues down its existing fallback chain, ending at
the empty string that triggers the initials circle.
get_hq_image_url() behaves the same way for a missing hq_image.
- Pages that render avatars return 200 with a fallback avatar for the affected
user, and are unchanged for every user whose file is present.
Acceptance criteria
Summary
User.get_thumbnail_url()raisesFileNotFoundErrorinstead of degradinggracefully when a user's
profile_imagerow points at a file that is notpresent in storage. Any page that renders that user's avatar returns a 500
rather than falling back to the initials circle.
Where it raises
users/models.py:359The exception escapes from the
ifcondition, before thesuppresscontext isever entered.
image_thumbnailis a django-imagekitImageSpecField. Evaluating it fortruthiness calls
ImageCacheFile.__bool__, which fires theexistence_requiredsignal, which under the default JustInTime strategy callsgenerate(), which opens the source file. When the source is missing thatraises
FileNotFoundErrorwhile the condition is still being evaluated.profile_imagebeing truthy only means the field holds a name. It says nothingabout whether the underlying file exists, so the first half of the condition
does not protect the second.
Impact
get_thumbnail_url()is reached fromget_avatar_url(), which is called fromat least:
ak/homepage.pycommunity post cards, vianews/models.pyto_v3_post_card_dictcore/views.py:2319libraries/models.py:620news/services.py:49users/views.py:237andusers/profile_cards.py:11users/templatetags/avatar_tags.py:110and:125A single missing media object therefore takes down the homepage, library pages,
and profile pages. This is reachable in production whenever a stored object goes
missing, for example a partially failed upload or an object deleted out of the
bucket. It is hit reliably in local and review environments, where a database is
restored from a remote environment but the corresponding media files are not.
The same pattern is used by
get_hq_image_url()for thehq_imagefield, whichusers/templatetags/avatar_tags.pycalls on the same render paths, so thatfield is expected to fail the same way.
The docstring on
get_avatar_url()already states the intended behaviour, sothe current handling contradicts the documented contract:
Steps to reproduce
The setup requires a local database change, which is reverted at the end.
1. Find a user whose avatar the homepage renders
Pick any listed author and note their id as
AUTHOR_ID.2. Record the current value so it can be restored
3. Point the row at a file that does not exist
4. Observe the failure
Method level:
Raises
FileNotFoundError: [Errno 2] No such file or directory: '/code/media/profile-images/does-not-exist.png', withusers/models.pyline361 in the traceback and
imagekit/cachefiles/__init__.py__bool__furtherdown.
Page level:
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/Returns
500.5. Restore the row
Expected behaviour
get_thumbnail_url()returnsNonewhen the source file cannot be opened.get_avatar_url()then continues down its existing fallback chain, ending atthe empty string that triggers the initials circle.
get_hq_image_url()behaves the same way for a missinghq_image.user, and are unchanged for every user whose file is present.
Acceptance criteria
get_thumbnail_url()returnsNonerather than raising when the sourcefile is missing.
get_hq_image_url()returnsNonerather than raising when thehq_imagesource file is missing.
get_avatar_url()returns the empty string that triggers the initialscircle.
a user whose
profile_imagepoints at a missing file.