We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Thumbnail timeouts raise `TimeoutError`
## 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." -->
We recently changed the thumbnail service we're using and some of the business logic around it (#630). The previous thumbnail retrieval step [did not have a timeout](https://github.com/WordPress/openverse-api/blame/0df21059dfe579be1e7a83f64e8e8a7d4e422416/api/catalog/api/views/media_views.py#L156), whereas the new thumbnail retrieval step has a timeout of 5 seconds:

https://github.com/WordPress/openverse-api/blob/03362db8caed8014e31d3fe5274c9bb084086b85/api/catalog/api/views/media_views.py#L173

We should also add a case for the `TimeoutError` exception here:

https://github.com/WordPress/openverse-api/blob/03362db8caed8014e31d3fe5274c9bb084086b85/api/catalog/api/views/media_views.py#L183

I think it might also be best for us to increase this timeout just a bit (maybe 10s?) to give the thumbnails for larger images/museum images more time to load.

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

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


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `api/catalog/api/views/media_views.py`
Content:
```
1 import json
2 import logging as log
3 from urllib.error import HTTPError
4 from urllib.parse import urlencode
5 from urllib.request import Request, urlopen
6 
7 from django.conf import settings
8 from django.http.response import HttpResponse
9 from rest_framework import status
10 from rest_framework.decorators import action
11 from rest_framework.response import Response
12 from rest_framework.viewsets import ReadOnlyModelViewSet
13 
14 from catalog.api.controllers import search_controller
15 from catalog.api.models import ContentProvider
16 from catalog.api.serializers.provider_serializers import ProviderSerializer
17 from catalog.api.utils.exceptions import get_api_exception
18 from catalog.api.utils.pagination import StandardPagination
19 from catalog.custom_auto_schema import CustomAutoSchema
20 
21 
22 class MediaViewSet(ReadOnlyModelViewSet):
23     swagger_schema = CustomAutoSchema
24 
25     lookup_field = "identifier"
26     # TODO: https://github.com/encode/django-rest-framework/pull/6789
27     lookup_value_regex = r"[0-9a-f\-]{36}"  # highly simplified approximation
28 
29     pagination_class = StandardPagination
30 
31     # Populate these in the corresponding subclass
32     model_class = None
33     query_serializer_class = None
34     default_index = None
35     qa_index = None
36 
37     def __init__(self, *args, **kwargs):
38         super().__init__(*args, **kwargs)
39         required_fields = [
40             self.model_class,
41             self.query_serializer_class,
42             self.default_index,
43             self.qa_index,
44         ]
45         if any(val is None for val in required_fields):
46             msg = "Viewset fields are not completely populated."
47             raise ValueError(msg)
48 
49     def get_queryset(self):
50         return self.model_class.objects.all()
51 
52     # Standard actions
53 
54     def list(self, request, *_, **__):
55         self.paginator.page_size = request.query_params.get("page_size")
56         page_size = self.paginator.page_size
57         self.paginator.page = request.query_params.get("page")
58         page = self.paginator.page
59 
60         params = self.query_serializer_class(data=request.query_params)
61         params.is_valid(raise_exception=True)
62 
63         hashed_ip = hash(self._get_user_ip(request))
64         qa = params.validated_data["qa"]
65         filter_dead = params.validated_data["filter_dead"]
66 
67         search_index = self.qa_index if qa else self.default_index
68         try:
69             results, num_pages, num_results = search_controller.search(
70                 params,
71                 search_index,
72                 page_size,
73                 hashed_ip,
74                 request,
75                 filter_dead,
76                 page,
77             )
78             self.paginator.page_count = num_pages
79             self.paginator.result_count = num_results
80         except ValueError as e:
81             raise get_api_exception(getattr(e, "message", str(e)))
82 
83         serializer = self.get_serializer(results, many=True)
84         return self.get_paginated_response(serializer.data)
85 
86     # Extra actions
87 
88     @action(detail=False, serializer_class=ProviderSerializer, pagination_class=None)
89     def stats(self, *_, **__):
90         source_counts = search_controller.get_sources(self.default_index)
91         context = self.get_serializer_context() | {
92             "source_counts": source_counts,
93         }
94 
95         providers = ContentProvider.objects.filter(
96             media_type=self.default_index, filter_content=False
97         )
98         serializer = self.get_serializer(providers, many=True, context=context)
99         return Response(serializer.data)
100 
101     @action(detail=True)
102     def related(self, request, identifier=None, *_, **__):
103         try:
104             results, num_results = search_controller.related_media(
105                 uuid=identifier,
106                 index=self.default_index,
107                 request=request,
108                 filter_dead=True,
109             )
110             self.paginator.result_count = num_results
111             self.paginator.page_count = 1
112             # `page_size` refers to the maximum number of related images to return.
113             self.paginator.page_size = 10
114         except ValueError as e:
115             raise get_api_exception(getattr(e, "message", str(e)))
116 
117         serializer = self.get_serializer(results, many=True)
118         return self.get_paginated_response(serializer.data)
119 
120     def report(self, request, *_, **__):
121         media = self.get_object()
122         identifier = media.identifier
123         serializer = self.get_serializer(data=request.data)
124         if not serializer.is_valid():
125             raise get_api_exception("Invalid input.", 400)
126         report = serializer.save(identifier=identifier)
127 
128         serializer = self.get_serializer(report)
129         return Response(data=serializer.data, status=status.HTTP_201_CREATED)
130 
131     def thumbnail(self, image_url, request, *_, **__):
132         serializer = self.get_serializer(data=request.query_params)
133         serializer.is_valid(raise_exception=True)
134         return self._get_proxied_image(
135             image_url,
136             accept_header=request.headers.get("Accept", "image/*"),
137             **serializer.validated_data,
138         )
139 
140     # Helper functions
141 
142     @staticmethod
143     def _get_user_ip(request):
144         """
145         Read request headers to find the correct IP address.
146         It is assumed that X-Forwarded-For has been sanitized by the load
147         balancer and thus cannot be rewritten by malicious users.
148         :param request: A Django request object.
149         :return: An IP address.
150         """
151         x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
152         if x_forwarded_for:
153             ip = x_forwarded_for.split(",")[0]
154         else:
155             ip = request.META.get("REMOTE_ADDR")
156         return ip
157 
158     @staticmethod
159     def _thumbnail_proxy_comm(
160         path: str,
161         params: dict,
162         headers: tuple[tuple[str, str]] = (),
163     ):
164         proxy_url = settings.THUMBNAIL_PROXY_URL
165         query_string = urlencode(params)
166         upstream_url = f"{proxy_url}/{path}?{query_string}"
167         log.debug(f"Image proxy upstream URL: {upstream_url}")
168 
169         try:
170             req = Request(upstream_url)
171             for key, val in headers:
172                 req.add_header(key, val)
173             upstream_response = urlopen(req, timeout=5)
174 
175             res_status = upstream_response.status
176             content_type = upstream_response.headers.get("Content-Type")
177             log.debug(
178                 "Image proxy response "
179                 f"status: {res_status}, content-type: {content_type}"
180             )
181 
182             return upstream_response, res_status, content_type
183         except HTTPError as exc:
184             raise get_api_exception(f"Failed to render thumbnail: {exc}")
185 
186     @staticmethod
187     def _get_proxied_image(
188         image_url: str,
189         accept_header: str = "image/*",
190         is_full_size: bool = False,
191         is_compressed: bool = True,
192     ):
193         width = settings.THUMBNAIL_WIDTH_PX
194         if is_full_size:
195             info_res, *_ = MediaViewSet._thumbnail_proxy_comm(
196                 "info", {"url": image_url}
197             )
198             info = json.loads(info_res.read())
199             width = info["width"]
200 
201         params = {
202             "url": image_url,
203             "width": width,
204         }
205 
206         if is_compressed:
207             params |= {
208                 "quality": settings.THUMBNAIL_JPG_QUALITY,
209                 "compression": settings.THUMBNAIL_PNG_COMPRESSION,
210             }
211         else:
212             params |= {
213                 "quality": 100,
214                 "compression": 0,
215             }
216 
217         if "webp" in accept_header:
218             params["type"] = "auto"  # Use ``Accept`` header to determine output type.
219 
220         img_res, res_status, content_type = MediaViewSet._thumbnail_proxy_comm(
221             "resize", params, (("Accept", accept_header),)
222         )
223         response = HttpResponse(
224             img_res.read(), status=res_status, content_type=content_type
225         )
226         return response
227
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

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
@@ -1,5 +1,6 @@
 import json
 import logging as log
+from http.client import RemoteDisconnected
 from urllib.error import HTTPError
 from urllib.parse import urlencode
 from urllib.request import Request, urlopen
@@ -170,7 +171,7 @@
             req = Request(upstream_url)
             for key, val in headers:
                 req.add_header(key, val)
-            upstream_response = urlopen(req, timeout=5)
+            upstream_response = urlopen(req, timeout=10)
 
             res_status = upstream_response.status
             content_type = upstream_response.headers.get("Content-Type")
@@ -180,8 +181,12 @@
             )
 
             return upstream_response, res_status, content_type
-        except HTTPError as exc:
+        except (HTTPError, RemoteDisconnected, TimeoutError) as exc:
             raise get_api_exception(f"Failed to render thumbnail: {exc}")
+        except Exception as exc:
+            raise get_api_exception(
+                f"Failed to render thumbnail due to unidentified exception: {exc}"
+            )
 
     @staticmethod
     def _get_proxied_image(

{"golden_diff": "diff --git a/api/catalog/api/views/media_views.py b/api/catalog/api/views/media_views.py\n--- a/api/catalog/api/views/media_views.py\n+++ b/api/catalog/api/views/media_views.py\n@@ -1,5 +1,6 @@\n import json\n import logging as log\n+from http.client import RemoteDisconnected\n from urllib.error import HTTPError\n from urllib.parse import urlencode\n from urllib.request import Request, urlopen\n@@ -170,7 +171,7 @@\n             req = Request(upstream_url)\n             for key, val in headers:\n                 req.add_header(key, val)\n-            upstream_response = urlopen(req, timeout=5)\n+            upstream_response = urlopen(req, timeout=10)\n \n             res_status = upstream_response.status\n             content_type = upstream_response.headers.get(\"Content-Type\")\n@@ -180,8 +181,12 @@\n             )\n \n             return upstream_response, res_status, content_type\n-        except HTTPError as exc:\n+        except (HTTPError, RemoteDisconnected, TimeoutError) as exc:\n             raise get_api_exception(f\"Failed to render thumbnail: {exc}\")\n+        except Exception as exc:\n+            raise get_api_exception(\n+                f\"Failed to render thumbnail due to unidentified exception: {exc}\"\n+            )\n \n     @staticmethod\n     def _get_proxied_image(\n", "issue": "Thumbnail timeouts raise `TimeoutError`\n## Description\n<!-- Concisely describe the bug. Compare your experience with what you expected to happen. -->\n<!-- For example: \"I clicked the 'submit' button and instead of seeing a thank you message, I saw a blank page.\" -->\nWe recently changed the thumbnail service we're using and some of the business logic around it (#630). The previous thumbnail retrieval step [did not have a timeout](https://github.com/WordPress/openverse-api/blame/0df21059dfe579be1e7a83f64e8e8a7d4e422416/api/catalog/api/views/media_views.py#L156), whereas the new thumbnail retrieval step has a timeout of 5 seconds:\n\nhttps://github.com/WordPress/openverse-api/blob/03362db8caed8014e31d3fe5274c9bb084086b85/api/catalog/api/views/media_views.py#L173\n\nWe should also add a case for the `TimeoutError` exception here:\n\nhttps://github.com/WordPress/openverse-api/blob/03362db8caed8014e31d3fe5274c9bb084086b85/api/catalog/api/views/media_views.py#L183\n\nI think it might also be best for us to increase this timeout just a bit (maybe 10s?) to give the thumbnails for larger images/museum images more time to load.\n\n## Additional context\n<!-- Add any other context about the problem here; or delete the section entirely. -->\nSentry issue: https://sentry.io/share/issue/3daf5a2129a449859200e2de0c458ed6/\n\n## Resolution\n<!-- Replace the [ ] with [x] to check the box. -->\n- [ ] \ud83d\ude4b I would be interested in resolving this bug.\n\n", "before_files": [{"content": "import json\nimport logging as log\nfrom urllib.error import HTTPError\nfrom urllib.parse import urlencode\nfrom urllib.request import Request, urlopen\n\nfrom django.conf import settings\nfrom django.http.response import HttpResponse\nfrom rest_framework import status\nfrom rest_framework.decorators import action\nfrom rest_framework.response import Response\nfrom rest_framework.viewsets import ReadOnlyModelViewSet\n\nfrom catalog.api.controllers import search_controller\nfrom catalog.api.models import ContentProvider\nfrom catalog.api.serializers.provider_serializers import ProviderSerializer\nfrom catalog.api.utils.exceptions import get_api_exception\nfrom catalog.api.utils.pagination import StandardPagination\nfrom catalog.custom_auto_schema import CustomAutoSchema\n\n\nclass MediaViewSet(ReadOnlyModelViewSet):\n    swagger_schema = CustomAutoSchema\n\n    lookup_field = \"identifier\"\n    # TODO: https://github.com/encode/django-rest-framework/pull/6789\n    lookup_value_regex = r\"[0-9a-f\\-]{36}\"  # highly simplified approximation\n\n    pagination_class = StandardPagination\n\n    # Populate these in the corresponding subclass\n    model_class = None\n    query_serializer_class = None\n    default_index = None\n    qa_index = None\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        required_fields = [\n            self.model_class,\n            self.query_serializer_class,\n            self.default_index,\n            self.qa_index,\n        ]\n        if any(val is None for val in required_fields):\n            msg = \"Viewset fields are not completely populated.\"\n            raise ValueError(msg)\n\n    def get_queryset(self):\n        return self.model_class.objects.all()\n\n    # Standard actions\n\n    def list(self, request, *_, **__):\n        self.paginator.page_size = request.query_params.get(\"page_size\")\n        page_size = self.paginator.page_size\n        self.paginator.page = request.query_params.get(\"page\")\n        page = self.paginator.page\n\n        params = self.query_serializer_class(data=request.query_params)\n        params.is_valid(raise_exception=True)\n\n        hashed_ip = hash(self._get_user_ip(request))\n        qa = params.validated_data[\"qa\"]\n        filter_dead = params.validated_data[\"filter_dead\"]\n\n        search_index = self.qa_index if qa else self.default_index\n        try:\n            results, num_pages, num_results = search_controller.search(\n                params,\n                search_index,\n                page_size,\n                hashed_ip,\n                request,\n                filter_dead,\n                page,\n            )\n            self.paginator.page_count = num_pages\n            self.paginator.result_count = num_results\n        except ValueError as e:\n            raise get_api_exception(getattr(e, \"message\", str(e)))\n\n        serializer = self.get_serializer(results, many=True)\n        return self.get_paginated_response(serializer.data)\n\n    # Extra actions\n\n    @action(detail=False, serializer_class=ProviderSerializer, pagination_class=None)\n    def stats(self, *_, **__):\n        source_counts = search_controller.get_sources(self.default_index)\n        context = self.get_serializer_context() | {\n            \"source_counts\": source_counts,\n        }\n\n        providers = ContentProvider.objects.filter(\n            media_type=self.default_index, filter_content=False\n        )\n        serializer = self.get_serializer(providers, many=True, context=context)\n        return Response(serializer.data)\n\n    @action(detail=True)\n    def related(self, request, identifier=None, *_, **__):\n        try:\n            results, num_results = search_controller.related_media(\n                uuid=identifier,\n                index=self.default_index,\n                request=request,\n                filter_dead=True,\n            )\n            self.paginator.result_count = num_results\n            self.paginator.page_count = 1\n            # `page_size` refers to the maximum number of related images to return.\n            self.paginator.page_size = 10\n        except ValueError as e:\n            raise get_api_exception(getattr(e, \"message\", str(e)))\n\n        serializer = self.get_serializer(results, many=True)\n        return self.get_paginated_response(serializer.data)\n\n    def report(self, request, *_, **__):\n        media = self.get_object()\n        identifier = media.identifier\n        serializer = self.get_serializer(data=request.data)\n        if not serializer.is_valid():\n            raise get_api_exception(\"Invalid input.\", 400)\n        report = serializer.save(identifier=identifier)\n\n        serializer = self.get_serializer(report)\n        return Response(data=serializer.data, status=status.HTTP_201_CREATED)\n\n    def thumbnail(self, image_url, request, *_, **__):\n        serializer = self.get_serializer(data=request.query_params)\n        serializer.is_valid(raise_exception=True)\n        return self._get_proxied_image(\n            image_url,\n            accept_header=request.headers.get(\"Accept\", \"image/*\"),\n            **serializer.validated_data,\n        )\n\n    # Helper functions\n\n    @staticmethod\n    def _get_user_ip(request):\n        \"\"\"\n        Read request headers to find the correct IP address.\n        It is assumed that X-Forwarded-For has been sanitized by the load\n        balancer and thus cannot be rewritten by malicious users.\n        :param request: A Django request object.\n        :return: An IP address.\n        \"\"\"\n        x_forwarded_for = request.META.get(\"HTTP_X_FORWARDED_FOR\")\n        if x_forwarded_for:\n            ip = x_forwarded_for.split(\",\")[0]\n        else:\n            ip = request.META.get(\"REMOTE_ADDR\")\n        return ip\n\n    @staticmethod\n    def _thumbnail_proxy_comm(\n        path: str,\n        params: dict,\n        headers: tuple[tuple[str, str]] = (),\n    ):\n        proxy_url = settings.THUMBNAIL_PROXY_URL\n        query_string = urlencode(params)\n        upstream_url = f\"{proxy_url}/{path}?{query_string}\"\n        log.debug(f\"Image proxy upstream URL: {upstream_url}\")\n\n        try:\n            req = Request(upstream_url)\n            for key, val in headers:\n                req.add_header(key, val)\n            upstream_response = urlopen(req, timeout=5)\n\n            res_status = upstream_response.status\n            content_type = upstream_response.headers.get(\"Content-Type\")\n            log.debug(\n                \"Image proxy response \"\n                f\"status: {res_status}, content-type: {content_type}\"\n            )\n\n            return upstream_response, res_status, content_type\n        except HTTPError as exc:\n            raise get_api_exception(f\"Failed to render thumbnail: {exc}\")\n\n    @staticmethod\n    def _get_proxied_image(\n        image_url: str,\n        accept_header: str = \"image/*\",\n        is_full_size: bool = False,\n        is_compressed: bool = True,\n    ):\n        width = settings.THUMBNAIL_WIDTH_PX\n        if is_full_size:\n            info_res, *_ = MediaViewSet._thumbnail_proxy_comm(\n                \"info\", {\"url\": image_url}\n            )\n            info = json.loads(info_res.read())\n            width = info[\"width\"]\n\n        params = {\n            \"url\": image_url,\n            \"width\": width,\n        }\n\n        if is_compressed:\n            params |= {\n                \"quality\": settings.THUMBNAIL_JPG_QUALITY,\n                \"compression\": settings.THUMBNAIL_PNG_COMPRESSION,\n            }\n        else:\n            params |= {\n                \"quality\": 100,\n                \"compression\": 0,\n            }\n\n        if \"webp\" in accept_header:\n            params[\"type\"] = \"auto\"  # Use ``Accept`` header to determine output type.\n\n        img_res, res_status, content_type = MediaViewSet._thumbnail_proxy_comm(\n            \"resize\", params, ((\"Accept\", accept_header),)\n        )\n        response = HttpResponse(\n            img_res.read(), status=res_status, content_type=content_type\n        )\n        return response\n", "path": "api/catalog/api/views/media_views.py"}], "after_files": [{"content": "import json\nimport logging as log\nfrom http.client import RemoteDisconnected\nfrom urllib.error import HTTPError\nfrom urllib.parse import urlencode\nfrom urllib.request import Request, urlopen\n\nfrom django.conf import settings\nfrom django.http.response import HttpResponse\nfrom rest_framework import status\nfrom rest_framework.decorators import action\nfrom rest_framework.response import Response\nfrom rest_framework.viewsets import ReadOnlyModelViewSet\n\nfrom catalog.api.controllers import search_controller\nfrom catalog.api.models import ContentProvider\nfrom catalog.api.serializers.provider_serializers import ProviderSerializer\nfrom catalog.api.utils.exceptions import get_api_exception\nfrom catalog.api.utils.pagination import StandardPagination\nfrom catalog.custom_auto_schema import CustomAutoSchema\n\n\nclass MediaViewSet(ReadOnlyModelViewSet):\n    swagger_schema = CustomAutoSchema\n\n    lookup_field = \"identifier\"\n    # TODO: https://github.com/encode/django-rest-framework/pull/6789\n    lookup_value_regex = r\"[0-9a-f\\-]{36}\"  # highly simplified approximation\n\n    pagination_class = StandardPagination\n\n    # Populate these in the corresponding subclass\n    model_class = None\n    query_serializer_class = None\n    default_index = None\n    qa_index = None\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        required_fields = [\n            self.model_class,\n            self.query_serializer_class,\n            self.default_index,\n            self.qa_index,\n        ]\n        if any(val is None for val in required_fields):\n            msg = \"Viewset fields are not completely populated.\"\n            raise ValueError(msg)\n\n    def get_queryset(self):\n        return self.model_class.objects.all()\n\n    # Standard actions\n\n    def list(self, request, *_, **__):\n        self.paginator.page_size = request.query_params.get(\"page_size\")\n        page_size = self.paginator.page_size\n        self.paginator.page = request.query_params.get(\"page\")\n        page = self.paginator.page\n\n        params = self.query_serializer_class(data=request.query_params)\n        params.is_valid(raise_exception=True)\n\n        hashed_ip = hash(self._get_user_ip(request))\n        qa = params.validated_data[\"qa\"]\n        filter_dead = params.validated_data[\"filter_dead\"]\n\n        search_index = self.qa_index if qa else self.default_index\n        try:\n            results, num_pages, num_results = search_controller.search(\n                params,\n                search_index,\n                page_size,\n                hashed_ip,\n                request,\n                filter_dead,\n                page,\n            )\n            self.paginator.page_count = num_pages\n            self.paginator.result_count = num_results\n        except ValueError as e:\n            raise get_api_exception(getattr(e, \"message\", str(e)))\n\n        serializer = self.get_serializer(results, many=True)\n        return self.get_paginated_response(serializer.data)\n\n    # Extra actions\n\n    @action(detail=False, serializer_class=ProviderSerializer, pagination_class=None)\n    def stats(self, *_, **__):\n        source_counts = search_controller.get_sources(self.default_index)\n        context = self.get_serializer_context() | {\n            \"source_counts\": source_counts,\n        }\n\n        providers = ContentProvider.objects.filter(\n            media_type=self.default_index, filter_content=False\n        )\n        serializer = self.get_serializer(providers, many=True, context=context)\n        return Response(serializer.data)\n\n    @action(detail=True)\n    def related(self, request, identifier=None, *_, **__):\n        try:\n            results, num_results = search_controller.related_media(\n                uuid=identifier,\n                index=self.default_index,\n                request=request,\n                filter_dead=True,\n            )\n            self.paginator.result_count = num_results\n            self.paginator.page_count = 1\n            # `page_size` refers to the maximum number of related images to return.\n            self.paginator.page_size = 10\n        except ValueError as e:\n            raise get_api_exception(getattr(e, \"message\", str(e)))\n\n        serializer = self.get_serializer(results, many=True)\n        return self.get_paginated_response(serializer.data)\n\n    def report(self, request, *_, **__):\n        media = self.get_object()\n        identifier = media.identifier\n        serializer = self.get_serializer(data=request.data)\n        if not serializer.is_valid():\n            raise get_api_exception(\"Invalid input.\", 400)\n        report = serializer.save(identifier=identifier)\n\n        serializer = self.get_serializer(report)\n        return Response(data=serializer.data, status=status.HTTP_201_CREATED)\n\n    def thumbnail(self, image_url, request, *_, **__):\n        serializer = self.get_serializer(data=request.query_params)\n        serializer.is_valid(raise_exception=True)\n        return self._get_proxied_image(\n            image_url,\n            accept_header=request.headers.get(\"Accept\", \"image/*\"),\n            **serializer.validated_data,\n        )\n\n    # Helper functions\n\n    @staticmethod\n    def _get_user_ip(request):\n        \"\"\"\n        Read request headers to find the correct IP address.\n        It is assumed that X-Forwarded-For has been sanitized by the load\n        balancer and thus cannot be rewritten by malicious users.\n        :param request: A Django request object.\n        :return: An IP address.\n        \"\"\"\n        x_forwarded_for = request.META.get(\"HTTP_X_FORWARDED_FOR\")\n        if x_forwarded_for:\n            ip = x_forwarded_for.split(\",\")[0]\n        else:\n            ip = request.META.get(\"REMOTE_ADDR\")\n        return ip\n\n    @staticmethod\n    def _thumbnail_proxy_comm(\n        path: str,\n        params: dict,\n        headers: tuple[tuple[str, str]] = (),\n    ):\n        proxy_url = settings.THUMBNAIL_PROXY_URL\n        query_string = urlencode(params)\n        upstream_url = f\"{proxy_url}/{path}?{query_string}\"\n        log.debug(f\"Image proxy upstream URL: {upstream_url}\")\n\n        try:\n            req = Request(upstream_url)\n            for key, val in headers:\n                req.add_header(key, val)\n            upstream_response = urlopen(req, timeout=10)\n\n            res_status = upstream_response.status\n            content_type = upstream_response.headers.get(\"Content-Type\")\n            log.debug(\n                \"Image proxy response \"\n                f\"status: {res_status}, content-type: {content_type}\"\n            )\n\n            return upstream_response, res_status, content_type\n        except (HTTPError, RemoteDisconnected, TimeoutError) as exc:\n            raise get_api_exception(f\"Failed to render thumbnail: {exc}\")\n        except Exception as exc:\n            raise get_api_exception(\n                f\"Failed to render thumbnail due to unidentified exception: {exc}\"\n            )\n\n    @staticmethod\n    def _get_proxied_image(\n        image_url: str,\n        accept_header: str = \"image/*\",\n        is_full_size: bool = False,\n        is_compressed: bool = True,\n    ):\n        width = settings.THUMBNAIL_WIDTH_PX\n        if is_full_size:\n            info_res, *_ = MediaViewSet._thumbnail_proxy_comm(\n                \"info\", {\"url\": image_url}\n            )\n            info = json.loads(info_res.read())\n            width = info[\"width\"]\n\n        params = {\n            \"url\": image_url,\n            \"width\": width,\n        }\n\n        if is_compressed:\n            params |= {\n                \"quality\": settings.THUMBNAIL_JPG_QUALITY,\n                \"compression\": settings.THUMBNAIL_PNG_COMPRESSION,\n            }\n        else:\n            params |= {\n                \"quality\": 100,\n                \"compression\": 0,\n            }\n\n        if \"webp\" in accept_header:\n            params[\"type\"] = \"auto\"  # Use ``Accept`` header to determine output type.\n\n        img_res, res_status, content_type = MediaViewSet._thumbnail_proxy_comm(\n            \"resize\", params, ((\"Accept\", accept_header),)\n        )\n        response = HttpResponse(\n            img_res.read(), status=res_status, content_type=content_type\n        )\n        return response\n", "path": "api/catalog/api/views/media_views.py"}]}