relay_server/endpoints/
store.rs1use std::io::{self, Read};
4
5use axum::extract::{DefaultBodyLimit, Query};
6use axum::http::header;
7use axum::response::IntoResponse;
8use axum::routing::{post, MethodRouter};
9use bytes::Bytes;
10use data_encoding::BASE64;
11use flate2::bufread::ZlibDecoder;
12use relay_config::Config;
13use relay_event_schema::protocol::EventId;
14use serde::{Deserialize, Serialize};
15
16use crate::endpoints::common::{self, BadStoreRequest};
17use crate::envelope::{self, ContentType, Envelope, Item, ItemType};
18use crate::extractors::{RawContentType, RequestMeta};
19use crate::service::ServiceState;
20
21fn decode_bytes(body: Bytes, limit: usize) -> Result<Bytes, io::Error> {
28 if body.is_empty() || body.starts_with(b"{") {
29 return Ok(body);
30 }
31
32 let binary_body = BASE64
35 .decode(body.as_ref())
36 .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
37
38 if binary_body.starts_with(b"{") {
39 return Ok(binary_body.into());
40 }
41
42 let mut decode_stream = ZlibDecoder::new(binary_body.as_slice()).take(limit as u64);
43 let mut bytes = vec![];
44 decode_stream.read_to_end(&mut bytes)?;
45
46 Ok(Bytes::from(bytes))
47}
48
49fn parse_event(
53 mut body: Bytes,
54 meta: RequestMeta,
55 config: &Config,
56) -> Result<Box<Envelope>, BadStoreRequest> {
57 body = decode_bytes(body, config.max_event_size()).map_err(BadStoreRequest::InvalidBody)?;
60 if body.is_empty() {
61 return Err(BadStoreRequest::EmptyBody);
62 }
63
64 let is_legacy_python_json = meta.client().is_some_and(|agent| {
71 agent.starts_with("raven-python/") || agent.starts_with("sentry-python/")
72 });
73
74 if is_legacy_python_json {
75 let mut data_mut = body.to_vec();
76 json_forensics::translate_slice(&mut data_mut[..]);
77 body = data_mut.into();
78 }
79
80 let minimal = common::minimal_event_from_json(&body)?;
85
86 let item_type = ItemType::from_event_type(minimal.ty);
89 let mut event_item = Item::new(item_type);
90 event_item.set_payload(ContentType::Json, body);
91
92 let event_id = minimal.id.unwrap_or_else(EventId::new);
93 let mut envelope = Envelope::from_request(Some(event_id), meta);
94 envelope.add_item(event_item);
95
96 Ok(envelope)
97}
98
99#[derive(Serialize)]
100struct PostResponse {
101 #[serde(skip_serializing_if = "Option::is_none")]
102 id: Option<EventId>,
103}
104
105async fn handle_post(
107 state: ServiceState,
108 meta: RequestMeta,
109 content_type: RawContentType,
110 body: Bytes,
111) -> Result<impl IntoResponse, BadStoreRequest> {
112 let envelope = match content_type.as_ref() {
113 envelope::CONTENT_TYPE => Envelope::parse_request(body, meta)?,
114 _ => parse_event(body, meta, state.config())?,
115 };
116
117 let id = common::handle_envelope(&state, envelope).await?;
118 Ok(axum::Json(PostResponse { id }).into_response())
119}
120
121#[derive(Debug, Deserialize)]
123struct GetQuery {
124 sentry_data: String,
125}
126
127static PIXEL: &[u8] =
130 b"GIF89a\x01\x00\x01\x00\x00\xff\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x00;";
131
132async fn handle_get(
137 state: ServiceState,
138 meta: RequestMeta,
139 Query(query): Query<GetQuery>,
140) -> Result<impl IntoResponse, BadStoreRequest> {
141 let envelope = parse_event(query.sentry_data.into(), meta, state.config())?;
142 common::handle_envelope(&state, envelope).await?;
143 Ok(([(header::CONTENT_TYPE, "image/gif")], PIXEL))
144}
145
146pub fn route(config: &Config) -> MethodRouter<ServiceState> {
147 (post(handle_post).get(handle_get)).route_layer(DefaultBodyLimit::max(config.max_event_size()))
148}