Repository: WordPress/openverse-api

Issue:
Thumbnail endpoint returns 500 if the upstream image URL 404s
## 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." -->
I was looking through production logs to see where some 500 errors were coming from and I found a handful of logs that look like this:

Field | Value
-- | --
`@ingestionTime` | 1659625225674
`@log` | 140733703834:/prod/api/nginx
`@logStream` | i-05a7072edd44e97d9
`@timestamp` | 1659625221900
body_bytes_sent | 66
host_header | api.openverse.engineering
http_referrer | https://search.openverse.engineering/
http_user_agent | Mozilla/5.0  (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36  (KHTML, like Gecko) Chrome/103.0.5060.134 Mobile Safari/537.36  (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
http_x_forwarded_for | [redacted]
remote_addr | [redacted]
request | GET /v1/images/81954166-168e-41a6-bff5-eb966cb940ec/thumb/ HTTP/1.1
request_time | 1.175
status | 500
time_local | 04/Aug/2022:15:00:21 +0000
upstream_response_time | 1.175

Indeed, if you visit `https://api.openverse.engineering/v1/images/81954166-168e-41a6-bff5-eb966cb940ec/thumb/` you will get a 500 back with a message saying that the upstream request resulted in a 404. I think these should just 404 all the way, instead of resulting in a 500. 

I wonder if these should also be marked as dead links? It should already be anyway because the upstream url 404s and this is how we detect dead links during the search request, but clearly some still get through :thinking: 

## Reproduction
<!-- Provide detailed steps to reproduce the bug. -->
1. <!-- Step 1 ... --> Visit https://api.openverse.engineering/v1/images/81954166-168e-41a6-bff5-eb966cb940ec/thumb/
2. <!-- Step 2 ... --> See the 500 response code with the 404 body details.

## Resolution
<!-- Replace the [ ] with [x] to check the box. -->
- [ ] 🙋 I would be interested in resolving this bug.



Discussion:

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
@@ -9,9 +9,12 @@
 from django.http.response import HttpResponse
 from rest_framework import status
 from rest_framework.decorators import action
+from rest_framework.exceptions import APIException
 from rest_framework.response import Response
 from rest_framework.viewsets import ReadOnlyModelViewSet
 
+from sentry_sdk import capture_exception
+
 from catalog.api.controllers import search_controller
 from catalog.api.models import ContentProvider
 from catalog.api.serializers.provider_serializers import ProviderSerializer
@@ -20,6 +23,11 @@
 from catalog.custom_auto_schema import CustomAutoSchema
 
 
+class UpstreamThumbnailException(APIException):
+    status_code = status.HTTP_424_FAILED_DEPENDENCY
+    default_detail = "Could not render thumbnail due to upstream provider error."
+
+
 class MediaViewSet(ReadOnlyModelViewSet):
     swagger_schema = CustomAutoSchema
 
@@ -189,9 +197,11 @@ def _thumbnail_proxy_comm(
 
             return upstream_response, res_status, content_type
         except (HTTPError, RemoteDisconnected, TimeoutError) as exc:
-            raise get_api_exception(f"Failed to render thumbnail: {exc}")
+            capture_exception(exc)
+            raise UpstreamThumbnailException(f"Failed to render thumbnail: {exc}")
         except Exception as exc:
-            raise get_api_exception(
+            capture_exception(exc)
+            raise UpstreamThumbnailException(
                 f"Failed to render thumbnail due to unidentified exception: {exc}"
             )
 


Test patch:
diff --git a/api/test/factory/models/image.py b/api/test/factory/models/image.py
new file mode 100644
--- /dev/null
+++ b/api/test/factory/models/image.py
@@ -0,0 +1,8 @@
+from test.factory.models.media import MediaFactory
+
+from catalog.api.models.image import Image
+
+
+class ImageFactory(MediaFactory):
+    class Meta:
+        model = Image
diff --git a/api/test/unit/views/media_views_test.py b/api/test/unit/views/media_views_test.py
new file mode 100644
--- /dev/null
+++ b/api/test/unit/views/media_views_test.py
@@ -0,0 +1,40 @@
+from test.factory.models.image import ImageFactory
+from unittest import mock
+from urllib.error import HTTPError
+
+from rest_framework.test import APIClient
+
+import pytest
+
+from catalog.api.models.image import Image
+
+
+@pytest.fixture
+def api_client() -> APIClient:
+    return APIClient()
+
+
+@pytest.fixture
+def image() -> Image:
+    return ImageFactory.create()
+
+
+@pytest.mark.django_db
+def test_thumb_error(api_client, image):
+    error = None
+
+    def urlopen_503_response(url, **kwargs):
+        nonlocal error
+        error = HTTPError(url, 503, "Bad error upstream whoops", {}, None)
+        raise error
+
+    with mock.patch(
+        "catalog.api.views.media_views.urlopen"
+    ) as urlopen_mock, mock.patch(
+        "catalog.api.views.media_views.capture_exception", autospec=True
+    ) as mock_capture_exception:
+        urlopen_mock.side_effect = urlopen_503_response
+        response = api_client.get(f"/v1/images/{image.identifier}/thumb/")
+
+    assert response.status_code == 424
+    mock_capture_exception.assert_called_once_with(error)