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