mirror of
https://git.anonymousland.org/anonymousland/synapse.git
synced 2024-12-26 03:09:22 -05:00
Provide more info why we don't have any thumbnails to serve (#13038)
Fix https://github.com/matrix-org/synapse/issues/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: https://github.com/matrix-org/matrix-spec/issues/1122 We can also iterate on the bugs where Synapse doesn't thumbnail when it should in other issues/PRs.
This commit is contained in:
parent
e9ce4d089b
commit
7b67e93d49
1
changelog.d/13038.feature
Normal file
1
changelog.d/13038.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Provide more info why we don't have any thumbnails to serve.
|
@ -42,6 +42,18 @@ THUMBNAIL_SIZE_YAML = """\
|
|||||||
# method: %(method)s
|
# method: %(method)s
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# A map from the given media type to the type of thumbnail we should generate
|
||||||
|
# for it.
|
||||||
|
THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP = {
|
||||||
|
"image/jpeg": "jpeg",
|
||||||
|
"image/jpg": "jpeg",
|
||||||
|
"image/webp": "jpeg",
|
||||||
|
# Thumbnails can only be jpeg or png. We choose png thumbnails for gif
|
||||||
|
# because it can have transparency.
|
||||||
|
"image/gif": "png",
|
||||||
|
"image/png": "png",
|
||||||
|
}
|
||||||
|
|
||||||
HTTP_PROXY_SET_WARNING = """\
|
HTTP_PROXY_SET_WARNING = """\
|
||||||
The Synapse config url_preview_ip_range_blacklist will be ignored as an HTTP(s) proxy is configured."""
|
The Synapse config url_preview_ip_range_blacklist will be ignored as an HTTP(s) proxy is configured."""
|
||||||
|
|
||||||
@ -79,13 +91,22 @@ def parse_thumbnail_requirements(
|
|||||||
width = size["width"]
|
width = size["width"]
|
||||||
height = size["height"]
|
height = size["height"]
|
||||||
method = size["method"]
|
method = size["method"]
|
||||||
jpeg_thumbnail = ThumbnailRequirement(width, height, method, "image/jpeg")
|
|
||||||
png_thumbnail = ThumbnailRequirement(width, height, method, "image/png")
|
for format, thumbnail_format in THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP.items():
|
||||||
requirements.setdefault("image/jpeg", []).append(jpeg_thumbnail)
|
requirement = requirements.setdefault(format, [])
|
||||||
requirements.setdefault("image/jpg", []).append(jpeg_thumbnail)
|
if thumbnail_format == "jpeg":
|
||||||
requirements.setdefault("image/webp", []).append(jpeg_thumbnail)
|
requirement.append(
|
||||||
requirements.setdefault("image/gif", []).append(png_thumbnail)
|
ThumbnailRequirement(width, height, method, "image/jpeg")
|
||||||
requirements.setdefault("image/png", []).append(png_thumbnail)
|
)
|
||||||
|
elif thumbnail_format == "png":
|
||||||
|
requirement.append(
|
||||||
|
ThumbnailRequirement(width, height, method, "image/png")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise Exception(
|
||||||
|
"Unknown thumbnail mapping from %s to %s. This is a Synapse problem, please report!"
|
||||||
|
% (format, thumbnail_format)
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
media_type: tuple(thumbnails) for media_type, thumbnails in requirements.items()
|
media_type: tuple(thumbnails) for media_type, thumbnails in requirements.items()
|
||||||
}
|
}
|
||||||
|
@ -17,9 +17,11 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import Codes, SynapseError, cs_error
|
||||||
|
from synapse.config.repository import THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP
|
||||||
from synapse.http.server import (
|
from synapse.http.server import (
|
||||||
DirectServeJsonResource,
|
DirectServeJsonResource,
|
||||||
|
respond_with_json,
|
||||||
set_corp_headers,
|
set_corp_headers,
|
||||||
set_cors_headers,
|
set_cors_headers,
|
||||||
)
|
)
|
||||||
@ -309,6 +311,19 @@ class ThumbnailResource(DirectServeJsonResource):
|
|||||||
url_cache: True if this is from a URL cache.
|
url_cache: True if this is from a URL cache.
|
||||||
server_name: The server name, if this is a remote thumbnail.
|
server_name: The server name, if this is a remote thumbnail.
|
||||||
"""
|
"""
|
||||||
|
logger.debug(
|
||||||
|
"_select_and_respond_with_thumbnail: media_id=%s desired=%sx%s (%s) thumbnail_infos=%s",
|
||||||
|
media_id,
|
||||||
|
desired_width,
|
||||||
|
desired_height,
|
||||||
|
desired_method,
|
||||||
|
thumbnail_infos,
|
||||||
|
)
|
||||||
|
|
||||||
|
# If `dynamic_thumbnails` is enabled, we expect Synapse to go down a
|
||||||
|
# different code path to handle it.
|
||||||
|
assert not self.dynamic_thumbnails
|
||||||
|
|
||||||
if thumbnail_infos:
|
if thumbnail_infos:
|
||||||
file_info = self._select_thumbnail(
|
file_info = self._select_thumbnail(
|
||||||
desired_width,
|
desired_width,
|
||||||
@ -384,8 +399,29 @@ class ThumbnailResource(DirectServeJsonResource):
|
|||||||
file_info.thumbnail.length,
|
file_info.thumbnail.length,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# This might be because:
|
||||||
|
# 1. We can't create thumbnails for the given media (corrupted or
|
||||||
|
# unsupported file type), or
|
||||||
|
# 2. The thumbnailing process never ran or errored out initially
|
||||||
|
# when the media was first uploaded (these bugs should be
|
||||||
|
# reported and fixed).
|
||||||
|
# Note that we don't attempt to generate a thumbnail now because
|
||||||
|
# `dynamic_thumbnails` is disabled.
|
||||||
logger.info("Failed to find any generated thumbnails")
|
logger.info("Failed to find any generated thumbnails")
|
||||||
respond_404(request)
|
|
||||||
|
respond_with_json(
|
||||||
|
request,
|
||||||
|
400,
|
||||||
|
cs_error(
|
||||||
|
"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.)"
|
||||||
|
% (
|
||||||
|
request.postpath,
|
||||||
|
", ".join(THUMBNAIL_SUPPORTED_MEDIA_FORMAT_MAP.keys()),
|
||||||
|
),
|
||||||
|
code=Codes.UNKNOWN,
|
||||||
|
),
|
||||||
|
send_cors=True,
|
||||||
|
)
|
||||||
|
|
||||||
def _select_thumbnail(
|
def _select_thumbnail(
|
||||||
self,
|
self,
|
||||||
|
@ -126,7 +126,9 @@ class _TestImage:
|
|||||||
expected_scaled: The expected bytes from scaled thumbnailing, or None if
|
expected_scaled: The expected bytes from scaled thumbnailing, or None if
|
||||||
test should just check for a valid image returned.
|
test should just check for a valid image returned.
|
||||||
expected_found: True if the file should exist on the server, or False if
|
expected_found: True if the file should exist on the server, or False if
|
||||||
a 404 is expected.
|
a 404/400 is expected.
|
||||||
|
unable_to_thumbnail: True if we expect the thumbnailing to fail (400), or
|
||||||
|
False if the thumbnailing should succeed or a normal 404 is expected.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data: bytes
|
data: bytes
|
||||||
@ -135,6 +137,7 @@ class _TestImage:
|
|||||||
expected_cropped: Optional[bytes] = None
|
expected_cropped: Optional[bytes] = None
|
||||||
expected_scaled: Optional[bytes] = None
|
expected_scaled: Optional[bytes] = None
|
||||||
expected_found: bool = True
|
expected_found: bool = True
|
||||||
|
unable_to_thumbnail: bool = False
|
||||||
|
|
||||||
|
|
||||||
@parameterized_class(
|
@parameterized_class(
|
||||||
@ -192,6 +195,7 @@ class _TestImage:
|
|||||||
b"image/gif",
|
b"image/gif",
|
||||||
b".gif",
|
b".gif",
|
||||||
expected_found=False,
|
expected_found=False,
|
||||||
|
unable_to_thumbnail=True,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -366,18 +370,29 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
def test_thumbnail_crop(self) -> None:
|
def test_thumbnail_crop(self) -> None:
|
||||||
"""Test that a cropped remote thumbnail is available."""
|
"""Test that a cropped remote thumbnail is available."""
|
||||||
self._test_thumbnail(
|
self._test_thumbnail(
|
||||||
"crop", self.test_image.expected_cropped, self.test_image.expected_found
|
"crop",
|
||||||
|
self.test_image.expected_cropped,
|
||||||
|
expected_found=self.test_image.expected_found,
|
||||||
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_thumbnail_scale(self) -> None:
|
def test_thumbnail_scale(self) -> None:
|
||||||
"""Test that a scaled remote thumbnail is available."""
|
"""Test that a scaled remote thumbnail is available."""
|
||||||
self._test_thumbnail(
|
self._test_thumbnail(
|
||||||
"scale", self.test_image.expected_scaled, self.test_image.expected_found
|
"scale",
|
||||||
|
self.test_image.expected_scaled,
|
||||||
|
expected_found=self.test_image.expected_found,
|
||||||
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_type(self) -> None:
|
def test_invalid_type(self) -> None:
|
||||||
"""An invalid thumbnail type is never available."""
|
"""An invalid thumbnail type is never available."""
|
||||||
self._test_thumbnail("invalid", None, False)
|
self._test_thumbnail(
|
||||||
|
"invalid",
|
||||||
|
None,
|
||||||
|
expected_found=False,
|
||||||
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
@unittest.override_config(
|
||||||
{"thumbnail_sizes": [{"width": 32, "height": 32, "method": "scale"}]}
|
{"thumbnail_sizes": [{"width": 32, "height": 32, "method": "scale"}]}
|
||||||
@ -386,7 +401,12 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
"""
|
"""
|
||||||
Override the config to generate only scaled thumbnails, but request a cropped one.
|
Override the config to generate only scaled thumbnails, but request a cropped one.
|
||||||
"""
|
"""
|
||||||
self._test_thumbnail("crop", None, False)
|
self._test_thumbnail(
|
||||||
|
"crop",
|
||||||
|
None,
|
||||||
|
expected_found=False,
|
||||||
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
|
)
|
||||||
|
|
||||||
@unittest.override_config(
|
@unittest.override_config(
|
||||||
{"thumbnail_sizes": [{"width": 32, "height": 32, "method": "crop"}]}
|
{"thumbnail_sizes": [{"width": 32, "height": 32, "method": "crop"}]}
|
||||||
@ -395,14 +415,22 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
"""
|
"""
|
||||||
Override the config to generate only cropped thumbnails, but request a scaled one.
|
Override the config to generate only cropped thumbnails, but request a scaled one.
|
||||||
"""
|
"""
|
||||||
self._test_thumbnail("scale", None, False)
|
self._test_thumbnail(
|
||||||
|
"scale",
|
||||||
|
None,
|
||||||
|
expected_found=False,
|
||||||
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
|
)
|
||||||
|
|
||||||
def test_thumbnail_repeated_thumbnail(self) -> None:
|
def test_thumbnail_repeated_thumbnail(self) -> None:
|
||||||
"""Test that fetching the same thumbnail works, and deleting the on disk
|
"""Test that fetching the same thumbnail works, and deleting the on disk
|
||||||
thumbnail regenerates it.
|
thumbnail regenerates it.
|
||||||
"""
|
"""
|
||||||
self._test_thumbnail(
|
self._test_thumbnail(
|
||||||
"scale", self.test_image.expected_scaled, self.test_image.expected_found
|
"scale",
|
||||||
|
self.test_image.expected_scaled,
|
||||||
|
expected_found=self.test_image.expected_found,
|
||||||
|
unable_to_thumbnail=self.test_image.unable_to_thumbnail,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not self.test_image.expected_found:
|
if not self.test_image.expected_found:
|
||||||
@ -459,8 +487,24 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _test_thumbnail(
|
def _test_thumbnail(
|
||||||
self, method: str, expected_body: Optional[bytes], expected_found: bool
|
self,
|
||||||
|
method: str,
|
||||||
|
expected_body: Optional[bytes],
|
||||||
|
expected_found: bool,
|
||||||
|
unable_to_thumbnail: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
"""Test the given thumbnailing method works as expected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method: The thumbnailing method to use (crop, scale).
|
||||||
|
expected_body: The expected bytes from thumbnailing, or None if
|
||||||
|
test should just check for a valid image.
|
||||||
|
expected_found: True if the file should exist on the server, or False if
|
||||||
|
a 404/400 is expected.
|
||||||
|
unable_to_thumbnail: True if we expect the thumbnailing to fail (400), or
|
||||||
|
False if the thumbnailing should succeed or a normal 404 is expected.
|
||||||
|
"""
|
||||||
|
|
||||||
params = "?width=32&height=32&method=" + method
|
params = "?width=32&height=32&method=" + method
|
||||||
channel = make_request(
|
channel = make_request(
|
||||||
self.reactor,
|
self.reactor,
|
||||||
@ -496,6 +540,16 @@ class MediaRepoTests(unittest.HomeserverTestCase):
|
|||||||
else:
|
else:
|
||||||
# ensure that the result is at least some valid image
|
# ensure that the result is at least some valid image
|
||||||
Image.open(BytesIO(channel.result["body"]))
|
Image.open(BytesIO(channel.result["body"]))
|
||||||
|
elif unable_to_thumbnail:
|
||||||
|
# A 400 with a JSON body.
|
||||||
|
self.assertEqual(channel.code, 400)
|
||||||
|
self.assertEqual(
|
||||||
|
channel.json_body,
|
||||||
|
{
|
||||||
|
"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.)",
|
||||||
|
},
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# A 404 with a JSON body.
|
# A 404 with a JSON body.
|
||||||
self.assertEqual(channel.code, 404)
|
self.assertEqual(channel.code, 404)
|
||||||
|
Loading…
Reference in New Issue
Block a user