Repository: WordPress/openverse-api

Issue:
Invalid UUID breaks related images query
## Description
<!-- Concisely describe the bug. Compare your experience with what you expected to happen. -->
<!-- For example: "I clicked the 'submit' button and instead of seeing a thank you message, I saw a blank page." -->
It appears that if an invalid UUID-like string is supplied to a `/related/` URL, the UUID passes filters and an `IndexError` is raised.

```python
IndexError: list index out of range
  File "django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "rest_framework/viewsets.py", line 125, in view
    return self.dispatch(request, *args, **kwargs)
  File "rest_framework/views.py", line 509, in dispatch
    response = self.handle_exception(exc)
  File "rest_framework/views.py", line 469, in handle_exception
    self.raise_uncaught_exception(exc)
  File "rest_framework/views.py", line 480, in raise_uncaught_exception
    raise exc
  File "rest_framework/views.py", line 506, in dispatch
    response = handler(request, *args, **kwargs)
  File "django/utils/decorators.py", line 46, in _wrapper
    return bound_method(*args, **kwargs)
  File "catalog/api/views/media_views.py", line 99, in related
    results, num_results = related_media(
  File "catalog/api/controllers/elasticsearch/related.py", line 19, in related_media
    _id = item.execute().hits[0].id
  File "elasticsearch_dsl/utils.py", line 84, in __getitem__
    l = self._l_[k]
```

Here are some examples of invalid yet UUID-like identifiers:

<details> <summary>Invalid UUIDs</summary>

