Compare commits

..

7 Commits

Author SHA1 Message Date
timedout
3d8e763ae8 fix: M_BAD_JSON in send_join and send_knock 2026-01-12 14:29:35 +00:00
Astralchroma
60dd6baffd Link to documentation clarifying what exactly "Performance optimised version." means 2026-01-11 16:54:33 +00:00
timedout
99a10998b4 style: Remove unused import 2026-01-11 15:42:06 +00:00
nex
05c6b5df75 fix: M_BAD_JSON in c2s invite 2026-01-11 15:37:59 +00:00
Jade Ellis
74db426c6b fix: Correct federation timeouts 2026-01-09 19:51:29 +00:00
Jade Ellis
344d68dabc fix: Use correct token handlers for Ruma 2026-01-09 19:42:14 +00:00
Jade Ellis
d3ee9c407a fix: Apply timeouts in more places 2026-01-09 19:42:13 +00:00
12 changed files with 104 additions and 88 deletions

View File

@@ -340,7 +340,9 @@
# this to be high to account for extremely large room joins, slow
# homeservers, your own resources etc.
#
#federation_timeout = 300
# Joins have 6x the timeout.
#
#federation_timeout = 60
# MSC4284 Policy server request timeout (seconds). Generally policy
# servers should respond near instantly, however may slow down under
@@ -389,7 +391,15 @@
#
#appservice_idle_timeout = 300
# Notification gateway pusher idle connection pool timeout.
# Notification gateway pusher request connection timeout (seconds).
#
#pusher_conn_timeout = 15
# Notification gateway pusher total request timeout (seconds).
#
#pusher_timeout = 60
# Notification gateway pusher idle connection pool timeout (seconds).
#
#pusher_idle_timeout = 15
@@ -1446,6 +1456,11 @@
#
#url_preview_max_spider_size = 256000
# Total request timeout for URL previews (seconds). This includes
# connection, request, and response body reading time.
#
#url_preview_timeout = 120
# Option to decide whether you would like to run the domain allowlist
# checks (contains and explicit) on the root domain or not. Does not apply
# to URL contains allowlist. Defaults to false.

View File

