relay_server/endpoints/
forward.rs

1//! Server endpoint that proxies any request to the upstream.
2//!
3//! This endpoint will issue a client request to the upstream and append relay's own headers
4//! (`X-Forwarded-For` and `Sentry-Relay-Id`). The response is then streamed back to the origin.
5
6use std::future::Future;
7use std::sync::LazyLock;
8
9use axum::extract::{DefaultBodyLimit, Request};
10use axum::handler::Handler;
11use axum::http::{HeaderMap, HeaderValue, StatusCode, Uri};
12use axum::response::{IntoResponse, Response};
13use bytes::Bytes;
14use relay_common::glob2::GlobMatcher;
15use relay_config::Config;
16
17use crate::extractors::ForwardedFor;
18use crate::service::ServiceState;
19use crate::services::upstream::Method;
20use crate::utils::ForwardRequest;
21
22/// Root path of all API endpoints.
23const API_PATH: &str = "/api/";
24
25/// Internal implementation of the forward endpoint.
26async fn handle(
27    state: ServiceState,
28    forwarded_for: ForwardedFor,
29    method: Method,
30    uri: Uri,
31    headers: HeaderMap<HeaderValue>,
32    data: Bytes,
33) -> impl IntoResponse {
34    if !state.config().http_forward() {
35        return StatusCode::NOT_FOUND.into_response();
36    }
37
38    // The `/api/` path is special as it is actually a web UI endpoint. Therefore, reject requests
39    // that either go to the API root or point outside the API.
40    if uri.path() == API_PATH || !uri.path().starts_with(API_PATH) {
41        return StatusCode::NOT_FOUND.into_response();
42    }
43
44    ForwardRequest::builder(method, uri.to_string())
45        .with_name("forward")
46        .with_headers(headers)
47        .with_forwarded_for(forwarded_for)
48        .with_body(data)
49        .with_config(state.config())
50        .send_to(state.upstream_relay())
51        .await
52        .into_response()
53}
54
55/// Route classes with request body limit overrides.
56#[derive(Clone, Copy, Debug)]
57enum SpecialRoute {
58    FileUpload,
59    ChunkUpload,
60}
61
62/// Glob matcher for special routes.
63static SPECIAL_ROUTES: LazyLock<GlobMatcher<SpecialRoute>> = LazyLock::new(|| {
64    let mut m = GlobMatcher::new();
65    // file uploads / legacy dsym uploads
66    m.add(
67        "/api/0/projects/*/*/releases/*/files/",
68        SpecialRoute::FileUpload,
69    );
70    m.add(
71        "/api/0/projects/*/*/releases/*/dsyms/",
72        SpecialRoute::FileUpload,
73    );
74    // new chunk uploads
75    m.add(
76        "/api/0/organizations/*/chunk-upload/",
77        SpecialRoute::ChunkUpload,
78    );
79    m
80});
81
82/// Returns the maximum request body size for a route path.
83fn get_limit_for_path(path: &str, config: &Config) -> usize {
84    match SPECIAL_ROUTES.test(path) {
85        Some(SpecialRoute::FileUpload) => config.max_api_file_upload_size(),
86        Some(SpecialRoute::ChunkUpload) => config.max_api_chunk_upload_size(),
87        None => config.max_api_payload_size(),
88    }
89}
90
91/// Forward endpoint handler.
92///
93/// This endpoint will create a proxy request to the upstream for every incoming request and stream
94/// the request body back to the origin. Regardless of the incoming connection, the connection to
95/// the upstream uses its own HTTP version and transfer encoding.
96///
97/// # Usage
98///
99/// This endpoint is both a handler and a request function:
100///
101/// - Use it as [`Handler`] directly in router methods when registering this as a route.
102/// - Call this manually from other request handlers to conditionally forward from other endpoints.
103pub fn forward(state: ServiceState, req: Request) -> impl Future<Output = Response> {
104    let limit = get_limit_for_path(req.uri().path(), state.config());
105    handle.layer(DefaultBodyLimit::max(limit)).call(req, state)
106}