Skip to content

Commit

Permalink
feat(WidgetDriver): Pass Matrix API errors to the widget (#4241)
Browse files Browse the repository at this point in the history
Currently the WidgetDriver just returns unspecified error strings to the
widget that can be used to display an issue description to the user. It
is not helpful to run code like a retry or other error mitigation logic.

Here it is proposed to add standardized errors for issues that every
widget driver implementation can run into (all matrix cs api errors):
matrix-org/matrix-spec-proposals#2762 (comment)

This PR forwards the errors that occur during the widget processing to
the widget in the correct format.

NOTE:
It does not include request Url and http Headers. See also:
matrix-org/matrix-spec-proposals#2762 (comment)

Co-authored-by: Benjamin Bouvier <[email protected]>
  • Loading branch information
toger5 and bnjbvr authored Dec 4, 2024
1 parent a6e1f05 commit 111f916
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 124 deletions.
130 changes: 130 additions & 0 deletions crates/matrix-sdk/src/test_utils/mocks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ use ruma::{
directory::PublicRoomsChunk,
events::{AnyStateEvent, AnyTimelineEvent, MessageLikeEventType, StateEventType},
serde::Raw,
time::Duration,
MxcUri, OwnedEventId, OwnedRoomId, RoomId, ServerName,
};
use serde::Deserialize;
Expand Down Expand Up @@ -952,6 +953,72 @@ impl<'a> MockEndpoint<'a, RoomSendEndpoint> {
}
}

/// Ensures the event was sent as a delayed event.
///
/// Note: works with *any* room.
///
/// # Examples
///
/// see also [`MatrixMockServer::mock_room_send`] for more context.
///
/// ```
/// # tokio_test::block_on(async {
/// use matrix_sdk::{
/// ruma::{
/// api::client::delayed_events::{delayed_message_event, DelayParameters},
/// events::{message::MessageEventContent, AnyMessageLikeEventContent},
/// room_id,
/// time::Duration,
/// TransactionId,
/// },
/// test_utils::mocks::MatrixMockServer,
/// };
/// use serde_json::json;
/// use wiremock::ResponseTemplate;
///
/// let mock_server = MatrixMockServer::new().await;
/// let client = mock_server.client_builder().build().await;
///
/// mock_server.mock_room_state_encryption().plain().mount().await;
///
/// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await;
///
/// mock_server
/// .mock_room_send()
/// .with_delay(Duration::from_millis(500))
/// .respond_with(ResponseTemplate::new(200).set_body_json(json!({"delay_id":"$some_id"})))
/// .mock_once()
/// .mount()
/// .await;
///
/// let response_not_mocked =
/// room.send_raw("m.room.message", json!({ "body": "Hello world" })).await;
///
/// // A non delayed event should not be mocked by the server.
/// assert!(response_not_mocked.is_err());
///
/// let r = delayed_message_event::unstable::Request::new(
/// room.room_id().to_owned(),
/// TransactionId::new(),
/// DelayParameters::Timeout { timeout: Duration::from_millis(500) },
/// &AnyMessageLikeEventContent::Message(MessageEventContent::plain("hello world")),
/// )
/// .unwrap();
///
/// let response = room.client().send(r, None).await.unwrap();
/// // The delayed `m.room.message` event type should be mocked by the server.
/// assert_eq!("$some_id", response.delay_id);
/// # anyhow::Ok(()) });
/// ```
pub fn with_delay(self, delay: Duration) -> Self {
Self {
mock: self
.mock
.and(query_param("org.matrix.msc4140.delay", delay.as_millis().to_string())),
..self
}
}

/// Returns a send endpoint that emulates success, i.e. the event has been
/// sent with the given event id.
///
Expand Down Expand Up @@ -1117,6 +1184,69 @@ impl<'a> MockEndpoint<'a, RoomSendStateEndpoint> {
Self { mock: self.mock.and(path_regex(Self::generate_path_regexp(&self.endpoint))), ..self }
}

/// Ensures the event was sent as a delayed event.
///
/// Note: works with *any* room.
///
/// # Examples
///
/// see also [`MatrixMockServer::mock_room_send`] for more context.
///
/// ```
/// # tokio_test::block_on(async {
/// use matrix_sdk::{
/// ruma::{
/// api::client::delayed_events::{delayed_state_event, DelayParameters},
/// events::{room::create::RoomCreateEventContent, AnyStateEventContent},
/// room_id,
/// time::Duration,
/// },
/// test_utils::mocks::MatrixMockServer,
/// };
/// use wiremock::ResponseTemplate;
/// use serde_json::json;
///
/// let mock_server = MatrixMockServer::new().await;
/// let client = mock_server.client_builder().build().await;
///
/// mock_server.mock_room_state_encryption().plain().mount().await;
///
/// let room = mock_server.sync_joined_room(&client, room_id!("!room_id:localhost")).await;
///
/// mock_server
/// .mock_room_send_state()
/// .with_delay(Duration::from_millis(500))
/// .respond_with(ResponseTemplate::new(200).set_body_json(json!({"delay_id":"$some_id"})))
/// .mock_once()
/// .mount()
/// .await;
///
/// let response_not_mocked = room.send_state_event(RoomCreateEventContent::new_v11()).await;
/// // A non delayed event should not be mocked by the server.
/// assert!(response_not_mocked.is_err());
///
/// let r = delayed_state_event::unstable::Request::new(
/// room.room_id().to_owned(),
/// "".to_owned(),
/// DelayParameters::Timeout { timeout: Duration::from_millis(500) },
/// &AnyStateEventContent::RoomCreate(RoomCreateEventContent::new_v11()),
/// )
/// .unwrap();
/// let response = room.client().send(r, None).await.unwrap();
/// // The delayed `m.room.message` event type should be mocked by the server.
/// assert_eq!("$some_id", response.delay_id);
///
/// # anyhow::Ok(()) });
/// ```
pub fn with_delay(self, delay: Duration) -> Self {
Self {
mock: self
.mock
.and(query_param("org.matrix.msc4140.delay", delay.as_millis().to_string())),
..self
}
}

