From 7275d5fa23dd0e2108b4ce9b8b17ec3781a962b0 Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Wed, 25 Jun 2025 09:14:03 +0200 Subject: [PATCH 1/9] Initial solution: Passing a function --- relay-server/src/endpoints/playstation.rs | 39 +++++++++++++++++++++-- relay-server/src/utils/multipart.rs | 34 +++++++++++++++----- 2 files changed, 62 insertions(+), 11 deletions(-) diff --git a/relay-server/src/endpoints/playstation.rs b/relay-server/src/endpoints/playstation.rs index 28c13d6b273..59c328a3414 100644 --- a/relay-server/src/endpoints/playstation.rs +++ b/relay-server/src/endpoints/playstation.rs @@ -5,12 +5,17 @@ use axum::routing::{MethodRouter, post}; use relay_config::Config; use relay_dynamic_config::Feature; use relay_event_schema::protocol::EventId; +use relay_quotas::{DataCategory, Scoping}; +use relay_system::Addr; use crate::endpoints::common::{self, BadStoreRequest, TextResponse}; use crate::envelope::ContentType::OctetStream; use crate::envelope::{AttachmentType, Envelope}; use crate::extractors::{RawContentType, RequestMeta}; use crate::service::ServiceState; +use crate::services::outcome::{ + DiscardAttachmentType, DiscardItemType, DiscardReason, Outcome, TrackOutcome, +}; use crate::utils::UnconstrainedMultipart; /// The extension of a prosperodump in the multipart form-data upload. @@ -39,12 +44,40 @@ fn infer_attachment_type(_field_name: Option<&str>, file_name: &str) -> Attachme } } +fn emit_outcome( + aggregator: &Addr, + meta: &RequestMeta, + scoping: Scoping, + quanity: u32, +) { + aggregator.send(TrackOutcome { + timestamp: meta.received_at(), + scoping, + outcome: Outcome::Invalid(DiscardReason::TooLarge(DiscardItemType::Attachment( + DiscardAttachmentType::Attachment, + ))), + event_id: None, + remote_addr: meta.remote_addr(), + category: DataCategory::Attachment, + quantity: quanity, + }); +} + async fn extract_multipart( multipart: UnconstrainedMultipart, meta: RequestMeta, - config: &Config, + state: &ServiceState, ) -> Result, BadStoreRequest> { - let mut items = multipart.items(infer_attachment_type, config).await?; + let mut items = multipart + .items(infer_attachment_type, state.config(), |quantity| { + emit_outcome( + state.outcome_aggregator(), + &meta, + meta.get_partial_scoping(), + quantity, + ) + }) + .await?; let prosperodump_item = items .iter_mut() @@ -73,7 +106,7 @@ async fn handle( ) -> axum::response::Result { // The crash dumps are transmitted as `...` in a multipart form-data/ request. let multipart = request.extract_with_state(&state).await?; - let mut envelope = extract_multipart(multipart, meta, state.config()).await?; + let mut envelope = extract_multipart(multipart, meta, &state).await?; envelope.require_feature(Feature::PlaystationIngestion); let id = envelope.event_id(); diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index bad9dc31185..eb4206832ee 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -155,14 +155,16 @@ pub fn get_multipart_boundary(data: &[u8]) -> Option<&str> { .and_then(|slice| std::str::from_utf8(&slice[2..]).ok()) } -async fn multipart_items( +async fn multipart_items( mut multipart: Multipart<'_>, mut infer_type: F, config: &Config, + mut emit_outcome: G, ignore_large_fields: bool, ) -> Result where F: FnMut(Option<&str>, &str) -> AttachmentType, + G: FnMut(u32), { let mut items = Items::new(); let mut form_data = FormDataWriter::new(); @@ -177,7 +179,10 @@ where let content_type = field.content_type().cloned(); let field = LimitedField::new(field, config.max_attachment_size()); match field.bytes().await { - Err(multer::Error::FieldSizeExceeded { .. }) if ignore_large_fields => continue, + Err(multer::Error::FieldSizeExceeded { limit, .. }) if ignore_large_fields => { + emit_outcome(limit as u32); + continue; + } Err(err) => return Err(err), Ok(bytes) => { attachments_size += bytes.len(); @@ -276,7 +281,7 @@ impl futures::Stream for LimitedField<'_> { Poll::Ready(None) if self.consumed_size > self.size_limit => { self.inner_finished = true; Poll::Ready(Some(Err(multer::Error::FieldSizeExceeded { - limit: self.size_limit as u64, + limit: self.consumed_size as u64, field_name: self.field.name().map(Into::into), }))) } @@ -313,7 +318,8 @@ impl ConstrainedMultipart { where F: FnMut(Option<&str>, &str) -> AttachmentType, { - multipart_items(self.0, infer_type, config, false).await + // We don't emit an outcome in this code branch since we return an error to the request + multipart_items(self.0, infer_type, config, |_x| {}, false).await } } @@ -338,11 +344,17 @@ impl FromRequest for UnconstrainedMultipart { #[cfg_attr(not(any(test, sentry)), expect(dead_code))] impl UnconstrainedMultipart { - pub async fn items(self, infer_type: F, config: &Config) -> Result + pub async fn items( + self, + infer_type: F, + config: &Config, + emit_outcome: G, + ) -> Result where F: FnMut(Option<&str>, &str) -> AttachmentType, + G: FnMut(u32), { - multipart_items(self.0, infer_type, config, true).await + multipart_items(self.0, infer_type, config, emit_outcome, true).await } } @@ -460,8 +472,13 @@ mod tests { } }))?; + let mut mock_outcomes = vec![]; let items = UnconstrainedMultipart(multipart) - .items(|_, _| AttachmentType::Attachment, &config) + .items( + |_, _| AttachmentType::Attachment, + &config, + |x| (mock_outcomes.push(x)), + ) .await?; // The large field is skipped so only the small one should make it through. @@ -469,6 +486,7 @@ mod tests { let item = &items[0]; assert_eq!(item.filename(), Some("small.txt")); assert_eq!(item.payload(), Bytes::from("ok")); + assert_eq!(mock_outcomes, vec![27]); Ok(()) } @@ -498,7 +516,7 @@ mod tests { let multipart = Multipart::new(stream, "X-BOUNDARY"); let result = UnconstrainedMultipart(multipart) - .items(|_, _| AttachmentType::Attachment, &config) + .items(|_, _| AttachmentType::Attachment, &config, |_| ()) .await; // Should be warned if the overall stream limit is being breached. From 0cb107b200e30c9bb6a3c692b833209690cfa837 Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Wed, 25 Jun 2025 16:03:31 +0200 Subject: [PATCH 2/9] Add necessary info for emitting outcome on `UnconstrainedMultipart` struct. --- relay-server/src/endpoints/playstation.rs | 33 +----- relay-server/src/utils/multipart.rs | 135 +++++++++++++++++----- 2 files changed, 106 insertions(+), 62 deletions(-) diff --git a/relay-server/src/endpoints/playstation.rs b/relay-server/src/endpoints/playstation.rs index 59c328a3414..f7cc7f74a6f 100644 --- a/relay-server/src/endpoints/playstation.rs +++ b/relay-server/src/endpoints/playstation.rs @@ -5,17 +5,12 @@ use axum::routing::{MethodRouter, post}; use relay_config::Config; use relay_dynamic_config::Feature; use relay_event_schema::protocol::EventId; -use relay_quotas::{DataCategory, Scoping}; -use relay_system::Addr; use crate::endpoints::common::{self, BadStoreRequest, TextResponse}; use crate::envelope::ContentType::OctetStream; use crate::envelope::{AttachmentType, Envelope}; use crate::extractors::{RawContentType, RequestMeta}; use crate::service::ServiceState; -use crate::services::outcome::{ - DiscardAttachmentType, DiscardItemType, DiscardReason, Outcome, TrackOutcome, -}; use crate::utils::UnconstrainedMultipart; /// The extension of a prosperodump in the multipart form-data upload. @@ -44,39 +39,13 @@ fn infer_attachment_type(_field_name: Option<&str>, file_name: &str) -> Attachme } } -fn emit_outcome( - aggregator: &Addr, - meta: &RequestMeta, - scoping: Scoping, - quanity: u32, -) { - aggregator.send(TrackOutcome { - timestamp: meta.received_at(), - scoping, - outcome: Outcome::Invalid(DiscardReason::TooLarge(DiscardItemType::Attachment( - DiscardAttachmentType::Attachment, - ))), - event_id: None, - remote_addr: meta.remote_addr(), - category: DataCategory::Attachment, - quantity: quanity, - }); -} - async fn extract_multipart( multipart: UnconstrainedMultipart, meta: RequestMeta, state: &ServiceState, ) -> Result, BadStoreRequest> { let mut items = multipart - .items(infer_attachment_type, state.config(), |quantity| { - emit_outcome( - state.outcome_aggregator(), - &meta, - meta.get_partial_scoping(), - quantity, - ) - }) + .items(infer_attachment_type, state.config()) .await?; let prosperodump_item = items diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index eb4206832ee..aa6fe18c61f 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -1,17 +1,26 @@ +use std::convert::Infallible; use std::io; use std::task::Poll; -use axum::extract::FromRequest; -use axum::extract::Request; +use axum::RequestExt; +use axum::extract::{FromRequest, FromRequestParts, Request}; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; use bytes::{Bytes, BytesMut}; use futures::{StreamExt, TryStreamExt}; use multer::{Field, Multipart}; use relay_config::Config; +use relay_quotas::DataCategory; +use relay_system::Addr; use serde::{Deserialize, Serialize}; use crate::envelope::{AttachmentType, ContentType, Item, ItemType, Items}; -use crate::extractors::Remote; +use crate::extractors::{BadEventMeta, PartialDsn, Remote, RequestMeta}; use crate::service::ServiceState; +use crate::services::outcome::{ + DiscardAttachmentType, DiscardItemType, DiscardReason, Outcome, TrackOutcome, +}; +use crate::utils::ApiErrorResponse; /// Type used for encoding string lengths. type Len = u32; @@ -155,11 +164,38 @@ pub fn get_multipart_boundary(data: &[u8]) -> Option<&str> { .and_then(|slice| std::str::from_utf8(&slice[2..]).ok()) } +#[derive(Debug, thiserror::Error)] +pub enum BadMultipart { + #[error("event metadata error: {0}")] + EventMeta(#[from] BadEventMeta), + #[error("multipart error: {0}")] + Multipart(#[from] multer::Error), +} + +impl From for BadMultipart { + fn from(infallible: Infallible) -> Self { + match infallible {} + } +} + +impl IntoResponse for BadMultipart { + fn into_response(self) -> Response { + let status_code = match self { + BadMultipart::Multipart( + multer::Error::FieldSizeExceeded { .. } | multer::Error::StreamSizeExceeded { .. }, + ) => StatusCode::PAYLOAD_TOO_LARGE, + _ => StatusCode::BAD_REQUEST, + }; + + (status_code, ApiErrorResponse::from_error(&self)).into_response() + } +} + async fn multipart_items( mut multipart: Multipart<'_>, mut infer_type: F, - config: &Config, mut emit_outcome: G, + config: &Config, ignore_large_fields: bool, ) -> Result where @@ -180,7 +216,7 @@ where let field = LimitedField::new(field, config.max_attachment_size()); match field.bytes().await { Err(multer::Error::FieldSizeExceeded { limit, .. }) if ignore_large_fields => { - emit_outcome(limit as u32); + emit_outcome(u32::try_from(limit).unwrap_or(u32::MAX)); continue; } Err(err) => return Err(err), @@ -319,7 +355,7 @@ impl ConstrainedMultipart { F: FnMut(Option<&str>, &str) -> AttachmentType, { // We don't emit an outcome in this code branch since we return an error to the request - multipart_items(self.0, infer_type, config, |_x| {}, false).await + multipart_items(self.0, infer_type, |_| (), config, false).await } } @@ -327,34 +363,64 @@ impl ConstrainedMultipart { /// `max_attachment_size`. These fields are also not taken into account when checking that the /// combined size of all fields is smaller than `max_attachments_size`. #[allow(dead_code)] -pub struct UnconstrainedMultipart(pub Multipart<'static>); +pub struct UnconstrainedMultipart { + pub multipart: Multipart<'static>, + pub outcome_aggregator: Addr, + pub request_meta: RequestMeta, +} impl FromRequest for UnconstrainedMultipart { - type Rejection = Remote; + type Rejection = BadMultipart; async fn from_request( - request: Request, - _state: &ServiceState, + mut request: Request, + state: &ServiceState, ) -> Result { + let mut parts = request.extract_parts().await?; + let request_meta = RequestMeta::::from_request_parts(&mut parts, state).await?; + multipart_from_request(request, multer::Constraints::new()) - .map(Self) - .map_err(Remote) + .map(|multipart| UnconstrainedMultipart { + multipart, + outcome_aggregator: state.outcome_aggregator().clone(), + request_meta, + }) + .map_err(Into::into) } } #[cfg_attr(not(any(test, sentry)), expect(dead_code))] impl UnconstrainedMultipart { - pub async fn items( - self, - infer_type: F, - config: &Config, - emit_outcome: G, - ) -> Result + pub async fn items(self, infer_type: F, config: &Config) -> Result where F: FnMut(Option<&str>, &str) -> AttachmentType, - G: FnMut(u32), { - multipart_items(self.0, infer_type, config, emit_outcome, true).await + let UnconstrainedMultipart { + multipart, + outcome_aggregator, + request_meta, + } = self; + + multipart_items( + multipart, + infer_type, + |quantity| { + outcome_aggregator.send(TrackOutcome { + timestamp: request_meta.received_at(), + scoping: request_meta.get_partial_scoping(), + outcome: Outcome::Invalid(DiscardReason::TooLarge( + DiscardItemType::Attachment(DiscardAttachmentType::Attachment), + )), + event_id: None, + remote_addr: request_meta.remote_addr(), + category: DataCategory::Attachment, + quantity, + }) + }, + config, + true, + ) + .await } } @@ -473,13 +539,14 @@ mod tests { }))?; let mut mock_outcomes = vec![]; - let items = UnconstrainedMultipart(multipart) - .items( - |_, _| AttachmentType::Attachment, - &config, - |x| (mock_outcomes.push(x)), - ) - .await?; + let items = multipart_items( + multipart, + |_, _| AttachmentType::Attachment, + |x| (mock_outcomes.push(x)), + &config, + true, + ) + .await?; // The large field is skipped so only the small one should make it through. assert_eq!(items.len(), 1); @@ -515,9 +582,17 @@ mod tests { let multipart = Multipart::new(stream, "X-BOUNDARY"); - let result = UnconstrainedMultipart(multipart) - .items(|_, _| AttachmentType::Attachment, &config, |_| ()) - .await; + let result = UnconstrainedMultipart { + multipart, + outcome_aggregator: Addr::dummy(), + request_meta: RequestMeta::new( + "https://a94ae32be2584e0bbd7a4cbb95971fee:@sentry.io/42" + .parse() + .unwrap(), + ), + } + .items(|_, _| AttachmentType::Attachment, &config) + .await; // Should be warned if the overall stream limit is being breached. assert!(result.is_err_and(|x| matches!(x, multer::Error::StreamSizeExceeded { limit: _ }))); From 1cd3f8f9d247225ee8c836ba01c2b7896a832edc Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Wed, 25 Jun 2025 17:12:38 +0200 Subject: [PATCH 3/9] Add integration test --- tests/integration/test_playstation.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_playstation.py b/tests/integration/test_playstation.py index e8cb87792e5..419c9847057 100644 --- a/tests/integration/test_playstation.py +++ b/tests/integration/test_playstation.py @@ -268,7 +268,8 @@ def test_playstation_ignore_large_fields( { "limits": { "max_attachment_size": len(video_content) - 1, - } + }, + "outcomes": {"emit_outcomes": True, "batch_size": 1, "batch_interval": 1}, }, ) @@ -276,6 +277,16 @@ def test_playstation_ignore_large_fields( PROJECT_ID, playstation_dump, video_content ) assert response.ok + assert (mini_sentry.captured_outcomes.get()["outcomes"]) == [ + { + "timestamp": mock.ANY, + "project_id": 42, + "outcome": 3, + "reason": "too_large:attachment:attachment", + "category": 4, + "quantity": len(video_content), + } + ] assert [ item.headers["filename"] for item in mini_sentry.captured_events.get().items ] == ["playstation.prosperodmp"] From 25f09a4fb08f050a2df21b29b9ac56ccb9db2481 Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Wed, 25 Jun 2025 17:20:43 +0200 Subject: [PATCH 4/9] Clean up --- relay-server/src/endpoints/playstation.rs | 8 +++----- relay-server/src/utils/multipart.rs | 3 ++- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/relay-server/src/endpoints/playstation.rs b/relay-server/src/endpoints/playstation.rs index f7cc7f74a6f..28c13d6b273 100644 --- a/relay-server/src/endpoints/playstation.rs +++ b/relay-server/src/endpoints/playstation.rs @@ -42,11 +42,9 @@ fn infer_attachment_type(_field_name: Option<&str>, file_name: &str) -> Attachme async fn extract_multipart( multipart: UnconstrainedMultipart, meta: RequestMeta, - state: &ServiceState, + config: &Config, ) -> Result, BadStoreRequest> { - let mut items = multipart - .items(infer_attachment_type, state.config()) - .await?; + let mut items = multipart.items(infer_attachment_type, config).await?; let prosperodump_item = items .iter_mut() @@ -75,7 +73,7 @@ async fn handle( ) -> axum::response::Result { // The crash dumps are transmitted as `...` in a multipart form-data/ request. let multipart = request.extract_with_state(&state).await?; - let mut envelope = extract_multipart(multipart, meta, &state).await?; + let mut envelope = extract_multipart(multipart, meta, state.config()).await?; envelope.require_feature(Feature::PlaystationIngestion); let id = envelope.event_id(); diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index aa6fe18c61f..897315403bb 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -354,7 +354,8 @@ impl ConstrainedMultipart { where F: FnMut(Option<&str>, &str) -> AttachmentType, { - // We don't emit an outcome in this code branch since we return an error to the request + // The emit outcome closure here does nothing since in this code branch we don't want to + // emit outcomes as we already return an error to the request. multipart_items(self.0, infer_type, |_| (), config, false).await } } From c7f51a476fae576aa5ce91f85dac5b5de19b11b7 Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Mon, 14 Jul 2025 10:49:12 +0200 Subject: [PATCH 5/9] remove pub --- relay-server/src/utils/multipart.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index 897315403bb..689880a30d7 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -365,9 +365,9 @@ impl ConstrainedMultipart { /// combined size of all fields is smaller than `max_attachments_size`. #[allow(dead_code)] pub struct UnconstrainedMultipart { - pub multipart: Multipart<'static>, - pub outcome_aggregator: Addr, - pub request_meta: RequestMeta, + multipart: Multipart<'static>, + outcome_aggregator: Addr, + request_meta: RequestMeta, } impl FromRequest for UnconstrainedMultipart { From a79bb54ec54c568d6b2179c15c158da76777dab2 Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Tue, 15 Jul 2025 11:16:33 +0200 Subject: [PATCH 6/9] Changelog entry. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e25b738c834..639bae6a4be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Expand the NEL attributes & others. ([#4874](https://github.com/getsentry/relay/pull/4874)) - Normalize legacy AI agents attributes to OTel compatible names. ([#4916](https://github.com/getsentry/relay/pull/4916)) - Fix cost calculation for cached and reasoning tokens. ([#4922](https://github.com/getsentry/relay/pull/4922)) +- Emit outcomes for skipped large attachments on playstation crashes. ([#4862](https://github.com/getsentry/relay/pull/4862)) ## 25.6.2 From d94fd50a6ff948f028cf9d2786022e13e68da703 Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Thu, 17 Jul 2025 08:36:46 +0200 Subject: [PATCH 7/9] Address feedback --- relay-server/src/utils/multipart.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index 689880a30d7..408410ba817 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -200,7 +200,7 @@ async fn multipart_items( ) -> Result where F: FnMut(Option<&str>, &str) -> AttachmentType, - G: FnMut(u32), + G: FnMut(Outcome, u32), { let mut items = Items::new(); let mut form_data = FormDataWriter::new(); @@ -216,7 +216,12 @@ where let field = LimitedField::new(field, config.max_attachment_size()); match field.bytes().await { Err(multer::Error::FieldSizeExceeded { limit, .. }) if ignore_large_fields => { - emit_outcome(u32::try_from(limit).unwrap_or(u32::MAX)); + emit_outcome( + Outcome::Invalid(DiscardReason::TooLarge(DiscardItemType::Attachment( + DiscardAttachmentType::Attachment, + ))), + u32::try_from(limit).unwrap_or(u32::MAX), + ); continue; } Err(err) => return Err(err), @@ -356,7 +361,7 @@ impl ConstrainedMultipart { { // The emit outcome closure here does nothing since in this code branch we don't want to // emit outcomes as we already return an error to the request. - multipart_items(self.0, infer_type, |_| (), config, false).await + multipart_items(self.0, infer_type, |_, _| (), config, false).await } } @@ -405,13 +410,11 @@ impl UnconstrainedMultipart { multipart_items( multipart, infer_type, - |quantity| { + |outcome, quantity| { outcome_aggregator.send(TrackOutcome { timestamp: request_meta.received_at(), scoping: request_meta.get_partial_scoping(), - outcome: Outcome::Invalid(DiscardReason::TooLarge( - DiscardItemType::Attachment(DiscardAttachmentType::Attachment), - )), + outcome, event_id: None, remote_addr: request_meta.remote_addr(), category: DataCategory::Attachment, @@ -543,7 +546,7 @@ mod tests { let items = multipart_items( multipart, |_, _| AttachmentType::Attachment, - |x| (mock_outcomes.push(x)), + |_, x| (mock_outcomes.push(x)), &config, true, ) From e65467cc782bf16225a157f1562882760c68208e Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Thu, 17 Jul 2025 08:41:23 +0200 Subject: [PATCH 8/9] Move changelog message --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b0cc15a4fc..b8719dabb6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +**Internal**: + +- Emit outcomes for skipped large attachments on playstation crashes. ([#4862](https://github.com/getsentry/relay/pull/4862)) + ## 25.7.0 **Features**: @@ -27,7 +33,6 @@ - Only emit indexed span outcomes when producing directly to Snuba. ([#4936](https://github.com/getsentry/relay/pull/4936)) - Normalize legacy AI agents attributes to OTel compatible names. ([#4916](https://github.com/getsentry/relay/pull/4916)) - Fix cost calculation for cached and reasoning tokens. ([#4922](https://github.com/getsentry/relay/pull/4922)) -- Emit outcomes for skipped large attachments on playstation crashes. ([#4862](https://github.com/getsentry/relay/pull/4862)) - Implement serialization of metadata for logs. ([#4929](https://github.com/getsentry/relay/pull/4929)) ## 25.6.2 From cc3dddb143824ce2a48188fa628a3b67681ec61b Mon Sep 17 00:00:00 2001 From: tobias-wilfert Date: Fri, 18 Jul 2025 11:06:01 +0200 Subject: [PATCH 9/9] Addressing feedback --- relay-server/src/utils/multipart.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/relay-server/src/utils/multipart.rs b/relay-server/src/utils/multipart.rs index 408410ba817..921a46b3e6c 100644 --- a/relay-server/src/utils/multipart.rs +++ b/relay-server/src/utils/multipart.rs @@ -385,13 +385,12 @@ impl FromRequest for UnconstrainedMultipart { let mut parts = request.extract_parts().await?; let request_meta = RequestMeta::::from_request_parts(&mut parts, state).await?; - multipart_from_request(request, multer::Constraints::new()) - .map(|multipart| UnconstrainedMultipart { - multipart, - outcome_aggregator: state.outcome_aggregator().clone(), - request_meta, - }) - .map_err(Into::into) + let multipart = multipart_from_request(request, multer::Constraints::new())?; + Ok(UnconstrainedMultipart { + multipart, + outcome_aggregator: state.outcome_aggregator().clone(), + request_meta, + }) } }