Skip to main content

karbon_framework/http/
proxy.rs

1use axum::{
2    body::Body,
3    extract::Request,
4    response::{IntoResponse, Response},
5};
6use hyper_util::{client::legacy::Client, rt::TokioExecutor};
7
8/// Reverse proxy handler: forwards all requests to the target URL.
9///
10/// Used to proxy frontend SSR requests through the Rust backend,
11/// so only one port needs to be exposed in production.
12#[derive(Clone)]
13pub struct FrontendProxy {
14    target: String,
15    client: Client<hyper_util::client::legacy::connect::HttpConnector, Body>,
16}
17
18impl FrontendProxy {
19    pub fn new(target: &str) -> Self {
20        let client = Client::builder(TokioExecutor::new())
21            .build_http();
22
23        Self {
24            target: target.trim_end_matches('/').to_string(),
25            client,
26        }
27    }
28
29    /// Axum handler that proxies the request to the frontend
30    pub async fn handle(self, req: Request) -> Response {
31        match self.proxy(req).await {
32            Ok(resp) => resp,
33            Err(e) => {
34                tracing::error!("Proxy error: {e}");
35                (
36                    axum::http::StatusCode::BAD_GATEWAY,
37                    "Bad gateway".to_string(),
38                )
39                    .into_response()
40            }
41        }
42    }
43
44    async fn proxy(&self, req: Request) -> Result<Response, Box<dyn std::error::Error + Send + Sync>> {
45        let path = req.uri().path_and_query()
46            .map(|pq| pq.as_str())
47            .unwrap_or("/");
48
49        let uri = format!("{}{}", self.target, path).parse::<hyper::Uri>()?;
50
51        // Rebuild the request with the new URI
52        let (mut parts, body) = req.into_parts();
53        parts.uri = uri;
54
55        let proxy_req = Request::from_parts(parts, body);
56        let resp = self.client.request(proxy_req).await?;
57
58        Ok(resp.into_response())
59    }
60}