- `95-9297ada227947df116f43b87f0978ae-2`
- `3975690f43-7d1a9b6613-b25492d442--fe`
- `00000000000000000000000000000000770-`
- `00000000000000000000000000000000771-`
- `00000000000000000000000000000000777a`
- `000000000000000000000000000000007779`
- `000000000000000000000000000000007775`
- `000000000000000000000000000000007773`
- `000000000000000000000000000000007771`
- `000000000000000000000000000000007770`
- `00000000000000000000000000000000666-`
- `00000000000000000000000000000000555-`
- `00000000000000000000000000000000333-`
- `00000000000000000000000000000000111-`
- `00000000000000000000000000000000000-`
- `77700000000000000000000000000000000-`
- `77700-000000000000000000000000000000`
- `00000000000000000000000000000000777-`
- `777041000000000000000000000000000000`
- `777021000000000000000000000000000000`
- `777031000000000000000000000000000000`
- `777011000000000000000000000000000000`
- `700050000000000000000000000000000000`
- `777000500000000000000000000000000000`
- `770005000000000000000000000000000000`
- `777000050000000000000000000000000000`
- `6770a0000000000000000000000000000000`
- `7670a0000000000000000000000000000000`
- `7760a0000000000000000000000000000000`
- `7770a1000000000000000000000000000000`
- `7770a0000000000000000000000000000000`
- `777090000000000000000000000000000000`
- `777050000000000000000000000000000000`
- `777030000000000000000000000000000000`
- `777010000000000000000000000000000000`
- `777000000000000000000000000000000000`
- `7770a0000100004500700090aa0000000cde`
- `777090000100004500700090990000000cde`
- `777050000100004500700090550000000cde`
- `777030000100004500700090330000000cde`
- `777010000100004500700090110000000cde`
- `777000000100004500700090000000000cde`
- `7770a0000122334566788890aa0000000cde`
- `777050000122334566788890550000000cde`
- `777090000122334566788890990000000cde`
- `777030000122334566788890330000000cde`
- `777010000122334566788890110000000cde`
- `777000000122334566788890000000000cde`
- `7770a000012233456678889aaaaa00000cde`
- `77705000012233456678889a55aa00000cde`
- `77709000012233456678889a99aa00000cde`
- `77703000012233456678889a33aa00000cde`
- `77701000012233456678889a11aa00000cde`
- `77700000012233456678889a00aa00000cde`
- `777010112334456778999a------cccccdef`
- `77700-1112334456778999a-----cccccdef`
- `77701-33cf1ca9cc544e-89d--77c29--106`
- `001112334456777778999a------cccccdef`
- `000000000000000000000000000000000001`
- `000000000000000000000000000000000010`
- `00020000000000008000a00000000000de00`
- `01120000000000008000a00000000000de00`
- `01123344555777778999a------cccccdeff`
- `77705f53cf1ca9cc544e-89d--77c29--106`
- `0-135f53cf1ca9cc544e-89d--77c29--106`
- `7-135f53cf1ca9cc544e-89d--77c29--106`
- `000000000000000000000000000000000000`
- `000000000000000000000000000000000010`
- `000000000000000000000000000000000001`
- `0001000000400000008000a000cd00000001`
- `0000000000400000008000a000cd00000001`
- `0001100000400000008000a000cd00000001`
- `0001110000003000008000a000cd00000001`
- `0001110000004000000070a000cd00000001`
- `00011100000040000000800090cd00000001`
- `000111000000400000008000a000b0000001`
- `000111000000400000008000a000cc000001`
- `000111000000400000008000a000cd000000`
- `000000000000400000008000a000cd000000`
- `000111223333455666778999abbbcdeeffff`
- `f716f2b1a293f990b108e6f53dbc4357e061`
- `f716f2b1a2933f990b108e6f3dbc4357e061`
- `f716f2ba2933f990b108e6f53dbc4357e061`
- `f716f2b1a2933f90b108e6f53dbc4357e061`
- `f716f2b1a2933f990b1086f53dbc4357e061`
- `f716f2b1a233f990b108e6f53dbc4357e061`
- `f716f2b12933f990b108e6f53dbc4357e061`
- `f16f2b1a2933f990b108e6f53dbc4357e061`
- `f716f2b1a2933f990b108e653dbc4357e061`
- `716f2b1a2933f990b108e6f53dbc4357e061`
- `f716f2b1a2933f990b108e6f53bc4357e061`
- `f716f2b1a2933f990b108e6f53dc4357e061`
- `f716f2b1a2933f990b108e6f53db4357e061`
- `f716f2b1a2933f990b108e6f53dbc357e061`
- `f716f2b1a2933f990b108e6f53dbc457e061`
- `f716f2b1a2933f990b108e6f53dbc437e061`
- `f716f2b1a2933f990b108e6f53dbc435e061`
- `f716f2b1a2933f990b108e6f53dbc4357061`
- `f716f2b1a2933f990b108e6f53dbc4357e61`
- `f716f2b1a2933f0b108e6f53dbc4357e0610`
- `f716f2b1a2933f990b108e53dbc4357e0610`
- `f716f2b1a293990b108e6f53dbc4357e0610`
- `f716f2b1a2933f990b106f53dbc4357e0610`
- `f716f2b1a293390b108e6f53dbc4357e0610`
- `f6f2b1a2933f990b108e6f53dbc4357e0610`
- `f716f2b1a23f990b108e6f53dbc4357e0610`
- `f716f2b1a2933f990b108e63dbc4357e0610`
- `f716f2b1a33f990b108e6f53dbc4357e0610`
- `f7f2b1a2933f990b108e6f53dbc4357e0610`
- `f716f2b1a2933f990b108e6fdbc4357e0610`
- `f716f2b1a2933f990b108e6f5bc4357e0610`
- `f716f2b1a2933f990b108e6f53c4357e0610`
- `f716f2b1a2933f990b108e6f53d4357e0610`
- `f716f2b1a2933f990b108e6f53db357e0610`
- `f716f2b1a2933f990b108e6f53dbc57e0610`
- `f716f2b1a2933f990b108e6f53dbc43e0610`
- `f716f2b1a2933f990b108e6f53dbc47e0610`
- `f716f2b1a2933f990b108e6f53dbc4350610`
- `f716f2b1a2933f990b108e6f53dbc4357610`
- `f716f2b1a2933f990b108e6dbc4357e06108`
- `f71b1a2933f990b108e6f53dbc4357e06108`
- `f716f22933f990b108e6f53dbc4357e06108`
- `f716f2b1a29390b108e6f53dbc4357e06108`
- `f716f2b1a2933f9908e6f53dbc4357e06108`
- `f716f2b1a29330b108e6f53dbc4357e06108`
- `6f2b1a2933f990b108e6f53dbc4357e06108`
- `f72b1a2933f990b108e6f53dbc4357e06108`
- `f716f2b1a2933f990b108e6fbc4357e06108`
- `f716f2b1a2933f990b108e6f5c4357e06108`
- `f716f2b1a2933f990be6f53dbc4357e06108`
- `f716f2b1a2933f990b108e6f534357e06108`
- `f716f2b1a2933f990b108e6f53d357e06108`
- `f716f2b1a2933f990b108e6f53db57e06108`
- `f716f2b1a2933f990b108e6f53dbc7e06108`
- `f716f2b1a2933f990b108e6f53dbc4e06108`
- `f716f2b1a2933f990b108e6f53dbc4306108`
- `f716f2b1a2933f990b108e6f53dbc4356108`
- `f716f2b1a2933f990b108e6f53dbc4357108`
- `f716f2b1a2933f990b108e6f53dbc4357e08`
- `f716f2b1a2933f990e6f53dbc4357e061081`
- `f716f2b1a2990b108e6f53dbc4357e061081`
- `f2b1a2933f990b108e6f53dbc4357e061081`
- `f716f2b1a2933f998e6f53dbc4357e061081`
- `f716f2b1a2933f990b108e6f53d57e061081`
- `f711a2933f990b108e6f53dbc4357e061081`
- `f716f2b1a2933f990b108e6fc4357e061081`
- `f716f2b1a2933f990b108e6bc4357e061081`
- `f716f2b33f990b108e6f53dbc4357e061081`
- `f716f2b1a2930b108e6f53dbc4357e061081`
- `f716f2b1a2933f990b108e6f53dbce061081`
- `f716f2b1a2933f990b108e6f53dbc4061081`
- `f716f2b1a2933f990b108e6f53dbc4351081`
- `f716f2b1a2933f990b108e6f53dbc4361081`
- `f716f2b1a2933f990b108e6f53dbc4357081`
- `f716f2b1a2933f990b108e6f53dbc4357e81`
- `f716f2b1a2933f990b108e6f53dbc4357e01`
- `f7162933f990b108e6f53dbc4357e0610810`
- `f716f2b1a2933f990b153dbc4357e0610810`
- `f716f2b1a2933f990b108e6f5357e0610810`
- `f716f2b1a290b108e6f53dbc4357e0610810`
- `f716f2b1a2933f990b108e6c4357e0610810`
- `f716f2b1a2933f9906f53dbc4357e0610810`
- `f716f233f990b108e6f53dbc4357e0610810`
- `f716f2b1a2933108e6f53dbc4357e0610810`
- `f716f2b1a2933f990b108e6f53d7e0610810`
- `f716f2b1a2933f99e6f53dbc4357e0610810`
- `f716f2b1a2933f990b108e6f53dbe0610810`
- `f716f2b1a2933f990b108e6f53dbc0610810`
- `f716f2b1a2933f990b108e6f53dbc4610810`
- `f716f2b1a2933f990b108e6f53dbc4310810`
- `f716f2b1a2933f990b108e6f53dbc4350810`
- `f716f2b1a2933f990b108e6f53dbc4357e10`
- `f716f2b1a2933f990b108e6f53dbc4357810`
- `f716f2b1a2933f990b108e6f53dbc4357e06`
- `f716f2b1a2933f990b108e6f53dbc4357e00`
- `000000000000000000000000000000000000`
- `-decb5f1ba-91a3e18933826-19dd74a3ce6`

