Skip to main content

arminius_lib_component/
ruma_requests.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Utilities when using ruma. Either they convert between ruma and axum,
6//! or they convert between the different representations of the same data
7//! that ruma has.
8
9#![allow(
10	clippy::allow_attributes,
11	reason = "This source file contains enums which allow `clippy::missing_docs_in_private_items`. But an exception to this lint will mistakenly report that it's unfulfilled when used with expect, even though it is not."
12)]
13
14mod tests;
15
16use std::io::Read as _;
17
18use axum::{
19	body::{Body, to_bytes},
20	extract::{FromRequestParts as _, Path},
21	http::StatusCode,
22	response::IntoResponse,
23};
24use ruma::api::error::ErrorKind;
25
26use crate::error::{InputJsonError, InternalServerError, MatrixApiError, ResultExt as _};
27
28/// The user sent us an invalid request path, which we could not parse
29#[derive(thiserror::Error, Debug, MatrixApiError)]
30#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::InvalidParam, component_crate_root = crate)]
31#[error("Invalid request path {0}")]
32pub struct InvalidRequestPath(pub String);
33
34#[expect(
35	missing_docs,
36	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
37)]
38#[derive(thiserror::Error, Debug, MatrixApiError)]
39#[matrix_api_error(status_code = StatusCode::PAYLOAD_TOO_LARGE, error_kind = ErrorKind::TooLarge, component_crate_root = crate)]
40#[error("Request too large")]
41pub struct RequestTooLarge;
42
43/// The request was invalid for any other reason.
44#[derive(thiserror::Error, Debug, MatrixApiError)]
45#[matrix_api_error(status_code = StatusCode::BAD_REQUEST, error_kind = ErrorKind::Unknown, component_crate_root = crate)]
46#[error("Unknown error: {0}")]
47pub struct OtherDeserializationError(pub String);
48
49#[expect(
50	missing_docs,
51	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
52)]
53#[derive(thiserror::Error, Debug, MatrixApiError)]
54#[matrix_api_error(component_crate_root = crate)]
55pub enum InputDeserializationError {
56	#[error(transparent)]
57	InputJsonError(InputJsonError),
58	#[error(transparent)]
59	InvalidRequestPath(InvalidRequestPath),
60	#[error(transparent)]
61	OtherError(OtherDeserializationError),
62}
63
64impl From<ruma::api::error::DeserializationError> for InputDeserializationError {
65	fn from(error: ruma::api::error::DeserializationError) -> Self {
66		match error {
67			ruma::api::error::DeserializationError::Json(error) => {
68				InputDeserializationError::InputJsonError(error.into())
69			}
70			ruma::api::error::DeserializationError::Query(error) => {
71				InputDeserializationError::InvalidRequestPath(InvalidRequestPath(error.to_string()))
72			}
73			ruma::api::error::DeserializationError::Ident(error) => {
74				InputDeserializationError::InvalidRequestPath(InvalidRequestPath(error.to_string()))
75			}
76			other_error => InputDeserializationError::OtherError(OtherDeserializationError(
77				other_error.to_string(),
78			)),
79		}
80	}
81}
82
83#[expect(
84	missing_docs,
85	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
86)]
87#[derive(thiserror::Error, Debug, MatrixApiError)]
88#[matrix_api_error(component_crate_root = crate)]
89pub enum RumaFromRequestError {
90	#[error(transparent)]
91	DeserializationError(InputDeserializationError),
92	#[error(transparent)]
93	InternalServerError(InternalServerError),
94}
95
96impl From<ruma::api::error::FromHttpRequestError> for RumaFromRequestError {
97	fn from(error: ruma::api::error::FromHttpRequestError) -> Self {
98		match error {
99			ruma::api::error::FromHttpRequestError::Deserialization(deserialization_error) => {
100				RumaFromRequestError::DeserializationError(InputDeserializationError::from(
101					deserialization_error,
102				))
103			}
104			unknown_error => {
105				tracing::error!("Unknown ruma `FromHttpRequestError`: {unknown_error}",);
106				RumaFromRequestError::InternalServerError(InternalServerError)
107			}
108		}
109	}
110}
111
112#[allow(
113	missing_docs,
114	clippy::missing_docs_in_private_items,
115	reason = "TODO(#2003): This exception predates us enabling `clippy::allow_attributes_without_reason`, so we don't know why it was enabled"
116)]
117#[derive(MatrixApiError, thiserror::Error, Debug)]
118#[matrix_api_error(component_crate_root = crate)]
119pub enum ParseRequestError {
120	#[error(transparent)]
121	RumaFromRequestError(#[from] RumaFromRequestError),
122	#[error(transparent)]
123	RequestTooLarge(#[from] RequestTooLarge),
124	#[error(transparent)]
125	InvalidRequestPath(#[from] InvalidRequestPath),
126	#[error(transparent)]
127	InternalServerError(#[from] InternalServerError),
128}
129
130/// Take an `axum` request, and turn it into a `ruma` request
131pub async fn extract_ruma_request<T: ruma::api::IncomingRequestExt>(
132	request: axum::extract::Request<Vec<u8>>,
133) -> Result<T, ParseRequestError> {
134	let (parts, body) = request.into_parts();
135	let uri = parts.uri.to_string();
136
137	let path =
138		Path::<Vec<String>>::from_request_parts(&mut parts.clone(), &()).await.map_err(|err| {
139			tracing::warn!(uri = uri, error = err.to_string(), "User requested invalid path");
140			InvalidRequestPath(uri)
141		})?;
142
143	let request = axum::extract::Request::from_parts(parts, &body[..]);
144	let path: Vec<&str> = path.iter().map(String::as_str).collect();
145
146	Ok(T::try_from_http_request(request, path.as_slice()).map_err(RumaFromRequestError::from)?)
147}
148
149/// Takes a [axum::http::Request] object with a [Body]
150/// parameter, and converts it into one defined over a vector, which `ruma`
151/// likes better.
152pub async fn to_vector_request(
153	request: axum::http::Request<Body>,
154) -> Result<axum::http::Request<Vec<u8>>, ParseRequestError> {
155	let (parts, body) = request.into_parts();
156	let body = to_bytes(body, 1024 * 1024 * 1024)
157		.await
158		.map_err(|_| RequestTooLarge)?
159		.bytes()
160		.collect::<Result<_, _>>()
161		.unrecoverable_error("Could not read serialized request")?;
162	Ok(ruma::exports::http::Request::from_parts(parts, body))
163}
164
165/// Custom response type, which wraps a ruma [ruma::api::OutgoingResponse], and
166/// provides [IntoResponse]
167#[derive(Debug)]
168pub struct ArminiusResponse<T: ruma::api::OutgoingResponse>(pub T);
169
170impl<T: ruma::api::OutgoingResponse> From<T> for ArminiusResponse<T> {
171	fn from(response: T) -> Self {
172		Self(response)
173	}
174}
175
176impl<T: ruma::api::OutgoingResponseExt> IntoResponse for ArminiusResponse<T> {
177	fn into_response(self) -> axum::response::Response {
178		let response = self.0;
179		match response.try_into_http_response::<Vec<u8>>() {
180			Ok(http_response) => {
181				let (parts, body) = http_response.into_parts();
182				axum::http::Response::from_parts(parts, Body::from(body))
183			}
184			Err(_) => {
185				tracing::error!("Error making axum response");
186				StatusCode::INTERNAL_SERVER_ERROR.into_response()
187			}
188		}
189	}
190}
191
192/// axum extractor that uses the path parameters and body to build a ruma
193/// request
194#[derive(Debug)]
195pub struct RumaRequest<T: ruma::api::IncomingRequest>(pub T);
196
197impl<T: ruma::api::IncomingRequest, S> axum::extract::FromRequest<S> for RumaRequest<T>
198where
199	S: Send + Sync,
200{
201	type Rejection = ParseRequestError;
202	async fn from_request(
203		request: axum::extract::Request,
204		_: &S,
205	) -> Result<Self, ParseRequestError> {
206		Ok(RumaRequest(extract_ruma_request(to_vector_request(request).await?).await?))
207	}
208}