Skip to content

Missing profile image file causes a 500 on every page that renders an avatar #2638

Description

@herzog0

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

  • get_thumbnail_url() returns None rather than raising when the source
    file is missing.
  • get_hq_image_url() returns None rather than raising when the hq_image
    source file is missing.
  • get_avatar_url() returns the empty string that triggers the initials
    circle.
  • The homepage, a library page, and a user profile page all return 200 with
    a user whose profile_image points at a missing file.
  • Behaviour is unchanged when the file exists.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions