1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
//! An example of a reverse proxy that accepts HTTP requests and forwards them to a
//! FastCGI backend application, returning the application's response as an HTTP reply.
//!
//! The example spawns two services:
//!
//! 1. A FastCGI application server on `127.0.0.1:62054` that wraps a plain HTTP echo
//! handler via [`FastCgiHttpService`] — demonstrating that any HTTP service can be
//! deployed as a FastCGI backend without modification.
//!
//! 2. An HTTP server on `127.0.0.1:62053` that accepts ordinary HTTP requests,
//! translates them into FastCGI requests via [`FastCgiHttpClient`], and proxies
//! them to the FastCGI backend.
//!
//! This demonstrates both sides of the HTTP adaptive layer:
//! - [`FastCgiHttpClient`]: HTTP reverse proxy → FastCGI backend (client side).
//! - [`FastCgiHttpService`]: HTTP handler deployed as a FastCGI application (server side).
//!
//! # Run the example
//!
//! ```sh
//! cargo run --example fastcgi_reverse_proxy --features=http-full,fastcgi
//! ```
//!
//! # Expected output
//!
//! The proxy will start and listen on `:62053`. You can use `curl` to interact:
//!
//! ```sh
//! curl -v http://127.0.0.1:62053/hello?foo=bar
//! curl -v -X POST http://127.0.0.1:62053/submit -d 'name=rama'
//! ```
//!
//! Each response is the HTTP request echoed back by the backend handler.
#![expect(
clippy::expect_used,
clippy::let_underscore_must_use,
reason = "example/test/bench: panic-on-error and print-for-output are the standard patterns for demos and harnesses"
)]
use rama::{
error::BoxError,
extensions::Extensions,
gateway::fastcgi::{
FastCgiClientRequest, FastCgiHttpClient, FastCgiHttpService, FastCgiServer,
},
http::{
Request, Response, StatusCode, body::util::BodyExt, server::HttpServer,
service::web::response::IntoResponse,
},
net::{address::SocketAddress, client::EstablishedClientConnection},
rt::Executor,
service::service_fn,
tcp::{TcpStream, client::default_tcp_connect, server::TcpListener},
telemetry::tracing::{
self,
level_filters::LevelFilter,
subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt},
},
};
use std::fmt::Write as _;
use std::sync::Arc;
use std::time::Duration;
const PROXY_ADDR: SocketAddress = SocketAddress::local_ipv4(62053);
const BACKEND_ADDR: SocketAddress = SocketAddress::local_ipv4(62054);
#[tokio::main]
async fn main() {
tracing::subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env_lossy(),
)
.init();
let graceful = rama::graceful::Shutdown::default();
let exec = Executor::graceful(graceful.guard());
// Spawn the FastCGI backend: a plain HTTP echo handler wrapped in FastCgiHttpService
// so it can accept FastCGI connections from the proxy.
{
let exec = exec.clone();
let tcp = TcpListener::bind_address(BACKEND_ADDR, exec)
.await
.expect("bind fastcgi backend");
let service = Arc::new(FastCgiServer::new(FastCgiHttpService::new(service_fn(
echo_http,
))));
graceful.spawn_task(tcp.serve(service));
tracing::info!("FastCGI backend listening on {BACKEND_ADDR}");
}
// Small delay to let the backend bind before the proxy starts sending.
tokio::time::sleep(Duration::from_millis(50)).await;
// Spawn the HTTP reverse proxy that translates HTTP to FastCGI.
{
let exec2 = Executor::graceful(graceful.guard());
let tcp = TcpListener::bind_address(PROXY_ADDR, exec)
.await
.expect("bind http proxy");
let proxy = Arc::new(FastCgiProxyService::new());
graceful.spawn_task(tcp.serve(HttpServer::auto(exec2).service(proxy)));
tracing::info!("HTTP→FastCGI reverse proxy listening on {PROXY_ADDR}");
}
graceful
.shutdown_with_limit(Duration::from_secs(30))
.await
.expect("graceful shutdown");
}
// ---------------------------------------------------------------------------
// HTTP echo handler (the real backend logic)
// ---------------------------------------------------------------------------
/// A plain HTTP handler that echoes the request back as plain text.
///
/// This is a normal HTTP service — it knows nothing about FastCGI.
/// [`FastCgiHttpService`] wraps it to accept FastCGI connections.
async fn echo_http(req: Request) -> Result<Response, BoxError> {
let (parts, body) = req.into_parts();
let body_bytes = body.collect().await?.to_bytes();
let mut text = format!("=== {} {} ===\n", parts.method, parts.uri);
for (name, value) in &parts.headers {
// `fmt::Write::write_fmt` cannot fail when writing into a `String`.
let _ = writeln!(
&mut text,
"{name}: {}",
String::from_utf8_lossy(value.as_bytes()),
);
}
if !body_bytes.is_empty() {
text.push_str("\n=== Request Body ===\n");
text.push_str(&String::from_utf8_lossy(&body_bytes));
text.push('\n');
}
Ok(text.into_response())
}
// ---------------------------------------------------------------------------
// TCP connector for the FastCGI backend
// ---------------------------------------------------------------------------
/// Connector that opens a plain TCP connection to the FastCGI backend.
struct BackendConnector;
impl rama::Service<FastCgiClientRequest> for BackendConnector {
type Output = EstablishedClientConnection<TcpStream, FastCgiClientRequest>;
type Error = BoxError;
async fn serve(&self, input: FastCgiClientRequest) -> Result<Self::Output, Self::Error> {
let ext = Extensions::default();
let (conn, _peer) = default_tcp_connect(&ext, BACKEND_ADDR.into()).await?;
Ok(EstablishedClientConnection { input, conn })
}
}
// ---------------------------------------------------------------------------
// HTTP reverse proxy service
// ---------------------------------------------------------------------------
/// HTTP service: proxies each HTTP request to the FastCGI backend.
///
/// [`FastCgiHttpClient`] handles all CGI environment construction and CGI stdout
/// parsing automatically — the proxy only needs to handle connection errors.
struct FastCgiProxyService {
client: FastCgiHttpClient<BackendConnector>,
}
impl FastCgiProxyService {
fn new() -> Self {
Self {
client: FastCgiHttpClient::new(BackendConnector),
}
}
}
impl rama::Service<Request> for FastCgiProxyService {
type Output = Response;
type Error = std::convert::Infallible;
async fn serve(&self, req: Request) -> Result<Response, std::convert::Infallible> {
match self.client.serve(req).await {
Ok(resp) => Ok(resp),
Err(e) => {
tracing::error!("fastcgi backend error: {e}");
Ok((StatusCode::BAD_GATEWAY, "FastCGI backend error\n").into_response())
}
}
}
}