</details>


## Reproduction
<!-- Provide detailed steps to reproduce the bug. -->
1. Visit http://localhost:8000/v1/images/000000000000000000000000000000000000/related/ (or any similarly invalid UUID related page)
2. Observe 500 error

## Additional context
<!-- Add any other context about the problem here; or delete the section entirely. -->
Sentry link: https://sentry.io/share/issue/17c9afdf7ab14723bcf2a861db42b529/

## Resolution
<!-- Replace the [ ] with [x] to check the box. -->
- [ ] 🙋 I would be interested in resolving this bug.



Discussion:
can't we just validate the  8-4-4-4-12 structure of UUIDs?
We could! I think that'd be a solid mitigation. Would you like to tackle this @raghuvar-arora-au2? 😄 
would love to!
While the regex here 
https://github.com/WordPress/openverse-api/blob/c2df644e5faec7013412080a0a258117ae4ccaf8/api/catalog/api/views/media_views.py#L28
can and should certainly be tightened, that would still leave the issue of the UUIDs not actually existing in the database. 

Would a [`get()` check](https://docs.djangoproject.com/en/4.0/ref/models/querysets/#get) be too expensive to perform as a validation? If not, this can be solved in a simpler (and more solid way) by replacing this line
https://github.com/WordPress/openverse-api/blob/c2df644e5faec7013412080a0a258117ae4ccaf8/api/catalog/api/views/media_views.py#L108
with 
```py
uuid=self.get_object().identifier
```
which invokes [`get_object()`](https://www.django-rest-framework.org/api-guide/viewsets/#genericviewset) with the UUID fragment, which raises `DoesNotExist` and returns a 404.
I _think_ that with something that actually looks like a proper UUID, we currently get a real 404 instead of the 500 you see with other things that are the same length but not actually patterned against a UUID:

Compare:

https://api.openverse.engineering/v1/images/f716f2b1a2933f990b108e653dbc4357e061/related/

(Would not pattern match against a UUID)

to:

https://api.openverse.engineering/v1/images/da5cb478-c093-4d62-b721-cda18797e3fz/related/

(Does pattern match against a UUID but does not correspond to an actual image record)

I also confirmed that UUID doesn't happen to be an audio one and mysteriously doesn't break because of that or something: https://api.openverse.engineering/v1/audio/da5cb478-c093-4d62-b721-cda18797e3fz/related/

It might be that this ticket really just needs the regex to be refined as you suggested.
I was thinking it just needs to catch the `IndexError` exception, currently only catches the ones for `ValueError`.
Reassigning this issue to @obulat given she has a PR open for it already. Sorry @raghuvar-arora-au2 :confused: 
yikes
@raghuvar-arora-au2 , I'm really sorry for interrupting your work on this. Actually, as @dhruvkb [commented](https://github.com/WordPress/openverse-api/pull/766#pullrequestreview-1015937754), we still need to tighten the regex for the UUID. 
So, I've re-assigned the issue of improving the `UUID` regex to you. Please, let us know if you're still interested.

Resolving patch:
diff --git a/api/catalog/api/views/media_views.py b/api/catalog/api/views/media_views.py
--- a/api/catalog/api/views/media_views.py
+++ b/api/catalog/api/views/media_views.py
@@ -25,7 +25,9 @@ class MediaViewSet(ReadOnlyModelViewSet):
 
     lookup_field = "identifier"
     # TODO: https://github.com/encode/django-rest-framework/pull/6789
-    lookup_value_regex = r"[0-9a-f\-]{36}"  # highly simplified approximation
+    lookup_value_regex = (
+        r"[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}"
+    )
 
     pagination_class = StandardPagination
 


Test patch:
diff --git a/api/test/audio_integration_test.py b/api/test/audio_integration_test.py
--- a/api/test/audio_integration_test.py
+++ b/api/test/audio_integration_test.py
@@ -21,6 +21,7 @@
     thumb_compression,
     thumb_full_size,
     thumb_webp,
+    uuid_validation,
 )
 
 import pytest
@@ -115,3 +116,9 @@ def test_audio_report(audio_fixture):
 
 def test_audio_license_filter_case_insensitivity():
     license_filter_case_insensitivity("audio")
+
+
+def test_audio_uuid_validation():
+    uuid_validation("audio", "123456789123456789123456789123456789")
+    uuid_validation("audio", "12345678-1234-5678-1234-1234567891234")
+    uuid_validation("audio", "abcd")
diff --git a/api/test/image_integration_test.py b/api/test/image_integration_test.py
--- a/api/test/image_integration_test.py
+++ b/api/test/image_integration_test.py
@@ -21,6 +21,7 @@
     thumb_compression,
     thumb_full_size,
     thumb_webp,
+    uuid_validation,
 )
 from urllib.parse import urlencode
 
@@ -132,3 +133,9 @@ def test_oembed_endpoint_for_xml():
 
 def test_image_license_filter_case_insensitivity():
     license_filter_case_insensitivity("images")
+
+
+def test_image_uuid_validation():
+    uuid_validation("images", "123456789123456789123456789123456789")
+    uuid_validation("images", "12345678-1234-5678-1234-1234567891234")
+    uuid_validation("images", "abcd")
diff --git a/api/test/media_integration.py b/api/test/media_integration.py
--- a/api/test/media_integration.py
+++ b/api/test/media_integration.py
@@ -164,3 +164,8 @@ def license_filter_case_insensitivity(media_type):
     response = requests.get(f"{API_URL}/v1/{media_type}?license=bY", verify=False)
     parsed = json.loads(response.text)
     assert parsed["result_count"] > 0
+
+
+def uuid_validation(media_type, identifier):
+    response = requests.get(f"{API_URL}/v1/{media_type}/{identifier}", verify=False)
+    assert response.status_code == 404