boostorg / boostorg/website-v2

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

Open
#2,638 0 comments 0 reactions 1 assignee Claimed by @herzog0 View on GitHub
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.