arminius_lib_component/
ruma_requests.rs1#![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#[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#[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
130pub 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
149pub 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#[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#[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}