///
/// ```
/// # tokio_test::block_on(async {
Expand Down
4 changes: 3 additions & 1 deletion crates/matrix-sdk/src/widget/machine/driver_req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,11 @@ where
Self { request_meta: None, _phantom: PhantomData }
}

/// Setup a callback function that will be called once the matrix driver has
/// processed the request.
pub(crate) fn then(
self,
response_handler: impl FnOnce(Result<T, String>, &mut WidgetMachine) -> Vec<Action>
response_handler: impl FnOnce(Result<T, crate::Error>, &mut WidgetMachine) -> Vec<Action>
+ Send
+ 'static,
) {
Expand Down
71 changes: 64 additions & 7 deletions crates/matrix-sdk/src/widget/machine/from_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::fmt;

use as_variant::as_variant;
use ruma::{
api::client::delayed_events::{
delayed_message_event, delayed_state_event, update_delayed_event,
api::client::{
delayed_events::{delayed_message_event, delayed_state_event, update_delayed_event},
error::{ErrorBody, StandardErrorBody},
},
events::{AnyTimelineEvent, MessageLikeEventType, StateEventType},
serde::Raw,
Expand All @@ -25,7 +25,7 @@ use ruma::{
use serde::{Deserialize, Serialize};

use super::{SendEventRequest, UpdateDelayedEventRequest};
use crate::widget::StateKeySelector;
use crate::{widget::StateKeySelector, Error, HttpError, RumaApiError};

#[derive(Deserialize, Debug)]
#[serde(tag = "action", rename_all = "snake_case", content = "data")]
Expand All @@ -41,28 +41,85 @@ pub(super) enum FromWidgetRequest {
DelayedEventUpdate(UpdateDelayedEventRequest),
}

/// The full response a client sends to a [`FromWidgetRequest`] in case of an
/// error.
#[derive(Serialize)]
pub(super) struct FromWidgetErrorResponse {
error: FromWidgetError,
}

impl FromWidgetErrorResponse {
pub(super) fn new(e: impl fmt::Display) -> Self {
Self { error: FromWidgetError { message: e.to_string() } }
/// Create a error response to send to the widget from an http error.
pub(crate) fn from_http_error(error: HttpError) -> Self {
let message = error.to_string();
let matrix_api_error = as_variant!(error, HttpError::Api(ruma::api::error::FromHttpResponseError::Server(RumaApiError::ClientApi(err))) => err);

Self {
error: FromWidgetError {
message,
matrix_api_error: matrix_api_error.and_then(|api_error| match api_error.body {
ErrorBody::Standard { kind, message } => Some(FromWidgetMatrixErrorBody {
http_status: api_error.status_code.as_u16().into(),
response: StandardErrorBody { kind, message },
}),
_ => None,
}),
},
}
}

/// Create a error response to send to the widget from a matrix sdk error.
pub(crate) fn from_error(error: Error) -> Self {
match error {
Error::Http(e) => FromWidgetErrorResponse::from_http_error(e),
// For UnknownError's we do not want to have the `unknown error` bit in the message.
// Hence we only convert the inner error to a string.
Error::UnknownError(e) => FromWidgetErrorResponse::from_string(e.to_string()),
_ => FromWidgetErrorResponse::from_string(error.to_string()),
}
}

/// Create a error response to send to the widget from a string.
pub(crate) fn from_string<S: Into<String>>(error: S) -> Self {
Self { error: FromWidgetError { message: error.into(), matrix_api_error: None } }
}
}

/// Serializable section of an error response send by the client as a
/// response to a [`FromWidgetRequest`].
#[derive(Serialize)]
struct FromWidgetError {
/// Unspecified error message text that caused this widget action to
/// fail.
///
/// This is useful to prompt the user on an issue but cannot be used to
/// decide on how to deal with the error.
message: String,

/// Optional matrix error hinting at workarounds for specific errors.
matrix_api_error: Option<FromWidgetMatrixErrorBody>,
}

/// Serializable section of a widget response that represents a matrix error.
#[derive(Serialize)]
struct FromWidgetMatrixErrorBody {
/// Status code of the http response.
http_status: u32,

/// Standard error response including the `errorcode` and the `error`
/// message as defined in the [spec](https://spec.matrix.org/v1.12/client-server-api/#standard-error-response).
response: StandardErrorBody,
}

/// The serializable section of a widget response containing the supported
/// versions.
#[derive(Serialize)]
pub(super) struct SupportedApiVersionsResponse {
supported_versions: Vec<ApiVersion>,
}

impl SupportedApiVersionsResponse {
/// The currently supported widget api versions from the rust widget driver.
pub(super) fn new() -> Self {
Self {
supported_versions: vec![
Expand Down
7 changes: 5 additions & 2 deletions crates/matrix-sdk/src/widget/machine/incoming.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ pub(crate) enum IncomingMessage {
/// The ID of the request that this response corresponds to.
request_id: Uuid,

/// The result of the request: response data or error message.
response: Result<MatrixDriverResponse, String>,
/// Result of the request: the response data, or a matrix sdk error.
///
/// Http errors will be forwarded to the widget in a specified format so
/// the widget can parse the error.
response: Result<MatrixDriverResponse, crate::Error>,
},

/// The `MatrixDriver` notified the `WidgetMachine` of a new matrix event.
Expand Down
Loading

0 comments on commit 111f916

Please sign in to comment.