Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 7b67e93

Browse files
Provide more info why we don't have any thumbnails to serve (#13038)
Fix #13016 ## New error code and status ### Before Previously, we returned a `404` for `/thumbnail` which isn't even in the spec. ```json { "errcode": "M_NOT_FOUND", "error": "Not found [b'hs1', b'tefQeZhmVxoiBfuFQUKRzJxc']" } ``` ### After What does the spec say? > 400: The request does not make sense to the server, or the server cannot thumbnail the content. For example, the client requested non-integer dimensions or asked for negatively-sized images. > > *-- https://spec.matrix.org/v1.1/client-server-api/#get_matrixmediav3thumbnailservernamemediaid* Now with this PR, we respond with a `400` when we don't have thumbnails to serve and we explain why we might not have any thumbnails. ```json { "errcode": "M_UNKNOWN", "error": "Cannot find any thumbnails for the requested media ([b'example.com', b'12345']). This might mean the media is not a supported_media_format=(image/jpeg, image/jpg, image/webp, image/gif, image/png) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)", } ``` > Cannot find any thumbnails for the requested media ([b'example.com', b'12345']). This might mean the media is not a supported_media_format=(image/jpeg, image/jpg, image/webp, image/gif, image/png) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.) --- We still respond with a 404 in many other places. But we can iterate on those later and maybe keep some in some specific places after spec updates/clarification: matrix-org/matrix-spec#1122 We can also iterate on the bugs where Synapse doesn't thumbnail when it should in other issues/PRs.
1 parent e9ce4d0 commit 7b67e93

File tree

4 files changed

+129
-17
lines changed

4 files changed

+129
-17
lines changed

changelog.d/13038.feature

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Provide more info why we don't have any thumbnails to serve.

synapse/config/repository.py