@@ -13,8 +13,8 @@ ### Use a registry
| --------------- | --------------------------------------------------------------- | -----------------------|
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest) | Latest tagged image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main) | Main branch image. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf) | Performance optimised version. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf) | Performance optimised version. |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf) | [Performance optimised version.](./generic.mdx#performance-optimised-builds) |
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf) | [Performance optimised version.](./generic.mdx#performance-optimised-builds) |
Use

View File

@@ -7,7 +7,7 @@
};
use futures::FutureExt;
use ruma::{
OwnedServerName, RoomId, UserId,
RoomId, UserId,
api::{client::membership::invite_user, federation::membership::create_invite},
events::{
invite_permission_config::FilterLevel,
@@ -203,19 +203,10 @@ pub(crate) async fn invite_helper(
))));
}
let origin: OwnedServerName = serde_json::from_value(serde_json::to_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event missing origin field."))))?,
)?)
.map_err(|e| {
err!(Request(BadJson(warn!("Origin field in event is not a valid server name: {e}"))))
})?;
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, room_id, &event_id, value, true)
.handle_incoming_pdu(recipient_user.server_name(), room_id, &event_id, value, true)
.boxed()
.await?
.ok_or_else(|| {

View File

@@ -178,15 +178,6 @@ async fn create_join_event(
}
}
let origin: OwnedServerName = serde_json::from_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event does not have an origin server name."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson("Event has an invalid origin server name: {e}"))))?;
trace!("Signing send_join event");
services
.server_keys
@@ -204,7 +195,7 @@ async fn create_join_event(
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, room_id, &event_id, value.clone(), true)
.handle_incoming_pdu(sender.server_name(), room_id, &event_id, value.clone(), true)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;

View File

@@ -136,15 +136,6 @@ pub(crate) async fn create_knock_event_v1_route(
return Err!(Request(InvalidParam("state_key does not match sender user of event.")));
}
let origin: OwnedServerName = serde_json::from_value(
value
.get("origin")
.ok_or_else(|| err!(Request(BadJson("Event does not have an origin server name."))))?
.clone()
.into(),
)
.map_err(|e| err!(Request(BadJson("Event has an invalid origin server name: {e}"))))?;
let mut event: JsonObject = serde_json::from_str(body.pdu.get())
.map_err(|e| err!(Request(InvalidParam("Invalid knock event PDU: {e}"))))?;
@@ -163,7 +154,7 @@ pub(crate) async fn create_knock_event_v1_route(
let pdu_id = services
.rooms
.event_handler
.handle_incoming_pdu(&origin, &body.room_id, &event_id, value.clone(), true)
.handle_incoming_pdu(sender.server_name(), &body.room_id, &event_id, value.clone(), true)
.boxed()
.await?
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;

View File

@@ -435,7 +435,9 @@ pub struct Config {
/// this to be high to account for extremely large room joins, slow
/// homeservers, your own resources etc.
///
/// default: 300
/// Joins have 6x the timeout.
///
/// default: 60
#[serde(default = "default_federation_timeout")]
pub federation_timeout: u64,
@@ -501,7 +503,19 @@ pub struct Config {
#[serde(default = "default_appservice_idle_timeout")]
pub appservice_idle_timeout: u64,
/// Notification gateway pusher idle connection pool timeout.
/// Notification gateway pusher request connection timeout (seconds).
///
/// default: 15
#[serde(default = "default_pusher_conn_timeout")]
pub pusher_conn_timeout: u64,
/// Notification gateway pusher total request timeout (seconds).
///
/// default: 60
#[serde(default = "default_pusher_timeout")]
pub pusher_timeout: u64,
/// Notification gateway pusher idle connection pool timeout (seconds).
///
/// default: 15
#[serde(default = "default_pusher_idle_timeout")]
@@ -1663,6 +1677,13 @@ pub struct Config {
#[serde(default = "default_url_preview_max_spider_size")]
pub url_preview_max_spider_size: usize,
/// Total request timeout for URL previews (seconds). This includes
/// connection, request, and response body reading time.
///
/// default: 120
#[serde(default = "default_url_preview_timeout")]
pub url_preview_timeout: u64,
/// Option to decide whether you would like to run the domain allowlist
/// checks (contains and explicit) on the root domain or not. Does not apply
/// to URL contains allowlist. Defaults to false.
@@ -2455,7 +2476,7 @@ fn default_well_known_timeout() -> u64 { 10 }
fn default_federation_conn_timeout() -> u64 { 10 }
fn default_federation_timeout() -> u64 { 25 }
fn default_federation_timeout() -> u64 { 60 }
fn default_policy_server_request_timeout() -> u64 { 10 }
@@ -2473,6 +2494,10 @@ fn default_appservice_timeout() -> u64 { 35 }
fn default_appservice_idle_timeout() -> u64 { 300 }
fn default_pusher_conn_timeout() -> u64 { 15 }
fn default_pusher_timeout() -> u64 { 60 }
fn default_pusher_idle_timeout() -> u64 { 15 }
fn default_max_fetch_prev_events() -> u16 { 192_u16 }
@@ -2600,6 +2625,8 @@ fn default_url_preview_max_spider_size() -> usize {
256_000 // 256KB
}
fn default_url_preview_timeout() -> u64 { 120 }
fn default_new_user_displayname_suffix() -> String { "🏳️‍⚧️".to_owned() }
fn default_sentry_endpoint() -> Option<Url> { None }

View File

@@ -47,6 +47,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
})?
.local_address(url_preview_bind_addr)
.dns_resolver(resolver.resolver.clone())
.timeout(Duration::from_secs(config.url_preview_timeout))
.redirect(redirect::Policy::limited(3))
.build()?,
@@ -68,6 +69,11 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
.dns_resolver(resolver.resolver.hooked.clone())
.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
.read_timeout(Duration::from_secs(config.federation_timeout))
.timeout(Duration::from_secs(
config
.federation_timeout
.saturating_add(config.federation_conn_timeout),
))
.pool_max_idle_per_host(config.federation_idle_per_host.into())
.pool_idle_timeout(Duration::from_secs(config.federation_idle_timeout))
.redirect(redirect::Policy::limited(3))
@@ -76,7 +82,13 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
synapse: base(config)?
.dns_resolver(resolver.resolver.hooked.clone())
.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
.read_timeout(Duration::from_secs(305))
.read_timeout(Duration::from_secs(config.federation_timeout.saturating_mul(6)))
.timeout(Duration::from_secs(
config
.federation_timeout
.saturating_mul(6)
.saturating_add(config.federation_conn_timeout),
))
.pool_max_idle_per_host(0)
.redirect(redirect::Policy::limited(3))
.build()?,
@@ -103,6 +115,8 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
pusher: base(config)?
.dns_resolver(resolver.resolver.clone())
.connect_timeout(Duration::from_secs(config.pusher_conn_timeout))
.timeout(Duration::from_secs(config.pusher_timeout))
.pool_max_idle_per_host(1)
.pool_idle_timeout(Duration::from_secs(config.pusher_idle_timeout))
.redirect(redirect::Policy::limited(2))

View File

@@ -3,7 +3,7 @@
use bytes::Bytes;
use conduwuit::{
Err, Error, Result, debug, debug::INFO_SPAN_LEVEL, debug_error, debug_warn, err,
error::inspect_debug_log, implement, trace, utils::string::EMPTY,
error::inspect_debug_log, implement, trace,
};
use http::{HeaderValue, header::AUTHORIZATION};
use ipaddress::IPAddress;
@@ -90,7 +90,9 @@ async fn perform<T>(
debug!(%method, %url, "Sending request");
match client.execute(request).await {
| Ok(response) => handle_response::<T>(dest, actual, &method, &url, response).await,
| Ok(response) =>
self.handle_response::<T>(dest, actual, &method, &url, response)
.await,
| Err(error) =>
Err(handle_error(actual, &method, &url, error).expect_err("always returns error")),
}
@@ -119,7 +121,9 @@ fn validate_url(&self, url: &Url) -> Result<()> {
Ok(())
}
#[implement(super::Service)]
async fn handle_response<T>(
&self,
dest: &ServerName,
actual: &ActualDest,
method: &Method,
@@ -162,7 +166,6 @@ async fn into_http_response(
.expect("http::response::Builder is usable"),
);
// TODO: handle timeout
trace!("Waiting for response body...");
let body = response
.bytes()
@@ -286,10 +289,13 @@ fn into_http_request<T>(actual: &ActualDest, request: T) -> Result<http::Request
T: OutgoingRequest + Send,
{
const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_11];
const SATIR: SendAccessToken<'_> = SendAccessToken::IfRequired(EMPTY);
let http_request = request
.try_into_http_request::<Vec<u8>>(actual.string().as_str(), SATIR, &VERSIONS)
.try_into_http_request::<Vec<u8>>(
actual.string().as_str(),
SendAccessToken::None,
&VERSIONS,
)
.map_err(|e| err!(BadServerResponse("Invalid destination: {e:?}")))?;
Ok(http_request)

View File

@@ -198,7 +198,7 @@ pub async fn send_request<T>(&self, dest: &str, request: T) -> Result<T::Incomin
trace!("Push gateway destination: {dest}");
let http_request = request
.try_into_http_request::<BytesMut>(&dest, SendAccessToken::IfRequired(""), &VERSIONS)
.try_into_http_request::<BytesMut>(&dest, SendAccessToken::None, &VERSIONS)
.map_err(|e| {
err!(BadServerResponse(warn!(
"Failed to find destination {dest} for push gateway: {e}"
@@ -245,7 +245,7 @@ pub async fn send_request<T>(&self, dest: &str, request: T) -> Result<T::Incomin
.expect("http::response::Builder is usable"),
);
let body = response.bytes().await?; // TODO: handle timeout
let body = response.bytes().await?;
if !status.is_success() {
debug_warn!("Push gateway response body: {:?}", string_from_bytes(&body));
@@ -288,7 +288,7 @@ pub async fn send_push_notice<E>(
let mut notify = None;
let mut tweaks = Vec::new();
if event.room_id().is_none() {
// TODO(hydra): does this matter?
// This only affects v12+ create events
return Ok(());
}

View File

@@ -1,8 +1,7 @@
use std::{fmt::Debug, mem};
use bytes::BytesMut;
use conduwuit::{Err, Result, debug_error, err, trace, utils, warn};
use reqwest::Client;
use conduwuit::{Err, Result, debug_error, err, implement, trace, utils, warn};
use ruma::api::{
IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken, appservice::Registration,
};
@@ -11,8 +10,9 @@
///
/// Only returns Ok(None) if there is no url specified in the appservice
/// registration file
pub(crate) async fn send_request<T>(
client: &Client,
#[implement(super::Service)]
pub async fn send_appservice_request<T>(
&self,
registration: Registration,
request: T,
) -> Result<Option<T::IncomingResponse>>
@@ -35,7 +35,7 @@ pub(crate) async fn send_request<T>(
let mut http_request = request
.try_into_http_request::<BytesMut>(
&dest,
SendAccessToken::IfRequired(hs_token),
SendAccessToken::Appservice(hs_token),
&VERSIONS,
)
.map_err(|e| {
@@ -58,6 +58,8 @@ pub(crate) async fn send_request<T>(
let reqwest_request = reqwest::Request::try_from(http_request)?;
let client = &self.services.client.appservice;
let mut response = client.execute(reqwest_request).await.map_err(|e| {
warn!("Could not send request to appservice \"{}\" at {dest}: {e:?}", registration.id);
e
@@ -75,7 +77,7 @@ pub(crate) async fn send_request<T>(
.expect("http::response::Builder is usable"),
);
let body = response.bytes().await?; // TODO: handle timeout
let body = response.bytes().await?;
if !status.is_success() {
debug_error!("Appservice response bytes: {:?}", utils::string_from_bytes(&body));

View File

@@ -19,10 +19,7 @@
warn,
};
use futures::{FutureExt, Stream, StreamExt};
use ruma::{
RoomId, ServerName, UserId,
api::{OutgoingRequest, appservice::Registration},
};
use ruma::{RoomId, ServerName, UserId, api::OutgoingRequest};
use tokio::{task, task::JoinSet};
use self::data::Data;
@@ -319,22 +316,6 @@ pub async fn send_synapse_request<T>(
.await
}
/// Sends a request to an appservice
///
/// Only returns None if there is no url specified in the appservice
/// registration file
pub async fn send_appservice_request<T>(
&self,
registration: Registration,
request: T,
) -> Result<Option<T::IncomingResponse>>
where
T: OutgoingRequest + Debug + Send,
{
let client = &self.services.client.appservice;
appservice::send_request(client, registration, request).await
}
/// Clean up queued sending event data
///
/// Used after we remove an appservice registration or a user deletes a push

View File

@@ -50,9 +50,7 @@
};
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
use super::{
Destination, EduBuf, EduVec, Msg, SendingEvent, Service, appservice, data::QueueItem,
};
use super::{Destination, EduBuf, EduVec, Msg, SendingEvent, Service, data::QueueItem};
#[derive(Debug)]
enum TransactionStatus {
@@ -720,18 +718,18 @@ async fn send_events_dest_appservice(
//debug_assert!(pdu_jsons.len() + edu_jsons.len() > 0, "sending empty
// transaction");
let client = &self.services.client.appservice;
match appservice::send_request(
client,
appservice,
ruma::api::appservice::event::push_events::v1::Request {
events: pdu_jsons,
txn_id: txn_id.into(),
ephemeral: edu_jsons,
to_device: Vec::new(), // TODO
},
)
.await
match self
.send_appservice_request(
appservice,
ruma::api::appservice::event::push_events::v1::Request {
events: pdu_jsons,
txn_id: txn_id.into(),
ephemeral: edu_jsons,
to_device: Vec::new(), // TODO
},
)
.await
{
| Ok(_) => Ok(Destination::Appservice(id)),
| Err(e) => Err((Destination::Appservice(id), e)),