Skip to main content

rust_web_server/proxy_config/
builder.rs

1//! Builder: converts a `ProxyConfig` into a live `ConfigDrivenApp` with
2//! health checkers and L4/WS proxy threads.
3
4use std::collections::HashMap;
5use std::sync::atomic::AtomicBool;
6use std::sync::{Arc, RwLock};
7
8use crate::middleware::WithMiddleware;
9use crate::proxy_config::{
10    ActionConfig, AuthConfig, CompiledRoute, ConfigDrivenApp, DynamicProxy, MiddlewareConfig,
11    ProxyConfig, RedirectAdapter, RespondAdapter, RouteMatcher, StaticAdapter, arc_app,
12    AuthMisconfigured, BearerAuthMiddleware, PerRouteMaxBodySize, PerRouteRateLimit,
13};
14
15/// Build a `ConfigDrivenApp` from `rws.config.toml` and spawn L4/WS proxy
16/// threads. Returns the app and a list of background thread handles.
17/// Problems `ProxyConfig::validate` finds are printed first (the same
18/// findings `rws --check` reports). The returned app reloads its routes when
19/// the configuration is reloaded (`SIGHUP`, `POST /admin/config/reload`) —
20/// see [`ConfigDrivenApp::reload_from_file`].
21pub fn build_from_file() -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
22    let source = std::fs::read_to_string(crate::proxy_config::config_file_path()).unwrap_or_default();
23    let config = ProxyConfig::from_str(&source);
24    let mut report = crate::config_check::ConfigReport::default();
25    config.validate(&mut report);
26    for e in &report.errors {
27        eprintln!("[proxy_config] ERROR: {e}");
28    }
29    for w in &report.warnings {
30        eprintln!("[proxy_config] Warning: {w}");
31    }
32    let (app, handles) = build_with_source(config, Some(source));
33    let reloadable = app.clone();
34    crate::config_reload::on_reload(move || reloadable.reload_from_file());
35    (app, handles)
36}
37
38/// Build a `ConfigDrivenApp` from a `ProxyConfig` struct.
39pub fn build(config: ProxyConfig) -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
40    build_with_source(config, None)
41}
42
43fn build_with_source(config: ProxyConfig, source: Option<String>) -> (ConfigDrivenApp, Vec<std::thread::JoinHandle<()>>) {
44    let health_stop = Arc::new(AtomicBool::new(false));
45    let compiled = compile_routes(&config, &health_stop);
46    let handles = spawn_listeners(&config);
47    (ConfigDrivenApp::new(compiled, config, health_stop, source), handles)
48}
49
50/// Compile `config`'s routes and start health checkers for its upstreams;
51/// the checkers stop when `health_stop` is set. Used at startup and on every
52/// hot reload.
53pub(crate) fn compile_routes(config: &ProxyConfig, health_stop: &Arc<AtomicBool>) -> Vec<CompiledRoute> {
54    // ── 1. Build upstream name → live backends map ─────────────────────────
55    let mut upstream_lives: HashMap<String, Arc<RwLock<Vec<String>>>> = HashMap::new();
56    // tls_ca_file trust settings per upstream, loaded once per compile (so a
57    // reload picks up a rotated CA). A CA file that can't be loaded fails
58    // closed: every handshake fails (502) instead of trusting public CAs.
59    let mut upstream_trust: HashMap<String, Arc<crate::proxy::UpstreamTls>> = HashMap::new();
60    // Weights, stickiness, circuit breaker, retries: one state per
61    // upstream, shared by its routes.
62    let mut upstream_policies: HashMap<String, Arc<crate::proxy_config::upstream::UpstreamPolicy>> = HashMap::new();
63
64    for upstream in &config.upstreams {
65        let live = Arc::new(RwLock::new(upstream.backends.clone()));
66        upstream_lives.insert(upstream.name.clone(), Arc::clone(&live));
67        upstream_policies.insert(
68            upstream.name.clone(),
69            Arc::new(crate::proxy_config::upstream::UpstreamPolicy::new(
70                &upstream.name,
71                Arc::clone(&live),
72                &upstream.backends,
73                &upstream.weights,
74                upstream.sticky.clone(),
75                upstream.circuit_breaker.as_ref(),
76                upstream.retry.clone(),
77            )),
78        );
79
80        let client = upstream.tls_client_cert_file.as_deref().zip(upstream.tls_client_key_file.as_deref());
81        let configured = upstream.tls_ca_file.is_some() || upstream.tls_client_cert_file.is_some() || upstream.tls_client_key_file.is_some();
82        let trust = configured.then(|| {
83            Arc::new(crate::proxy::UpstreamTls::new(upstream.tls_ca_file.as_deref(), client).unwrap_or_else(|e| {
84                eprintln!("[proxy_config] ERROR: upstream '{}': {e} — its TLS backends answer 502 until this is fixed", upstream.name);
85                crate::proxy::UpstreamTls::reject_all(&upstream.name)
86            }))
87        });
88        if let Some(trust) = &trust {
89            upstream_trust.insert(upstream.name.clone(), Arc::clone(trust));
90        }
91
92        if let Some(ref hc) = upstream.health_check {
93            crate::proxy_config::health::start_health_checker(
94                upstream.name.clone(),
95                upstream.backends.clone(),
96                Arc::clone(&live),
97                hc.clone(),
98                Arc::clone(health_stop),
99                trust,
100            );
101        }
102    }
103
104    crate::proxy_config::upstream::register(upstream_policies.values().cloned().collect());
105
106    // ── 2. Build compiled routes ───────────────────────────────────────────
107    let mut caches: Vec<(String, crate::cache::CacheLayer)> = Vec::new();
108    let mut compiled: Vec<CompiledRoute> = Vec::new();
109
110    for route in &config.routes {
111        let matcher = RouteMatcher::from_match_config(&route.match_);
112
113        let mut async_proxy: Option<Arc<DynamicProxy>> = None;
114        let cache_name = format!("route:{}", if route.name.is_empty() { format!("#{}", compiled.len() + 1) } else { route.name.clone() });
115        let mirror = route.middleware.mirror.as_ref().map(|m| build_mirror(&route.name, m, &upstream_lives));
116
117        // Build the action handler (base application)
118        let base_handler: Arc<dyn crate::application::Application + Send + Sync> =
119            match &route.action {
120                ActionConfig::Proxy {
121                    upstream,
122                    connect_timeout_ms,
123                    read_timeout_ms,
124                    strip_path_prefix,
125                    add_path_prefix,
126                } => {
127                    let live = upstream_lives
128                        .get(upstream.as_str())
129                        .cloned()
130                        .unwrap_or_else(|| {
131                            // Fallback: upstream not declared, use name as single backend
132                            Arc::new(RwLock::new(vec![upstream.clone()]))
133                        });
134                    let upstream_cfg = config.upstreams.iter().find(|u| u.name == *upstream);
135                    let upstream_tls = upstream_cfg.map_or_else(|| upstream.starts_with("https://"), |u| u.tls);
136                    let upstream_strategy = upstream_cfg
137                        .map(|u| u.strategy.clone())
138                        .unwrap_or_else(|| "round_robin".to_string());
139                    let proxy = Arc::new(DynamicProxy::new(
140                        live,
141                        *connect_timeout_ms,
142                        *read_timeout_ms,
143                        strip_path_prefix.clone(),
144                        add_path_prefix.clone(),
145                        upstream_tls,
146                        upstream_strategy,
147                    )
148                    .with_trust(upstream_trust.get(upstream.as_str()).cloned())
149                    .with_policy(upstream, upstream_policies.get(upstream.as_str()).cloned())
150                    .with_mirror(mirror.clone())
151                    .with_early_hints(&route.middleware.early_hints));
152                    if proxy.supports_async() && route.middleware.is_empty() {
153                        async_proxy = Some(Arc::clone(&proxy));
154                    }
155                    proxy
156                }
157
158                ActionConfig::Redirect { location, status } => {
159                    arc_app(RedirectAdapter::new(location.clone(), *status))
160                }
161
162                ActionConfig::Respond { status, body, content_type } => {
163                    arc_app(RespondAdapter::new(*status, body.clone(), content_type.clone()))
164                }
165
166                ActionConfig::Static { root, index } => {
167                    arc_app(StaticAdapter::new(root.clone(), index.clone()))
168                }
169
170                // Grpc action: HTTP/2 to the upstream (HTTP/1.1 only in builds
171                // without the http2 feature, where gRPC can't work anyway).
172                ActionConfig::Grpc { upstream, connect_timeout_ms, read_timeout_ms } => {
173                    let live = upstream_lives
174                        .get(upstream.as_str())
175                        .cloned()
176                        .unwrap_or_else(|| Arc::new(RwLock::new(vec![upstream.clone()])));
177                    let upstream_cfg = config.upstreams.iter().find(|u| u.name == *upstream);
178                    let upstream_tls = upstream_cfg.map_or_else(|| upstream.starts_with("https://"), |u| u.tls);
179                    #[cfg(feature = "http2")]
180                    {
181                        let _ = upstream_cfg;
182                        arc_app(crate::proxy_config::H2Route::new(
183                            live,
184                            upstream_tls,
185                            upstream_trust.get(upstream.as_str()).cloned(),
186                            *connect_timeout_ms,
187                            *read_timeout_ms,
188                        ))
189                    }
190                    #[cfg(not(feature = "http2"))]
191                    {
192                        let upstream_strategy = upstream_cfg
193                            .map(|u| u.strategy.clone())
194                            .unwrap_or_else(|| "round_robin".to_string());
195                        arc_app(
196                            DynamicProxy::new(live, *connect_timeout_ms, *read_timeout_ms, None, None, upstream_tls, upstream_strategy)
197                                .with_trust(upstream_trust.get(upstream.as_str()).cloned())
198                                .with_policy(upstream, upstream_policies.get(upstream.as_str()).cloned()),
199                        )
200                    }
201                }
202
203                ActionConfig::Mcp(gateway) => arc_app(crate::proxy_config::mcp_gateway::GatewayAdapter::start(
204                    (**gateway).clone(),
205                    route.match_.path.clone().unwrap_or_else(|| "/mcp".to_string()),
206                )),
207                ActionConfig::Llm(llm) => arc_app(crate::proxy_config::llm::build(llm)),
208                ActionConfig::Unknown(_) => {
209                    // No-op: these fall through to fallback
210                    arc_app(crate::proxy_config::NullApp)
211                }
212            };
213
214        // Wrap the base handler with middleware layers. A proxy route sends
215        // its mirror copies and early hints itself (so it keeps the async
216        // forwarder); any other action gets a MirrorLayer / EarlyHintsLayer.
217        let mut handler = apply_middleware_named(base_handler, &route.middleware, &cache_name, &mut caches);
218        if !matches!(route.action, ActionConfig::Proxy { .. }) {
219            if let Some(mirror) = &mirror {
220                handler = arc_app(WithMiddleware::new(ArcApp(handler)).wrap(crate::proxy::mirror::MirrorLayer::new(mirror.clone())));
221            }
222            if !route.middleware.early_hints.is_empty() {
223                let layer = crate::early_hints::EarlyHintsLayer::new(route.middleware.early_hints.iter().cloned());
224                handler = arc_app(WithMiddleware::new(ArcApp(handler)).wrap(layer));
225            }
226        }
227
228        compiled.push(CompiledRoute { matcher, handler, async_proxy });
229    }
230    crate::cache::registry::replace_group("route:", caches);
231    compiled
232}
233
234/// Start the `[[tcp_proxy]]` / `[[udp_proxy]]` / `[[ws_proxy]]` listeners.
235/// Startup only: a hot reload can't move bound sockets, so changes to these
236/// sections need a restart.
237fn spawn_listeners(config: &ProxyConfig) -> Vec<std::thread::JoinHandle<()>> {
238    let mut handles: Vec<std::thread::JoinHandle<()>> = Vec::new();
239
240    for tcp_cfg in &config.tcp_proxies {
241        let listen = tcp_cfg.listen.clone();
242        let backends = tcp_cfg.backends.clone();
243        let timeout_ms = tcp_cfg.connect_timeout_ms;
244        let name = tcp_cfg.name.clone();
245        let h = std::thread::Builder::new()
246            .name(format!("tcp-proxy-{}", name))
247            .spawn(move || {
248                let proxy = crate::tcp_proxy::TcpProxy::new(backends)
249                    .connect_timeout_ms(timeout_ms);
250                if let Err(e) = proxy.bind(&listen) {
251                    eprintln!("[tcp_proxy:{}] {}", name, e);
252                }
253            })
254            .expect("failed to spawn tcp proxy thread");
255        handles.push(h);
256    }
257
258    for udp_cfg in &config.udp_proxies {
259        let listen = udp_cfg.listen.clone();
260        let backends = udp_cfg.backends.clone();
261        let reply_timeout_ms = udp_cfg.reply_timeout_ms;
262        let buffer_size = udp_cfg.buffer_size;
263        let name = udp_cfg.name.clone();
264        let h = std::thread::Builder::new()
265            .name(format!("udp-proxy-{}", name))
266            .spawn(move || {
267                let proxy = crate::udp_proxy::UdpProxy::new(backends)
268                    .reply_timeout_ms(reply_timeout_ms)
269                    .buffer_size(buffer_size);
270                if let Err(e) = proxy.bind(&listen) {
271                    eprintln!("[udp_proxy:{}] {}", name, e);
272                }
273            })
274            .expect("failed to spawn udp proxy thread");
275        handles.push(h);
276    }
277
278    for ws_cfg in &config.ws_proxies {
279        let listen = ws_cfg.listen.clone();
280        let backends = ws_cfg.backends.clone();
281        let connect_timeout_ms = ws_cfg.connect_timeout_ms;
282        let read_timeout_ms = ws_cfg.read_timeout_ms;
283        let name = ws_cfg.name.clone();
284
285        // With a health check configured, share a live-backend list with a
286        // background checker thread (same mechanism `[[upstream]]` pools
287        // use) instead of treating every configured backend as always live.
288        let live = Arc::new(RwLock::new(backends.clone()));
289        if let Some(ref hc) = ws_cfg.health_check {
290            crate::proxy_config::health::start_health_checker(
291                name.clone(),
292                backends.clone(),
293                Arc::clone(&live),
294                hc.clone(),
295                Arc::new(AtomicBool::new(false)),
296                None,
297            );
298        }
299
300        let h = std::thread::Builder::new()
301            .name(format!("ws-proxy-{}", name))
302            .spawn(move || {
303                let proxy = crate::ws_proxy::WsProxy::with_live_backends(backends, live)
304                    .connect_timeout_ms(connect_timeout_ms)
305                    .read_timeout_ms(read_timeout_ms);
306                if let Err(e) = proxy.bind(&listen) {
307                    eprintln!("[ws_proxy:{}] {}", name, e);
308                }
309            })
310            .expect("failed to spawn ws proxy thread");
311        handles.push(h);
312    }
313
314    handles
315}
316
317/// Wrap `handler` with the middleware layers described by `mw`, innermost
318/// (closest to the handler) to outermost (runs first, on every request):
319///
320///   1. Cache (innermost)
321///   2. Rewrite (request + response rules combined)
322///   3. Auth
323///   4. Rate limit
324///   5. IP filter
325///   6. Timeout — bounds this route's total time, including every layer above
326///   7. Max body size (outermost) — a fast, non-blocking length check, so a
327///      request this route will reject outright doesn't pay for a timeout
328///      wrapper (which runs its target on a background thread) or engage
329///      any other layer at all
330/// A route's mirror: a declared upstream's live backends, or its own list.
331fn build_mirror(route: &str, config: &crate::proxy_config::MirrorConfig, lives: &HashMap<String, Arc<RwLock<Vec<String>>>>) -> crate::proxy::mirror::Mirror {
332    let name = if config.upstream.is_empty() { format!("{route}-mirror") } else { config.upstream.clone() };
333    let mirror = match lives.get(&config.upstream) {
334        Some(live) if !config.upstream.is_empty() => crate::proxy::mirror::Mirror::with_live_backends(name, Arc::clone(live)),
335        _ if !config.upstream.is_empty() => crate::proxy::mirror::Mirror::new(name, vec![config.upstream.clone()]),
336        _ => crate::proxy::mirror::Mirror::new(name, config.backends.clone()),
337    };
338    mirror.percent(if config.percent.is_finite() { config.percent } else { 0.0 })
339}
340
341pub(crate) fn apply_middleware(
342    handler: Arc<dyn crate::application::Application + Send + Sync>,
343    mw: &MiddlewareConfig,
344) -> Arc<dyn crate::application::Application + Send + Sync> {
345    apply_middleware_named(handler, mw, "middleware", &mut Vec::new())
346}
347
348/// [`apply_middleware`], recording the route's cache (if any) as `name`
349/// in `caches` for the admin API's registry.
350pub(crate) fn apply_middleware_named(
351    handler: Arc<dyn crate::application::Application + Send + Sync>,
352    mw: &MiddlewareConfig,
353    name: &str,
354    caches: &mut Vec<(String, crate::cache::CacheLayer)>,
355) -> Arc<dyn crate::application::Application + Send + Sync> {
356    // Wrap the Arc<dyn Application> in an ArcApp adapter so it implements
357    // Application itself (needed for WithMiddleware::new).
358    let mut app: Arc<dyn crate::application::Application + Send + Sync> = handler;
359
360    // ── Cache (applied first so it's the innermost middleware) ─────────────
361    if let Some(ref cache_cfg) = mw.cache {
362        let mut layer = if cache_cfg.dir.is_empty() {
363            crate::cache::CacheLayer::memory(1000)
364        } else {
365            let max = crate::proxy_config::cache_max_bytes(&cache_cfg.max_size).unwrap_or(256 * 1024 * 1024);
366            crate::cache::CacheLayer::disk(&cache_cfg.dir, max).unwrap_or_else(|e| {
367                eprintln!("[proxy_config] ERROR: cache dir {}: {e} — this route caches in memory instead", cache_cfg.dir);
368                crate::cache::CacheLayer::memory(1000)
369            })
370        }
371        .ttl(cache_cfg.ttl_secs);
372        if !cache_cfg.purge_token_env.is_empty() {
373            layer = layer.purge_token(std::env::var(&cache_cfg.purge_token_env).unwrap_or_default().trim());
374        }
375        caches.push((name.to_string(), layer.clone()));
376        for vh in &cache_cfg.vary_by {
377            layer = layer.vary_by_header(vh.as_str());
378        }
379        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
380    }
381
382    // ── Rewrite ────────────────────────────────────────────────────────────
383    if !mw.rewrite_request.is_empty() || !mw.rewrite_response.is_empty() {
384        let mut layer = crate::rewrite::RewriteLayer::new();
385        for rule in &mw.rewrite_request {
386            layer = apply_request_rewrite_rule(layer, rule);
387        }
388        for rule in &mw.rewrite_response {
389            layer = apply_response_rewrite_rule(layer, rule);
390        }
391        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer));
392    }
393
394    // ── WebAssembly plugin (inside auth: it sees authenticated requests) ──
395    if let Some(ref wasm) = mw.wasm {
396        #[cfg(feature = "wasm-plugins")]
397        let layer = crate::wasm_plugin::WasmLayer::from_file(&wasm.module)
398            .map(|l| l.fuel(wasm.fuel).memory_mb(wasm.memory_mb).config(wasm.config.clone()));
399        #[cfg(not(feature = "wasm-plugins"))]
400        let layer: Result<crate::proxy_config::PluginMisconfigured, String> =
401            Err(format!("plugin {}: this rws binary was built without the wasm-plugins feature", wasm.module));
402        app = match layer {
403            Ok(layer) => arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer)),
404            Err(e) => {
405                eprintln!("[proxy_config] ERROR: {e} — this route answers 503 until the plugin is fixed.");
406                arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(crate::proxy_config::PluginMisconfigured))
407            }
408        };
409    }
410
411    // ── Auth ───────────────────────────────────────────────────────────────
412    // Fails closed: an auth setting that can't be honoured (secret unset,
413    // htpasswd missing, feature not compiled in) puts AuthMisconfigured in
414    // front of the route instead of silently serving it unauthenticated.
415    if let Some(ref auth_cfg) = mw.auth {
416        let deny = |reason: String| -> Arc<dyn crate::application::Application + Send + Sync> {
417            eprintln!(
418                "[proxy_config] ERROR: {reason} — this route answers 503 until the auth configuration is fixed."
419            );
420            arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(AuthMisconfigured))
421        };
422        app = match auth_cfg {
423            AuthConfig::Bearer { token_env } => {
424                let token = std::env::var(token_env).unwrap_or_default();
425                if token.is_empty() {
426                    deny(format!("bearer auth: env var '{token_env}' is unset or empty"))
427                } else {
428                    arc_app(
429                        WithMiddleware::new(ArcApp(Arc::clone(&app)))
430                            .wrap(BearerAuthMiddleware { token: Arc::new(token) }),
431                    )
432                }
433            }
434            #[cfg(feature = "auth")]
435            AuthConfig::Jwt { secret_env } => {
436                let secret = std::env::var(secret_env).unwrap_or_default();
437                if secret.is_empty() {
438                    deny(format!("JWT auth: env var '{secret_env}' is unset or empty"))
439                } else {
440                    arc_app(
441                        WithMiddleware::new(ArcApp(Arc::clone(&app)))
442                            .wrap(crate::auth::JwtLayer::new(secret)),
443                    )
444                }
445            }
446            #[cfg(not(feature = "auth"))]
447            AuthConfig::Jwt { .. } => {
448                deny("JWT auth requires the 'auth' feature, which this build doesn't include".to_string())
449            }
450            #[cfg(feature = "auth")]
451            AuthConfig::Basic { htpasswd_file } => {
452                match crate::auth::BasicAuthLayer::from_htpasswd_file(htpasswd_file) {
453                    Ok(layer) => arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(layer)),
454                    Err(e) => deny(format!("basic auth: {e}")),
455                }
456            }
457            #[cfg(not(feature = "auth"))]
458            AuthConfig::Basic { .. } => {
459                deny("basic auth requires the 'auth' feature, which this build doesn't include".to_string())
460            }
461        };
462    }
463
464    // ── Rate limit ─────────────────────────────────────────────────────────
465    if let Some(ref rl_cfg) = mw.rate_limit {
466        let limiter = Arc::new(crate::rate_limit::RateLimiter::new(
467            rl_cfg.max_requests,
468            rl_cfg.window_secs,
469        ));
470        app = arc_app(
471            WithMiddleware::new(ArcApp(Arc::clone(&app)))
472                .wrap(PerRouteRateLimit(limiter)),
473        );
474    }
475
476    // ── IP filter ────────────────────────────────────────────────────────────
477    if !mw.ip_allow.is_empty() {
478        let filter = crate::ip_filter::IpFilter::allow(mw.ip_allow.iter().map(|s| s.as_str()));
479        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
480    } else if !mw.ip_deny.is_empty() {
481        let filter = crate::ip_filter::IpFilter::deny(mw.ip_deny.iter().map(|s| s.as_str()));
482        app = arc_app(WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(filter));
483    }
484
485    // ── Timeout (bounds this route's total time, including every other
486    // middleware above) ─────────────────────────────────────────────────────
487    if let Some(timeout_ms) = mw.timeout_ms {
488        app = arc_app(crate::timeout::TimeoutLayer::from_arc(
489            app,
490            std::time::Duration::from_millis(timeout_ms),
491        ));
492    }
493
494    // ── Max body size (outermost — see PerRouteMaxBodySize's docs for why
495    // this is not a substitute for RWS_CONFIG_MAX_BODY_SIZE_IN_BYTES) ──────
496    if let Some(max_body_size) = mw.max_body_size {
497        app = arc_app(
498            WithMiddleware::new(ArcApp(Arc::clone(&app))).wrap(PerRouteMaxBodySize(max_body_size)),
499        );
500    }
501
502    app
503}
504
505fn apply_request_rewrite_rule(
506    layer: crate::rewrite::RewriteLayer,
507    rule: &crate::proxy_config::RewriteRuleConfig,
508) -> crate::rewrite::RewriteLayer {
509    match rule.type_.as_str() {
510        "header_set" => {
511            if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
512                return layer.request_header_set(name, value);
513            }
514        }
515        "header_remove" => {
516            if let Some(name) = &rule.name {
517                return layer.request_header_remove(name);
518            }
519        }
520        "uri_set" => {
521            if let Some(value) = &rule.value {
522                return layer.request_uri_set(value);
523            }
524        }
525        "uri_strip_prefix" | "strip_prefix" => {
526            if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
527                return layer.request_uri_strip_prefix(prefix);
528            }
529        }
530        "uri_add_prefix" | "add_prefix" => {
531            if let Some(prefix) = rule.prefix.as_ref().or(rule.value.as_ref()) {
532                return layer.request_uri_add_prefix(prefix);
533            }
534        }
535        _ => {}
536    }
537    layer
538}
539
540fn apply_response_rewrite_rule(
541    layer: crate::rewrite::RewriteLayer,
542    rule: &crate::proxy_config::RewriteRuleConfig,
543) -> crate::rewrite::RewriteLayer {
544    match rule.type_.as_str() {
545        "header_set" => {
546            if let (Some(name), Some(value)) = (&rule.name, &rule.value) {
547                return layer.response_header_set(name, value);
548            }
549        }
550        "header_remove" => {
551            if let Some(name) = &rule.name {
552                return layer.response_header_remove(name);
553            }
554        }
555        "status" => {
556            if let (Some(code), Some(reason)) = (&rule.code, &rule.reason) {
557                return layer.response_status(*code as i16, reason);
558            }
559        }
560        "body_replace" => {
561            if let (Some(from), Some(to)) = (&rule.from, &rule.to) {
562                return layer.response_body_replace(from, to);
563            }
564        }
565        _ => {}
566    }
567    layer
568}
569
570// ── ArcApp adapter ─────────────────────────────────────────────────────────────
571
572/// Wraps `Arc<dyn Application + Send + Sync>` to implement `Application`
573/// (needed because you can't implement a foreign trait on a foreign type).
574pub(crate) struct ArcApp(pub(crate) Arc<dyn crate::application::Application + Send + Sync>);
575
576impl crate::application::Application for ArcApp {
577    fn execute(
578        &self,
579        request: &crate::request::Request,
580        conn: &crate::server::ConnectionInfo,
581    ) -> Result<crate::response::Response, String> {
582        self.0.execute(request, conn)
583    }
584
585    fn streams_body(&self, request: &crate::request::Request, conn: &crate::server::ConnectionInfo) -> bool {
586        self.0.streams_body(request, conn)
587    }
588}
589
590impl Clone for ArcApp {
591    fn clone(&self) -> Self {
592        ArcApp(Arc::clone(&self.0))
593    }
594}