Skip to content

Commit 4c7c08d

Browse files
authored
ref(server): Use multer directly (#3978)
Switches to direct use of `multer` instead of `axum`'s wrapper. This allows us to use constraints to configure the maximum body size and maximum field size directly on the `Multipart` instance instead of handling this manually. In order to keep using the multipart type as an extractor, we define `Remote` which is a shallow transparent wrapper around any remote type we would like to implement `FromRequest`, `FromRequestParts`, or `IntoResponse` for. This way, we can use the `Multipart` and `multer::Error` types directly in our signatures. Usage of this type is documented on the type.
1 parent 6a41447 commit 4c7c08d

File tree

10 files changed

+149
-100
lines changed

10 files changed

+149
-100
lines changed

Cargo.lock

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

relay-server/Cargo.toml

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,7 @@ workspace = true
3232
[dependencies]
3333
anyhow = { workspace = true }
3434
serde_path_to_error = { workspace = true }
35-
axum = { workspace = true, features = [
36-
"macros",
37-
"matched-path",
38-
"multipart",
39-
"tracing",
40-
] }
35+
axum = { workspace = true, features = ["macros", "matched-path", "tracing"] }
4136
axum-extra = { workspace = true }
4237
axum-server = { workspace = true }
4338
arc-swap = { workspace = true }
Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,43 @@
1-
use axum::extract::{DefaultBodyLimit, Multipart, Path};
1+
use axum::extract::Path;
22
use axum::http::StatusCode;
33
use axum::response::IntoResponse;
4-
use axum::routing::{post, MethodRouter};
5-
use relay_config::Config;
4+
use multer::Multipart;
65
use relay_event_schema::protocol::EventId;
76
use serde::Deserialize;
87

98
use crate::endpoints::common::{self, BadStoreRequest};
109
use crate::envelope::{AttachmentType, Envelope};
11-
use crate::extractors::RequestMeta;
10+
use crate::extractors::{Remote, RequestMeta};
1211
use crate::service::ServiceState;
1312
use crate::utils;
1413

1514
#[derive(Debug, Deserialize)]
16-
struct AttachmentPath {
15+
pub struct AttachmentPath {
1716
event_id: EventId,
1817
}
1918

2019
async fn extract_envelope(
21-
config: &Config,
2220
meta: RequestMeta,
2321
path: AttachmentPath,
24-
multipart: Multipart,
22+
multipart: Multipart<'static>,
2523
) -> Result<Box<Envelope>, BadStoreRequest> {
26-
let max_size = config.max_attachment_size();
27-
let items = utils::multipart_items(multipart, max_size, |_| AttachmentType::default()).await?;
24+
let items = utils::multipart_items(multipart, |_| AttachmentType::default()).await?;
2825

2926
let mut envelope = Envelope::from_request(Some(path.event_id), meta);
3027
for item in items {
3128
envelope.add_item(item);
3229
}
30+
3331
Ok(envelope)
3432
}
3533

36-
async fn handle(
34+
pub async fn handle(
3735
state: ServiceState,
3836
meta: RequestMeta,
3937
Path(path): Path<AttachmentPath>,
40-
multipart: Multipart,
38+
Remote(multipart): Remote<Multipart<'static>>,
4139
) -> Result<impl IntoResponse, BadStoreRequest> {
42-
let envelope = extract_envelope(state.config(), meta, path, multipart).await?;
40+
let envelope = extract_envelope(meta, path, multipart).await?;
4341
common::handle_envelope(&state, envelope).await?;
4442
Ok(StatusCode::CREATED)
4543
}
46-
47-
pub fn route(config: &Config) -> MethodRouter<ServiceState> {
48-
post(handle).route_layer(DefaultBodyLimit::max(config.max_attachments_size()))
49-
}

relay-server/src/endpoints/common.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::services::outcome::{DiscardReason, Outcome};
1515
use crate::services::processor::{MetricData, ProcessMetricMeta, ProcessingGroup};
1616
use crate::services::project_cache::{CheckEnvelope, ProcessMetrics, ValidateEnvelope};
1717
use crate::statsd::{RelayCounters, RelayHistograms};
18-
use crate::utils::{self, ApiErrorResponse, FormDataIter, ManagedEnvelope, MultipartError};
18+
use crate::utils::{self, ApiErrorResponse, FormDataIter, ManagedEnvelope};
1919

2020
#[derive(Clone, Copy, Debug, thiserror::Error)]
2121
#[error("the service is overloaded")]
@@ -61,9 +61,6 @@ pub enum BadStoreRequest {
6161
#[error("invalid multipart data")]
6262
InvalidMultipart(#[from] multer::Error),
6363

64-
#[error("invalid multipart data")]
65-
InvalidMultipartAxum(#[from] MultipartError),
66-
6764
#[error("invalid minidump")]
6865
InvalidMinidump,
6966

relay-server/src/endpoints/minidump.rs

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
use std::convert::Infallible;
22

3-
use axum::extract::{DefaultBodyLimit, Multipart, Request};
3+
use axum::extract::{DefaultBodyLimit, Request};
44
use axum::response::IntoResponse;
55
use axum::routing::{post, MethodRouter};
66
use axum::RequestExt;
77
use bytes::Bytes;
8-
use futures::{future, FutureExt};
8+
use multer::Multipart;
99
use relay_config::Config;
1010
use relay_event_schema::protocol::EventId;
1111

1212
use crate::constants::{ITEM_NAME_BREADCRUMBS1, ITEM_NAME_BREADCRUMBS2, ITEM_NAME_EVENT};
1313
use crate::endpoints::common::{self, BadStoreRequest, TextResponse};
1414
use crate::envelope::{AttachmentType, ContentType, Envelope, Item, ItemType};
15-
use crate::extractors::{RawContentType, RequestMeta};
15+
use crate::extractors::{RawContentType, Remote, RequestMeta};
1616
use crate::service::ServiceState;
1717
use crate::utils;
1818

@@ -69,8 +69,8 @@ async fn extract_embedded_minidump(payload: Bytes) -> Result<Option<Bytes>, BadS
6969
None => return Ok(None),
7070
};
7171

72-
let stream = future::ok::<_, Infallible>(payload.clone()).into_stream();
73-
let mut multipart = multer::Multipart::new(stream, boundary);
72+
let stream = futures::stream::once(async { Ok::<_, Infallible>(payload.clone()) });
73+
let mut multipart = Multipart::new(stream, boundary);
7474

7575
while let Some(field) = multipart.next_field().await? {
7676
if field.name() == Some(MINIDUMP_FIELD_NAME) {
@@ -82,12 +82,10 @@ async fn extract_embedded_minidump(payload: Bytes) -> Result<Option<Bytes>, BadS
8282
}
8383

8484
async fn extract_multipart(
85-
config: &Config,
86-
multipart: Multipart,
85+
multipart: Multipart<'static>,
8786
meta: RequestMeta,
8887
) -> Result<Box<Envelope>, BadStoreRequest> {
89-
let max_size = config.max_attachment_size();
90-
let mut items = utils::multipart_items(multipart, max_size, infer_attachment_type).await?;
88+
let mut items = utils::multipart_items(multipart, infer_attachment_type).await?;
9189

9290
let minidump_item = items
9391
.iter_mut()
@@ -139,7 +137,8 @@ async fn handle(
139137
let envelope = if MINIDUMP_RAW_CONTENT_TYPES.contains(&content_type.as_ref()) {
140138
extract_raw_minidump(request.extract().await?, meta)?
141139
} else {
142-
extract_multipart(state.config(), request.extract().await?, meta).await?
140+
let Remote(multipart) = request.extract_with_state(&state).await?;
141+
extract_multipart(multipart, meta).await?
143142
};
144143

145144
let id = envelope.event_id();
@@ -156,15 +155,15 @@ async fn handle(
156155
}
157156

158157
pub fn route(config: &Config) -> MethodRouter<ServiceState> {
159-
post(handle).route_layer(DefaultBodyLimit::max(config.max_attachments_size()))
158+
// Set the single-attachment limit that applies only for raw minidumps. Multipart bypasses the
159+
// limited body and applies its own limits.
160+
post(handle).route_layer(DefaultBodyLimit::max(config.max_attachment_size()))
160161
}
161162

162163
#[cfg(test)]
163164
mod tests {
164165
use axum::body::Body;
165-
use axum::extract::FromRequest;
166-
167-
use relay_config::ByteSize;
166+
use relay_config::Config;
168167

169168
use crate::utils::{multipart_items, FormDataIter};
170169

@@ -214,14 +213,10 @@ mod tests {
214213
.body(Body::from(multipart_body))
215214
.unwrap();
216215

217-
let multipart = Multipart::from_request(request, &()).await?;
216+
let config = Config::default();
218217

219-
let items = multipart_items(
220-
multipart,
221-
ByteSize::mebibytes(100).as_bytes(),
222-
infer_attachment_type,
223-
)
224-
.await?;
218+
let multipart = utils::multipart_from_request(request, &config)?;
219+
let items = multipart_items(multipart, infer_attachment_type).await?;
225220

226221
// we expect the multipart body to contain
227222
// * one arbitrary attachment from the user (a `config.json`)

relay-server/src/endpoints/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ pub fn routes(config: &Config) -> Router<ServiceState>{
7272
// No mandatory trailing slash here because people already use it like this.
7373
.route("/api/:project_id/minidump", minidump::route(config))
7474
.route("/api/:project_id/minidump/", minidump::route(config))
75-
.route("/api/:project_id/events/:event_id/attachments/", attachments::route(config))
75+
.route("/api/:project_id/events/:event_id/attachments/", post(attachments::handle))
7676
.route("/api/:project_id/unreal/:sentry_key/", unreal::route(config))
7777
// NOTE: If you add a new (non-experimental) route here, please also list it in
7878
// https://github.com/getsentry/sentry-docs/blob/master/docs/product/relay/operating-guidelines.mdx

relay-server/src/extractors/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
mod content_type;
22
mod forwarded_for;
33
mod mime;
4+
mod remote;
45
mod request_meta;
56
mod signed_json;
67
mod start_time;
78

89
pub use self::content_type::*;
910
pub use self::forwarded_for::*;
1011
pub use self::mime::*;
12+
pub use self::remote::*;
1113
pub use self::request_meta::*;
1214
pub use self::signed_json::*;
1315
pub use self::start_time::*;

relay-server/src/extractors/remote.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
//! Extractors for types from other crates via [`Remote`].
2+
3+
use axum::extract::{FromRequest, Request};
4+
use axum::http::StatusCode;
5+
use axum::response::{IntoResponse, Response};
6+
use multer::Multipart;
7+
8+
use crate::service::ServiceState;
9+
use crate::utils::{self, ApiErrorResponse};
10+
11+
/// A transparent wrapper around a remote type that implements [`FromRequest`] or [`IntoResponse`].
12+
///
13+
/// # Example
14+
///
15+
/// ```ignore
16+
/// use std::convert::Infallible;
17+
///
18+
/// use axum::extract::{FromRequest, Request};
19+
/// use axum::response::IntoResponse;
20+
///
21+
/// use crate::extractors::Remote;
22+
///
23+
/// // Derive `FromRequest` for `bool` for illustration purposes:
24+
/// #[axum::async_trait]
25+
/// impl<S> axum::extract::FromRequest<S> for Remote<bool> {
26+
/// type Rejection = Remote<Infallible>;
27+
///
28+
/// async fn from_request(request: Request) -> Result<Self, Self::Rejection> {
29+
/// Ok(Remote(true))
30+
/// }
31+
/// }
32+
///
33+
/// impl IntoResponse for Remote<Infallible> {
34+
/// fn into_response(self) -> axum::response::Response {
35+
/// match self.0 {}
36+
/// }
37+
/// }
38+
/// ```
39+
#[derive(Debug)]
40+
pub struct Remote<T>(pub T);
41+
42+
impl<T> From<T> for Remote<T> {
43+
fn from(inner: T) -> Self {
44+
Self(inner)
45+
}
46+
}
47+
48+
#[axum::async_trait]
49+
impl FromRequest<ServiceState> for Remote<Multipart<'static>> {
50+
type Rejection = Remote<multer::Error>;
51+
52+
async fn from_request(request: Request, state: &ServiceState) -> Result<Self, Self::Rejection> {
53+
utils::multipart_from_request(request, state.config())
54+
.map(Remote)
55+
.map_err(Remote)
56+
}
57+
}
58+
59+
impl IntoResponse for Remote<multer::Error> {
60+
fn into_response(self) -> Response {
61+
let Self(ref error) = self;
62+
63+
let status_code = match error {
64+
multer::Error::FieldSizeExceeded { .. } => StatusCode::PAYLOAD_TOO_LARGE,
65+
multer::Error::StreamSizeExceeded { .. } => StatusCode::PAYLOAD_TOO_LARGE,
66+
_ => StatusCode::BAD_REQUEST,
67+
};
68+
69+
(status_code, ApiErrorResponse::from_error(error)).into_response()
70+
}
71+
}

0 commit comments

Comments
 (0)