+28-7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@
4242
# method: %(method)s
4343
"""
4444

45+
# A map from the given media type to the type of thumbnail we should generate
46+
# for it.
47+
THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP = {
48+
"image/jpeg": "jpeg",
49+
"image/jpg": "jpeg",
50+
"image/webp": "jpeg",
51+
# Thumbnails can only be jpeg or png. We choose png thumbnails for gif
52+
# because it can have transparency.
53+
"image/gif": "png",
54+
"image/png": "png",
55+
}
56+
4557
HTTP_PROXY_SET_WARNING = """\
4658
The Synapse config url_preview_ip_range_blacklist will be ignored as an HTTP(s) proxy is configured."""
4759

@@ -79,13 +91,22 @@ def parse_thumbnail_requirements(
7991
width = size["width"]
8092
height = size["height"]
8193
method = size["method"]
82-
jpeg_thumbnail = ThumbnailRequirement(width, height, method, "image/jpeg")
83-
png_thumbnail = ThumbnailRequirement(width, height, method, "image/png")
84-
requirements.setdefault("image/jpeg", []).append(jpeg_thumbnail)
85-
requirements.setdefault("image/jpg", []).append(jpeg_thumbnail)
86-
requirements.setdefault("image/webp", []).append(jpeg_thumbnail)
87-
requirements.setdefault("image/gif", []).append(png_thumbnail)
88-
requirements.setdefault("image/png", []).append(png_thumbnail)
94+
95+
for format, thumbnail_format in THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP.items():
96+
requirement = requirements.setdefault(format, [])
97+
if thumbnail_format == "jpeg":
98+
requirement.append(
99+
ThumbnailRequirement(width, height, method, "image/jpeg")
100+
)
101+
elif thumbnail_format == "png":
102+
requirement.append(
103+
ThumbnailRequirement(width, height, method, "image/png")
104+
)
105+
else:
106+
raise Exception(
107+
"Unknown thumbnail mapping from %s to %s. This is a Synapse problem, please report!"
108+
% (format, thumbnail_format)
109+
)
89110
return {
90111
media_type: tuple(thumbnails) for media_type, thumbnails in requirements.items()
91112
}

synapse/rest/media/v1/thumbnail_resource.py

+38-2
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
import logging
1818
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
1919

20-
from synapse.api.errors import SynapseError
20+
from synapse.api.errors import Codes, SynapseError, cs_error
21+
from synapse.config.repository import THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP
2122
from synapse.http.server import (
2223
DirectServeJsonResource,
24+
respond_with_json,
2325
set_corp_headers,
2426
set_cors_headers,
2527
)
@@ -309,6 +311,19 @@ async def _select_and_respond_with_thumbnail(
309311
url_cache: True if this is from a URL cache.
310312
server_name: The server name, if this is a remote thumbnail.
311313
"""
314+
logger.debug(
315+
"_select_and_respond_with_thumbnail: media_id=%s desired=%sx%s (%s) thumbnail_infos=%s",
316+
media_id,
317+
desired_width,
318+
desired_height,
319+
desired_method,
320+
thumbnail_infos,
321+
)
322+
323+
# If `dynamic_thumbnails` is enabled, we expect Synapse to go down a
324+
# different code path to handle it.
325+
assert not self.dynamic_thumbnails
326+
312327
if thumbnail_infos:
313328
file_info = self._select_thumbnail(
314329
desired_width,
@@ -384,8 +399,29 @@ async def _select_and_respond_with_thumbnail(
384399
file_info.thumbnail.length,
385400
)
386401
else:
402+
# This might be because:
403+
# 1. We can't create thumbnails for the given media (corrupted or
404+
# unsupported file type), or
405+
# 2. The thumbnailing process never ran or errored out initially
406+
# when the media was first uploaded (these bugs should be
407+
# reported and fixed).
408+
# Note that we don't attempt to generate a thumbnail now because
409+
# `dynamic_thumbnails` is disabled.
387410
logger.info("Failed to find any generated thumbnails")
388-
respond_404(request)
411+
412+
respond_with_json(
413+
request,
414+
400,
415+
cs_error(
416+
"Cannot find any thumbnails for the requested media (%r). This might mean the media is not a supported_media_format=(%s) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)"
417+
% (
418+
request.postpath,
419+
", ".join(THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP.keys()),
420+
),
421+
code=Codes.UNKNOWN,
422+
),
423+
send_cors=True,
424+
)
389425

390426
def _select_thumbnail(
391427
self,

tests/rest/media/v1/test_media_storage.py

+62-8
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,9 @@ class _TestImage:
126126
expected_scaled: The expected bytes from scaled thumbnailing, or None if
127127
test should just check for a valid image returned.
128128
expected_found: True if the file should exist on the server, or False if
129-
a 404 is expected.
129+
a 404/400 is expected.
130+
unable_to_thumbnail: True if we expect the thumbnailing to fail (400), or
131+
False if the thumbnailing should succeed or a normal 404 is expected.
130132
"""
131133

132134
data: bytes
@@ -135,6 +137,7 @@ class _TestImage:
135137
expected_cropped: Optional[bytes] = None
136138
expected_scaled: Optional[bytes] = None
137139
expected_found: bool = True
140+
unable_to_thumbnail: bool = False
138141

139142

140143
@parameterized_class(
@@ -192,6 +195,7 @@ class _TestImage:
192195
b"image/gif",
193196
b".gif",
194197
expected_found=False,
198+
unable_to_thumbnail=True,
195199
),
196200
),
197201
],
@@ -366,18 +370,29 @@ def test_disposition_none(self) -> None:
366370
def test_thumbnail_crop(self) -> None:
367371
"""Test that a cropped remote thumbnail is available."""
368372
self._test_thumbnail(
369-
"crop", self.test_image.expected_cropped, self.test_image.expected_found
373+
"crop",
374+
self.test_image.expected_cropped,
375+
expected_found=self.test_image.expected_found,
376+
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
370377
)
371378

372379
def test_thumbnail_scale(self) -> None:
373380
"""Test that a scaled remote thumbnail is available."""
374381
self._test_thumbnail(
375-
"scale", self.test_image.expected_scaled, self.test_image.expected_found
382+
"scale",
383+
self.test_image.expected_scaled,
384+
expected_found=self.test_image.expected_found,
385+
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
376386
)
377387

378388
def test_invalid_type(self) -> None:
379389
"""An invalid thumbnail type is never available."""
380-
self._test_thumbnail("invalid", None, False)
390+
self._test_thumbnail(
391+
"invalid",
392+
None,
393+
expected_found=False,
394+
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
395+
)
381396

382397
@unittest.override_config(
383398
{"thumbnail_sizes": [{"width": 32, "height": 32, "method": "scale"}]}
@@ -386,7 +401,12 @@ def test_no_thumbnail_crop(self) -> None:
386401
"""
387402
Override the config to generate only scaled thumbnails, but request a cropped one.
388403
"""
389-
self._test_thumbnail("crop", None, False)
404+
self._test_thumbnail(
405+
"crop",
406+
None,
407+
expected_found=False,
408+
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
409+
)
390410

391411
@unittest.override_config(
392412
{"thumbnail_sizes": [{"width": 32, "height": 32, "method": "crop"}]}
@@ -395,14 +415,22 @@ def test_no_thumbnail_scale(self) -> None:
395415
"""
396416
Override the config to generate only cropped thumbnails, but request a scaled one.
397417
"""
398-
self._test_thumbnail("scale", None, False)
418+
self._test_thumbnail(
419+
"scale",
420+
None,
421+
expected_found=False,
422+
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
423+
)
399424

400425
def test_thumbnail_repeated_thumbnail(self) -> None:
401426
"""Test that fetching the same thumbnail works, and deleting the on disk
402427
thumbnail regenerates it.
403428
"""
404429
self._test_thumbnail(
405-
"scale", self.test_image.expected_scaled, self.test_image.expected_found
430+
"scale",
431+
self.test_image.expected_scaled,
432+
expected_found=self.test_image.expected_found,
433+
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
406434
)
407435

408436
if not self.test_image.expected_found:
@@ -459,8 +487,24 @@ def test_thumbnail_repeated_thumbnail(self) -> None:
459487
)
460488

461489
def _test_thumbnail(
462-
self, method: str, expected_body: Optional[bytes], expected_found: bool
490+
self,
491+
method: str,
492+
expected_body: Optional[bytes],
493+
expected_found: bool,
494+
unable_to_thumbnail: bool = False,
463495
) -> None:
496+
"""Test the given thumbnailing method works as expected.
497+
498+
Args:
499+
method: The thumbnailing method to use (crop, scale).
500+
expected_body: The expected bytes from thumbnailing, or None if
501+
test should just check for a valid image.
502+
expected_found: True if the file should exist on the server, or False if
503+
a 404/400 is expected.
504+
unable_to_thumbnail: True if we expect the thumbnailing to fail (400), or
505+
False if the thumbnailing should succeed or a normal 404 is expected.
506+
"""
507+
464508
params = "?width=32&height=32&method=" + method
465509
channel = make_request(
466510
self.reactor,
@@ -496,6 +540,16 @@ def _test_thumbnail(
496540
else:
497541
# ensure that the result is at least some valid image
498542
Image.open(BytesIO(channel.result["body"]))
543+
elif unable_to_thumbnail:
544+
# A 400 with a JSON body.
545+
self.assertEqual(channel.code, 400)
546+
self.assertEqual(
547+
channel.json_body,
548+
{
549+
"errcode": "M_UNKNOWN",
550+
"error": "Cannot find any thumbnails for the requested media ([b'example.com', b'12345']). This might mean the media is not a supported_media_format=(image/jpeg, image/jpg, image/webp, image/gif, image/png) or that thumbnailing failed for some other reason. (Dynamic thumbnails are disabled on this server.)",
551+
},
552+
)
499553
else:
500554
# A 404 with a JSON body.
501555
self.assertEqual(channel.code, 404)

0 commit comments

Comments
 (0)