Implement new send_federation_http_request module API

This new API allows a module to send an HTTP request authenticated as the homeserver. This allows modules to make arbitrary requests to other homeservers on behalf of the homeserver.
This commit is contained in:
Andrew Morgan
2026-07-22 16:59:15 +01:00
parent 3622579e7b
commit 7fd9f5d9fc
3 changed files with 181 additions and 1 deletions
+113 -1
View File
@@ -43,7 +43,12 @@ from twisted.web.resource import Resource
from synapse.api import errors
from synapse.api.constants import ProfileFields
from synapse.api.errors import SynapseError
from synapse.api.errors import (
FederationDeniedError,
HttpResponseException,
RequestSendFailed,
SynapseError,
)
from synapse.api.presence import UserPresenceState
from synapse.config import ConfigError
from synapse.config.repository import MediaUploadLimit
@@ -131,6 +136,12 @@ from synapse.module_api.callbacks.third_party_event_rules_callbacks import (
ON_THREEPID_BIND_CALLBACK,
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
)
from synapse.module_api.module_errors import (
FederationHttpDeniedException,
FederationHttpNotRetryingDestinationException,
FederationHttpRequestSendFailedException,
FederationHttpResponseException,
)
from synapse.push.httppusher import HttpPusher
from synapse.rest.client.login import LoginResponse
from synapse.storage import DataStore
@@ -162,6 +173,7 @@ from synapse.util.caches.descriptors import CachedFunction, cached as _cached
from synapse.util.clock import Clock
from synapse.util.duration import Duration
from synapse.util.frozenutils import freeze
from synapse.util.retryutils import NotRetryingDestination
if TYPE_CHECKING:
# Old versions don't have `LiteralString`
@@ -644,6 +656,106 @@ class ModuleApi:
"""
return self._http_client
async def send_federation_http_request(
self,
method: str,
remote_server_name: str,
path: str,
query_parameters: Mapping[str, Any] | None = None,
body: JsonDict | None = None,
timeout: int | None = None,
) -> JsonDict:
"""Send an authenticated HTTP request to a remote homeserver.
Synapse signs the request with the local homeserver's signing key and sends it
using the configured federation routing, TLS, IP filtering, and retry policy.
Added in Synapse v1.158.0.
Args:
method: The HTTP method to use. One of `GET`, `PUT`, `POST`, or
`DELETE`. Case-insensitive.
remote_server_name: The Matrix server name to send the request to.
Federation delegation is resolved automatically.
path: The absolute HTTP path for the request.
query_parameters: Query parameters to include in the request.
body: The JSON request body for `PUT` and `POST` requests.
timeout: Number of milliseconds to wait for response headers and the
response body. The configured federation timeout is used by default.
Returns:
The decoded JSON object returned by the remote homeserver.
Raises:
ValueError: If `method` is not supported.
FederationHttpResponseException: If the remote homeserver returns an
unsuccessful, non-retryable HTTP response.
FederationHttpNotRetryingDestinationException: If Synapse is backing off
requests to the remote homeserver.
FederationHttpDeniedException: If the remote homeserver is excluded by
Synapse's federation policy.
FederationHttpRequestSendFailedException: If the request could not be sent
or the response could not be decoded.
"""
method = method.upper()
federation_http_client = self._hs.get_federation_http_client()
try:
if method == "GET":
return await federation_http_client.get_json(
destination=remote_server_name,
path=path,
args=query_parameters,
timeout=timeout,
)
if method == "PUT":
return await federation_http_client.put_json(
destination=remote_server_name,
path=path,
args=query_parameters,
data=body,
timeout=timeout,
)
if method == "POST":
return await federation_http_client.post_json(
destination=remote_server_name,
path=path,
args=query_parameters,
data=body,
timeout=timeout,
)
if method == "DELETE":
return await federation_http_client.delete_json(
destination=remote_server_name,
path=path,
args=query_parameters,
timeout=timeout,
)
raise ValueError(
f"method must be one of GET, PUT, POST, or DELETE; received {method!r}"
)
except HttpResponseException as e:
raise FederationHttpResponseException(
remote_server_name=remote_server_name,
status_code=e.code,
msg=e.msg,
response_body=e.response,
) from e
except NotRetryingDestination as e:
raise FederationHttpNotRetryingDestinationException(
remote_server_name=remote_server_name
) from e
except FederationDeniedError as e:
raise FederationHttpDeniedException(
remote_server_name=remote_server_name
) from e
except RequestSendFailed as e:
raise FederationHttpRequestSendFailedException(
remote_server_name=remote_server_name,
can_retry=e.can_retry,
) from e
@property
def public_room_list_manager(self) -> "PublicRoomListManager":
"""Allows adding to, removing from and checking the status of rooms in the
+10
View File
@@ -29,6 +29,12 @@ from synapse.api.errors import (
)
from synapse.config._base import ConfigError
from synapse.handlers.push_rules import InvalidRuleException
from synapse.module_api.module_errors import (
FederationHttpDeniedException,
FederationHttpNotRetryingDestinationException,
FederationHttpRequestSendFailedException,
FederationHttpResponseException,
)
from synapse.storage.push_rule import RuleNotFoundException
__all__ = [
@@ -37,6 +43,10 @@ __all__ = [
"RedirectException",
"SynapseError",
"ConfigError",
"FederationHttpDeniedException",
"FederationHttpNotRetryingDestinationException",
"FederationHttpRequestSendFailedException",
"FederationHttpResponseException",
"InvalidRuleException",
"RuleNotFoundException",
]
+58
View File
@@ -0,0 +1,58 @@
#
# This file is licensed under the Affero General Public License (AGPL) version 3.
#
# Copyright (C) 2026 New Vector, 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>.
#
"""Exception types specific to the module API.
The definitions cannot live in `synapse.module_api.errors` because
`synapse.module_api` historically re-exports `synapse.api.errors` under the name
`errors`. Importing the `synapse.module_api.errors` submodule while initializing the
parent package would replace that re-export and could break existing modules.
The public `synapse.module_api.errors` module re-exports these exceptions, while the
parent package imports them directly from here to avoid that namespace collision.
"""
import attr
@attr.s(auto_attribs=True, slots=True)
class FederationHttpResponseException(Exception):
"""A remote homeserver returned an unsuccessful HTTP response."""
remote_server_name: str
status_code: int
msg: str
response_body: bytes
@attr.s(auto_attribs=True, slots=True)
class FederationHttpNotRetryingDestinationException(Exception):
"""Synapse is backing off federation requests to the remote homeserver."""
remote_server_name: str
@attr.s(auto_attribs=True, slots=True)
class FederationHttpDeniedException(Exception):
"""The remote homeserver is excluded by Synapse's federation policy."""
remote_server_name: str
@attr.s(auto_attribs=True, slots=True)
class FederationHttpRequestSendFailedException(Exception):
"""Synapse could not send or decode a federation HTTP request."""
remote_server_name: str
can_retry: bool