Return M_USER_LIMIT_EXCEEDED error code for media upload limits from MSC4335 (#18876)

This PR uses the new `M_USER_LIMIT_EXCEEDED` common error code instead
of the malformed `M_RESOURCE_LIMIT_EXCEEDED` error code (as reported by
#18749) (spec: MSC4335).

The error code is also changed from `400` to `403` as this matches what
is in the spec for the endpoints:
https://spec.matrix.org/v1.18/client-server-api/#post_matrixmediav3upload
and
https://spec.matrix.org/v1.18/client-server-api/#put_matrixmediav3uploadservernamemediaid
(albeit the latter says that `M_FORBIDDEN` would be returned)

By default a new built-in `media_upload_limit_exceeded.html` template
will be served for the `info_uri`. Administrators can specify an
external URI in config instead.

Compatibility is retained for any existing modules making use the
`MediaUploadLimit` (e.g. via the `get_media_upload_limits_for_user`
callback).
This commit is contained in:
Hugh Nimmo-Smith authored and GitHub committed 2026-07-24 16:39:04 -05:00
1 parent a58ad44eb0
commit 97fb38eca6
11 files changed
+419 -36

No files matched your search

+1
View File
@@ -0,0 +1 @@
Return `M_USER_LIMIT_EXCEEDED` error code for media upload limits from [MSC4335](https://github.com/matrix-org/matrix-spec-proposals/pull/4335).
@@ -2228,13 +2228,26 @@ These settings can be overridden using the `get_media_upload_limits_for_user` mo
Defaults to `[]`.
Options for each entry include:
* `time_period` (duration): The time period over which the limit applies. Required.
* `max_size` (byte size): Amount of data that can be uploaded in the time period by the user. Required.
* `info_uri` (string): URI returned to the client for where the user can find information about the upload limit and how users can reduce their upload usage or request an upload limit increase. Optional. If not set, Synapse serves a built-in page (customisable via the `media_upload_limit_exceeded.html` template) and uses its URL instead.
* `can_upgrade` (boolean): Value returned to the client for whether the limit can be increased. Defaults to `false`.
Example configuration:
```yaml
media_upload_limits:
- time_period: 1h
max_size: 100M
info_uri: https://example.com/quota#hour
- time_period: 1w
max_size: 500M
info_uri: https://example.com/quota
can_upgrade: true
```
---
### `max_image_pixels`
+31 -9
View File
@@ -2505,20 +2505,42 @@ properties:
module API [callback](../../modules/media_repository_callbacks.md#get_media_upload_limits_for_user).
default: []
items:
time_period:
type: "#/$defs/duration"
description: >-
The time period over which the limit applies. Required.
max_size:
type: "#/$defs/bytes"
description: >-
Amount of data that can be uploaded in the time period by the user.
Required.
type: object
required:
- time_period
- max_size
properties:
time_period:
$ref: "#/$defs/duration"
description: >-
The time period over which the limit applies. Required.
max_size:
$ref: "#/$defs/bytes"
description: >-
Amount of data that can be uploaded in the time period by the user.
Required.
info_uri:
type: string
description: >-
URI returned to the client for where the user can find information
about the upload limit and how users can reduce their upload usage
or request an upload limit increase. Optional.
If not set, Synapse serves a built-in page (customisable via the
`media_upload_limit_exceeded.html` template) and uses its URL
instead.
can_upgrade:
type: boolean
description: >-
Value returned to the client for whether the limit can be increased.
default: false
examples:
- - time_period: 1h
max_size: 100M
info_uri: https://example.com/quota#hour
- time_period: 1w
max_size: 500M
info_uri: https://example.com/quota
can_upgrade: true
max_image_pixels:
$ref: "#/$defs/bytes"
description: Maximum number of pixels that will be thumbnailed.
+30
View File
@@ -152,6 +152,8 @@ class Codes(str, Enum):
# Part of MSC4326
UNKNOWN_DEVICE = "ORG.MATRIX.MSC4326.M_UNKNOWN_DEVICE"
USER_LIMIT_EXCEEDED = "M_USER_LIMIT_EXCEEDED"
class CodeMessageException(RuntimeError):
"""An exception with integer code, a message string attributes and optional headers.
@@ -513,6 +515,34 @@ class ResourceLimitError(SynapseError):
)
class UserLimitExceededError(SynapseError):
"""
Implementation of M_USER_LIMIT_EXCEEDED error
"""
def __init__(
self,
code: int,
msg: str,
*,
info_uri: str,
can_upgrade: bool = False,
):
additional_fields: dict[str, str | bool] = {
"info_uri": info_uri,
}
if can_upgrade:
additional_fields["can_upgrade"] = can_upgrade
super().__init__(
code,
msg,
Codes.USER_LIMIT_EXCEEDED,
additional_fields=additional_fields,
)
class EventSizeError(SynapseError):
"""An error raised when an event is too big."""
+70 -5
View File
@@ -3,6 +3,7 @@
#
# Copyright 2014, 2015 OpenMarket Ltd
# Copyright (C) 2023 New Vector, Ltd
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -21,14 +22,16 @@
import logging
import os
from typing import Any
from typing import Annotated, Any
import attr
from pydantic import AnyUrl, BeforeValidator, ValidationError
from synapse.config.server import generate_ip_set, parse_proxy_config
from synapse.types import JsonDict
from synapse.util.check_dependencies import check_requirements
from synapse.util.module_loader import load_module
from synapse.util.pydantic_models import ParseModel
from ._base import Config, ConfigError
@@ -134,6 +137,27 @@ class MediaUploadLimit:
time_period_ms: int
"""The time period in milliseconds."""
info_uri: str | None = None
"""The URI to return with the M_USER_LIMIT_EXCEEDED error.
If left unset (`None`), Synapse falls back to a static page served by itself
(see `MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH`), which explains that the limit has been
exceeded and can be customized by server administrators via a custom
template."""
can_upgrade: bool = False
"""Whether the user can upgrade their plan to increase the limit. This is returned in the M_USER_LIMIT_EXCEEDED error."""
class MediaUploadLimitConfigModel(ParseModel):
"""Internal model for parsing a single media_upload_limits config entry."""
max_size: Annotated[int, BeforeValidator(Config.parse_size)]
time_period: Annotated[int, BeforeValidator(Config.parse_duration)]
info_uri: AnyUrl | None = None
"""We accept AnyUrl as a subset of valid URIs. It could be widened in future if needed."""
can_upgrade: bool = False
class ContentRepositoryConfig(Config):
section = "media"
@@ -143,6 +167,18 @@ class ContentRepositoryConfig(Config):
# is not a media repo worker, as it's exposed in `/capabilities`
self.url_preview_enabled = bool(config.get("url_preview_enabled", False))
# Load the template used to render the fallback page.
#
# We set this up on all workers (not just the media repo) as the
# fallback page is served by whichever process handles
# `/_synapse/client/media_upload_limit_exceeded`, so every process must
# be able to render it. This must happen before the early return below,
# which is taken by workers that do not load the media repo.
self.media_upload_limit_exceeded_template = self.read_templates(
["media_upload_limit_exceeded.html"],
(td for td in (self.root.server.custom_template_directory,) if td),
)[0]
# Only enable the media repo if either the media repo is enabled or the
# current worker app is the media repo.
if (
@@ -308,11 +344,40 @@ class ContentRepositoryConfig(Config):
self.enable_authenticated_media = config.get("enable_authenticated_media", True)
self.media_upload_limits: list[MediaUploadLimit] = []
for limit_config in config.get("media_upload_limits", []):
time_period_ms = self.parse_duration(limit_config["time_period"])
max_bytes = self.parse_size(limit_config["max_size"])
for raw_entry in config.get("media_upload_limits", []):
try:
entry = MediaUploadLimitConfigModel.model_validate(
raw_entry, strict=True
)
except ValidationError as e:
raise ConfigError(
"Could not validate media_upload_limits entry",
("media_upload_limits",),
) from e
self.media_upload_limits.append(MediaUploadLimit(max_bytes, time_period_ms))
info_uri = str(entry.info_uri) if entry.info_uri is not None else None
self.media_upload_limits.append(
MediaUploadLimit(
max_bytes=entry.max_size,
time_period_ms=entry.time_period,
info_uri=info_uri,
can_upgrade=entry.can_upgrade,
)
)
# The absolute URI of the static fallback page, used as the `info_uri`
# for any media upload limit (whether from config or a module callback)
# that doesn't specify one. Built from public_baseurl so that it is a
# usable absolute URL. We import here to avoid a circular import at
# module load time.
from synapse.rest.synapse.client.media_upload_limit_exceeded import (
MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH,
)
self.media_upload_limit_fallback_info_uri = (
self.root.server.public_baseurl
+ MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH.lstrip("/")
)
def generate_config_section(self, data_dir_path: str, **kwargs: Any) -> str:
assert data_dir_path is not None
+15 -2
View File
@@ -40,6 +40,7 @@ from synapse.api.errors import (
NotFoundError,
RequestSendFailed,
SynapseError,
UserLimitExceededError,
cs_error,
)
from synapse.api.ratelimiting import Ratelimiter
@@ -400,8 +401,20 @@ class MediaRepository:
sent_bytes=uploaded_media_size,
attempted_bytes=content_length,
)
raise SynapseError(
400, "Media upload limit exceeded", Codes.RESOURCE_LIMIT_EXCEEDED
# Fall back to the static page served by Synapse when the limit
# doesn't specify its own `info_uri` (e.g. limits returned by a
# module callback without one).
info_uri = (
limit.info_uri
or self.hs.config.media.media_upload_limit_fallback_info_uri
)
raise UserLimitExceededError(
403,
"Media upload limit exceeded",
info_uri=info_uri,
can_upgrade=limit.can_upgrade,
)
if is_new_media:
@@ -150,7 +150,10 @@ class MediaRepositoryModuleApiCallbacks:
):
# Use a copy of the data in case the module modifies it
limit_copy = MediaUploadLimit(
max_bytes=limit.max_bytes, time_period_ms=limit.time_period_ms
max_bytes=limit.max_bytes,
time_period_ms=limit.time_period_ms,
info_uri=limit.info_uri,
can_upgrade=limit.can_upgrade,
)
await delay_cancellation(
callback(user_id, limit_copy, sent_bytes, attempted_bytes)
@@ -0,0 +1,6 @@
{% extends "_base.html" %}
{% block title %}Media upload limit exceeded{% endblock %}
{% block body %}
<p>You have exceeded a media upload limit. Ask your server administrator for more information.</p>
{% endblock %}
+11
View File
@@ -3,6 +3,7 @@
#
# Copyright 2021 The Matrix.org Foundation C.I.C.
# Copyright (C) 2023 New Vector, Ltd
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -24,6 +25,10 @@ from typing import TYPE_CHECKING, Mapping
from twisted.web.resource import Resource
from synapse.rest.synapse.client.federation_whitelist import FederationWhitelistResource
from synapse.rest.synapse.client.media_upload_limit_exceeded import (
MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH,
MediaUploadLimitExceededResource,
)
from synapse.rest.synapse.client.new_user_consent import NewUserConsentResource
from synapse.rest.synapse.client.pick_idp import PickIdpResource
from synapse.rest.synapse.client.pick_username import pick_username_resource
@@ -54,6 +59,12 @@ def build_synapse_client_resource_tree(hs: "HomeServer") -> Mapping[str, Resourc
"/_synapse/client/sso_register": SsoRegisterResource(hs),
# Unsubscribe to notification emails link
"/_synapse/client/unsubscribe": UnsubscribeResource(hs),
# Fallback page served as the `info_uri` for media upload limits that
# don't have an explicit `info_uri`. Mounted unconditionally: in a
# worker deployment, the media repo (which generates the errors)
# typically runs on a different worker than the one serving
# `/_synapse/client`.
MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH: MediaUploadLimitExceededResource(hs),
}
if hs.config.mas.enabled:
@@ -0,0 +1,42 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# See the GNU Affero General Public License for more details:
# <https://www.gnu.org/licenses/agpl-3.0.html>.
#
from typing import TYPE_CHECKING
from synapse.http.server import DirectServeHtmlResource, respond_with_html
from synapse.http.site import SynapseRequest
if TYPE_CHECKING:
from synapse.server import HomeServer
# The path at which the fallback media upload limit exceeded info page is served.
# This is used as the `info_uri` returned in the `M_USER_LIMIT_EXCEEDED` error
# when no `info_uri` has been specified for a media upload limit.
MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH = "/_synapse/client/media_upload_limit_exceeded"
class MediaUploadLimitExceededResource(DirectServeHtmlResource):
"""Serves a static fallback page explaining that a media upload limit has
been exceeded.
This is used as the `info_uri` for the `M_USER_LIMIT_EXCEEDED` error when no
`info_uri` has been specified for a media upload limit.
"""
def __init__(self, hs: "HomeServer"):
super().__init__(clock=hs.get_clock())
self._template = hs.config.media.media_upload_limit_exceeded_template
async def _async_render_GET(self, request: SynapseRequest) -> None:
respond_with_html(request, 200, self._template.render())
+196 -19
View File
@@ -3,6 +3,7 @@
#
# Copyright 2022 The Matrix.org Foundation C.I.C.
# Copyright (C) 2024 New Vector, Ltd
# Copyright (C) 2026 Element Creations Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@@ -58,6 +59,10 @@ from synapse.media.url_previewer import IMAGE_CACHE_EXPIRY_MS
from synapse.module_api import MediaUploadLimit
from synapse.rest import admin
from synapse.rest.client import login, media
from synapse.rest.synapse.client import build_synapse_client_resource_tree
from synapse.rest.synapse.client.media_upload_limit_exceeded import (
MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH,
)
from synapse.server import HomeServer
from synapse.types import JsonDict, UserID
from synapse.util.clock import Clock
@@ -2933,11 +2938,20 @@ class MediaUploadLimits(unittest.HomeserverTestCase):
config["media_storage_providers"] = [provider_config]
# These are the limits that we are testing
config["media_upload_limits"] = [
{"time_period": "1d", "max_size": "1K"},
{"time_period": "1w", "max_size": "3K"},
]
# These are the limits that we are testing unless overridden
if config.get("media_upload_limits") is None:
config["media_upload_limits"] = [
{
"time_period": "1d",
"max_size": "1K",
"info_uri": "https://example.com/limits#daily",
},
{
"time_period": "1w",
"max_size": "3K",
"info_uri": "https://example.com/limits#weekly",
},
]
return self.setup_test_homeserver(config=config)
@@ -2951,6 +2965,9 @@ class MediaUploadLimits(unittest.HomeserverTestCase):
def create_resource_dict(self) -> dict[str, Resource]:
resources = super().create_resource_dict()
resources["/_matrix/media"] = self.hs.get_media_repository_resource()
# Mount the `/_synapse/client` tree, which includes the fallback page
# served at `MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH` which we use in the tests.
resources.update(build_synapse_client_resource_tree(self.hs))
return resources
def upload_media(self, size: int) -> FakeChannel:
@@ -2975,7 +2992,8 @@ class MediaUploadLimits(unittest.HomeserverTestCase):
self.assertEqual(channel.code, 200)
channel = self.upload_media(800)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.code, 403)
self.assertEqual(channel.json_body["errcode"], "M_USER_LIMIT_EXCEEDED")
def test_under_daily_limit(self) -> None:
"""Test that uploading media under the daily limit fails."""
@@ -3013,7 +3031,8 @@ class MediaUploadLimits(unittest.HomeserverTestCase):
# This will fail as the weekly limit has been exceeded
channel = self.upload_media(900)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.code, 403)
self.assertEqual(channel.json_body["errcode"], "M_USER_LIMIT_EXCEEDED")
# Reset the weekly limit by advancing a week
self.reactor.advance(7 * 60 * 60 * 24) # Advance by 7 days
@@ -3022,6 +3041,104 @@ class MediaUploadLimits(unittest.HomeserverTestCase):
channel = self.upload_media(900)
self.assertEqual(channel.code, 200)
@override_config(
{
"media_upload_limits": [
{
"time_period": "1d",
"max_size": "1K",
# No `info_uri` is configured, so the fallback resource
# should be used instead.
}
],
}
)
def test_falls_back_to_served_page_when_no_info_uri(self) -> None:
"""When no info_uri is configured, the error should point at a page
served by Synapse, and that page should return HTML."""
channel = self.upload_media(1300)
self.assertEqual(channel.code, 403)
self.assertEqual(channel.json_body["errcode"], "M_USER_LIMIT_EXCEEDED")
expected_info_uri = (
self.hs.config.server.public_baseurl
+ MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH.lstrip("/")
)
self.assertEqual(channel.json_body["info_uri"], expected_info_uri)
# The fallback page should be served by Synapse and return HTML.
page = self.make_request(
"GET",
MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH,
shorthand=False,
)
self.assertEqual(page.code, 200)
self.assertEqual(
page.headers.getRawHeaders("Content-Type"),
["text/html; charset=utf-8"],
)
self.assertIn(b"upload limit", page.result["body"])
def test_fallback_page_mounted(self) -> None:
"""The fallback resource should always be mounted, even if every configured
limit has an explicit info_uri, since module callbacks can return
limits without an info_uri at any time."""
# The default config for this test case sets an info_uri on every limit.
page = self.make_request(
"GET",
MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH,
shorthand=False,
)
self.assertEqual(page.code, 200)
self.assertEqual(
page.headers.getRawHeaders("Content-Type"),
["text/html; charset=utf-8"],
)
@override_config(
{
"media_upload_limits": [
{
"time_period": "1d",
"max_size": "1K",
"info_uri": "https://example.com",
}
],
}
)
def test_returns_hard_user_limit_exceeded_by_default(self) -> None:
"""Test that the error is returned with can_upgrade False by default."""
channel = self.upload_media(1300)
self.assertEqual(channel.code, 403)
self.assertEqual(channel.json_body["errcode"], "M_USER_LIMIT_EXCEEDED")
self.assertEqual(channel.json_body["info_uri"], "https://example.com/")
# the spec says that can_upgrade should not be included if it is False
self.assertIsNone(channel.json_body.get("can_upgrade"))
@override_config(
{
"media_upload_limits": [
{
"time_period": "1d",
"max_size": "1K",
"info_uri": "https://example.com",
"can_upgrade": True,
}
],
}
)
def test_returns_soft_user_limit_exceeded(self) -> None:
"""Test that the M_USER_LIMIT_EXCEEDED error is returned with
can_upgrade True when specified in config."""
channel = self.upload_media(500)
self.assertEqual(channel.code, 200)
channel = self.upload_media(800)
self.assertEqual(channel.code, 403)
self.assertEqual(channel.json_body["errcode"], "M_USER_LIMIT_EXCEEDED")
self.assertEqual(channel.json_body["info_uri"], "https://example.com/")
self.assertEqual(channel.json_body["can_upgrade"], True)
class MediaUploadLimitsModuleOverrides(unittest.HomeserverTestCase):
"""
@@ -3054,10 +3171,19 @@ class MediaUploadLimitsModuleOverrides(unittest.HomeserverTestCase):
config["media_storage_providers"] = [provider_config]
# default limits to use
config["media_upload_limits"] = [
{"time_period": "1d", "max_size": "1K"},
{"time_period": "1w", "max_size": "3K"},
]
if config.get("media_upload_limits") is None:
config["media_upload_limits"] = [
{
"time_period": "1d",
"max_size": "1K",
"info_uri": "https://example.com/limits#daily",
},
{
"time_period": "1w",
"max_size": "3K",
"info_uri": "https://example.com/limits#weekly",
},
]
return self.setup_test_homeserver(config=config)
@@ -3070,10 +3196,14 @@ class MediaUploadLimitsModuleOverrides(unittest.HomeserverTestCase):
# n.b. we return these in increasing duration order and Synapse will need to sort them correctly
return [
MediaUploadLimit(
time_period_ms=Config.parse_duration("1d"), max_bytes=5000
time_period_ms=Config.parse_duration("1d"),
max_bytes=5000,
info_uri="https://override.example.com/limits#daily",
),
MediaUploadLimit(
time_period_ms=Config.parse_duration("1w"), max_bytes=15000
time_period_ms=Config.parse_duration("1w"),
max_bytes=15000,
info_uri="https://override.example.com/limits#weekly",
),
]
# user2 has no limits
@@ -3154,13 +3284,16 @@ class MediaUploadLimitsModuleOverrides(unittest.HomeserverTestCase):
# User 1 attempts to upload 4000 bytes taking it over the limit
channel = self.upload_media(4000, self.tok1)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.code, 403)
assert self.last_media_upload_limit_exceeded is not None
self.assertEqual(self.last_media_upload_limit_exceeded["user_id"], self.user1)
self.assertEqual(
self.last_media_upload_limit_exceeded["limit"],
MediaUploadLimit(
max_bytes=5000, time_period_ms=Config.parse_duration("1d")
max_bytes=5000,
time_period_ms=Config.parse_duration("1d"),
info_uri="https://override.example.com/limits#daily",
can_upgrade=False,
),
)
self.assertEqual(self.last_media_upload_limit_exceeded["sent_bytes"], 3000)
@@ -3169,13 +3302,16 @@ class MediaUploadLimitsModuleOverrides(unittest.HomeserverTestCase):
# User 1 attempts to upload 20000 bytes which is over the weekly limit
# This tests that the limits have been sorted as expected
channel = self.upload_media(20000, self.tok1)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.code, 403)
assert self.last_media_upload_limit_exceeded is not None
self.assertEqual(self.last_media_upload_limit_exceeded["user_id"], self.user1)
self.assertEqual(
self.last_media_upload_limit_exceeded["limit"],
MediaUploadLimit(
max_bytes=15000, time_period_ms=Config.parse_duration("1w")
max_bytes=15000,
time_period_ms=Config.parse_duration("1w"),
info_uri="https://override.example.com/limits#weekly",
can_upgrade=False,
),
)
self.assertEqual(self.last_media_upload_limit_exceeded["sent_bytes"], 3000)
@@ -3198,18 +3334,59 @@ class MediaUploadLimitsModuleOverrides(unittest.HomeserverTestCase):
# User 3 uploads 800 bytes which is over the limit
channel = self.upload_media(800, self.tok3)
self.assertEqual(channel.code, 400)
self.assertEqual(channel.code, 403)
assert self.last_media_upload_limit_exceeded is not None
self.assertEqual(self.last_media_upload_limit_exceeded["user_id"], self.user3)
self.assertEqual(
self.last_media_upload_limit_exceeded["limit"],
MediaUploadLimit(
max_bytes=1024, time_period_ms=Config.parse_duration("1d")
max_bytes=1024,
time_period_ms=Config.parse_duration("1d"),
info_uri="https://example.com/limits#daily",
can_upgrade=False,
),
)
self.assertEqual(self.last_media_upload_limit_exceeded["sent_bytes"], 500)
self.assertEqual(self.last_media_upload_limit_exceeded["attempted_bytes"], 800)
def test_module_limit_without_info_uri_falls_back(self) -> None:
"""A limit returned by a module callback without an info_uri falls back
to the static page served by Synapse when the error is generated."""
async def _limits_without_info_uri(
user_id: str,
) -> list[MediaUploadLimit] | None:
# info_uri is omitted (defaults to None).
return [
MediaUploadLimit(
time_period_ms=Config.parse_duration("1d"),
max_bytes=123,
)
]
self.hs.get_module_api().register_media_repository_callbacks(
get_media_upload_limits_for_user=_limits_without_info_uri,
)
# This is over the limit of 123 bytes, but below the default limit of 1k bytes
channel = self.upload_media(130, self.tok3)
self.assertEqual(channel.code, 403)
self.assertEqual(channel.json_body["errcode"], "M_USER_LIMIT_EXCEEDED")
# Assert that the upload limit from the module callback was used (identified by
# max_bytes=123) (as opposed to the global default).
assert self.last_media_upload_limit_exceeded is not None
limit = self.last_media_upload_limit_exceeded["limit"]
assert isinstance(limit, MediaUploadLimit)
self.assertEqual(limit.max_bytes, 123)
# Assert that the info_uri was correctly populated (even though not provided by the module)
expected_info_uri = (
self.hs.config.server.public_baseurl
+ MEDIA_UPLOAD_LIMIT_EXCEEDED_PATH.lstrip("/")
)
self.assertEqual(channel.json_body["info_uri"], expected_info_uri)
class AnimatedThumbnailTestCase(unittest.HomeserverTestCase):
"""End-to-end tests for the `animated` query parameter on the local