boostorg / boostorg/website-v2
Missing profile image file causes a 500 on every page that renders an avatar
- Dominant language
- HTML
- Stars
- 18
- Forks
- 28
- Avg merge
- 2d 12h
- Merged PRs (30d)
- 77
Description
## 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`
```python
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
```bash
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
```sql
SELECT profile_image FROM users_user WHERE id = AUTHOR_ID;
```
### 3. Point the row at a file that does not exist
```sql
UPDATE users_user
SET profile_image = 'profile-images/does-not-exist.png'
WHERE id = AUTHOR_ID;
```
### 4. Observe the failure
Method level:
```bash
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:
```bash
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/
```
Returns `500`.
### 5. Restore the row
```sql
UPDATE users_user SET profile_image = '' 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.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.