diff --git a/synapse/api/errors.py b/synapse/api/errors.py
index 7ffd72c42c..578e798773 100644
--- a/synapse/api/errors.py
+++ b/synapse/api/errors.py
@@ -16,6 +16,7 @@
"""Contains exceptions and error codes."""
import logging
+import math
import typing
from enum import Enum
from http import HTTPStatus
@@ -503,6 +504,8 @@ class InvalidCaptchaError(SynapseError):
class LimitExceededError(SynapseError):
"""A client has sent too many requests and is being throttled."""
+ include_retry_after_header = False
+
def __init__(
self,
code: int = 429,
@@ -510,7 +513,12 @@ class LimitExceededError(SynapseError):
retry_after_ms: Optional[int] = None,
errcode: str = Codes.LIMIT_EXCEEDED,
):
- super().__init__(code, msg, errcode)
+ headers = (
+ {"Retry-After": str(math.ceil(retry_after_ms / 1000))}
+ if self.include_retry_after_header and retry_after_ms is not None
+ else None
+ )
+ super().__init__(code, msg, errcode, headers=headers)
self.retry_after_ms = retry_after_ms
def error_dict(self, config: Optional["HomeServerConfig"]) -> "JsonDict":
diff --git a/synapse/config/experimental.py b/synapse/config/experimental.py
index 84d6dd13af..cabe0d4397 100644
--- a/synapse/config/experimental.py
+++ b/synapse/config/experimental.py
@@ -18,6 +18,7 @@ from typing import TYPE_CHECKING, Any, Optional
import attr
import attr.validators
+from synapse.api.errors import LimitExceededError
from synapse.api.room_versions import KNOWN_ROOM_VERSIONS, RoomVersions
from synapse.config import ConfigError
from synapse.config._base import Config, RootConfig
@@ -406,3 +407,11 @@ class ExperimentalConfig(Config):
self.msc4010_push_rules_account_data = experimental.get(
"msc4010_push_rules_account_data", False
)
+
+ # MSC4041: Use HTTP header Retry-After to enable library-assisted retry handling
+ #
+ # This is a bit hacky, but the most reasonable way to *alway* include the
+ # headers.
+ LimitExceededError.include_retry_after_header = experimental.get(
+ "msc4041_enabled", False
+ )
|