Runtime agnostic
Anything implementing the futures-io traits works with no cargo feature: smol, async-net, or your own in-memory stream. Tokio streams are one optional feature away.
Rust crate · php-fpm & FastCGI backends
fastcgi-client is built on the futures-io AsyncRead / AsyncWrite traits and contains no task spawning, timers or socket creation — so it drops into smol, async-net, Tokio, or a bare futures executor without changing a line of protocol code.
cargo add fastcgi-client
Quick start
Every tab below is real code from the repository's examples and tests. The only thing that changes between runtimes is how you get a stream and how you block on the future.
use fastcgi_client::{Client, Params, Request, io};
use std::{error::Error, path::PathBuf};
use tokio::net::TcpStream;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn Error>> {
let document_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("php");
let script_filename = document_root.join("index.php");
let script_name = "/index.php";
// php-fpm default listening address.
let stream = TcpStream::connect(("127.0.0.1", 9000)).await?;
let client = Client::new_tokio(stream);
// FastCGI params mirror the CGI values nginx would send.
let params = Params::default()
.request_method("GET")
.document_root(document_root.to_string_lossy().into_owned())
.script_name(script_name)
.script_filename(script_filename.to_string_lossy().into_owned())
.request_uri(script_name)
.document_uri(script_name)
.remote_addr("127.0.0.1")
.remote_port(12345)
.server_addr("127.0.0.1")
.server_port(80)
.server_name("localhost")
.content_type("")
.content_length(0);
let output = client
.execute_once(Request::new(params, io::empty()))
.await?;
println!(
"stdout:\n{}",
String::from_utf8_lossy(&output.stdout.unwrap_or_default())
);
if let Some(stderr) = output.stderr {
eprintln!("stderr:\n{}", String::from_utf8_lossy(&stderr));
}
Ok(())
}
use fastcgi_client::{Client, Params, Request, io};
use smol::net::TcpStream;
use std::{error::Error, path::PathBuf};
fn main() -> Result<(), Box<dyn Error>> {
smol::block_on(async {
let document_root = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("php");
let script_filename = document_root.join("index.php");
let script_name = "/index.php";
let stream = TcpStream::connect(("127.0.0.1", 9000)).await?;
let mut client = Client::new_keep_alive(stream);
let params = Params::default()
.request_method("GET")
.document_root(document_root.to_string_lossy().into_owned())
.script_name(script_name)
.script_filename(script_filename.to_string_lossy().into_owned())
.request_uri(script_name)
.document_uri(script_name)
.content_type("")
.content_length(0);
// The connection is reused across every iteration.
for request_number in 1..=3 {
let output = client
.execute(Request::new(params.clone(), io::empty()))
.await?;
println!(
"response #{request_number}:\n{}",
String::from_utf8_lossy(&output.stdout.unwrap_or_default())
);
}
Ok(())
})
}
use fastcgi_client::{Client, Params, Request, io};
use futures_executor::block_on;
// `MockStream` is an in-memory duplex stream that implements only the
// futures-io traits — see tests/client_no_runtime.rs for the full source.
fn main() {
let mut stream = MockStream::new(response_bytes());
let output = block_on(async {
let client = Client::new(&mut stream);
let request = Request::new(
Params::default()
.request_method("GET")
.script_name("/index.php"),
io::Cursor::new(b"hello=world".to_vec()),
);
client.execute_once(request).await.unwrap()
});
assert_eq!(output.stderr, None);
println!(
"{}",
String::from_utf8_lossy(&output.stdout.unwrap_or_default())
);
}
use fastcgi_client::{Client, Params, Request, StreamExt, io, response::Content};
use tokio::net::TcpStream;
#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let stream = TcpStream::connect(("127.0.0.1", 9000)).await?;
let client = Client::new_tokio(stream);
let mut stream = client
.execute_once_stream(Request::new(params, io::empty()))
.await?;
let mut stdout_chunks = 0usize;
while let Some(content) = stream.next().await {
match content? {
Content::Stdout(out) => {
stdout_chunks += 1;
println!("stdout chunk #{stdout_chunks}: {} bytes", out.len());
}
Content::Stderr(err) => {
eprintln!("stderr: {} bytes", err.len());
}
}
}
Ok(())
}
# Any futures-io stream: no cargo feature
cargo add fastcgi-client
cargo add smol
# Tokio has its own I/O traits, so it
# gets an optional compat feature
cargo add fastcgi-client --features tokio
cargo add tokio --features full
# Optional http <-> FastCGI conversions
cargo add fastcgi-client --features http
How it works
The client encodes FastCGI records onto the stream you hand it, then decodes the multiplexed reply back into two separate channels.
Features
Four things the crate does, and a matrix of exactly what each cargo feature unlocks.
Anything implementing the futures-io traits works with no cargo feature: smol, async-net, or your own in-memory stream. Tokio streams are one optional feature away.
Short connection consumes the client so a socket can never be reused by accident. Keep-alive borrows it mutably and amortises setup across requests.
execute_once_stream and execute_stream yield Content::Stdout and Content::Stderr chunks as they arrive, so large pages never have to be buffered whole.
With the http feature, FastCGI requests convert into http::Request without buffering the body, and responses parse into http::Response<Vec<u8>>.
| Capability | default | tokio | http |
|---|---|---|---|
Client::new / new_keep_alive: any futures-io stream |
available | available | available |
execute_once / execute |
available | available | available |
execute_once_stream / execute_stream |
available | available | available |
Client::new_tokio / new_keep_alive_tokio |
not available | available | not available |
Request → http::Request |
not available | not available | available |
Response → http::Response<Vec<u8>> |
not available | not available | available |
| Extra dependencies pulled in | not available | tokio, tokio-util |
http |
API at a glance
Short connection methods take self, so the client is gone once the response arrives. Keep-alive methods take &mut self, so the same client keeps serving requests.
ShortConn
One request per client value.
KeepAlive
Reuse one connection for many requests.
The types you pass in and get back.
Examples
Start php-fpm, then run any of these against it. Feature flags are exactly what Cargo.toml requires.
One request to php-fpm with the smallest possible API surface.
cargo run --features tokio --example tokio_short_connection
Reuses a single FastCGI connection for sequential requests.
cargo run --features tokio --example tokio_keep_alive
Counts and prints stdout chunks from a large PHP response.
cargo run --features tokio --example tokio_stream_response
The same short-connection flow driven by smol::block_on.
cargo run --example smol_short_connection
Three requests over one smol TCP stream, no cargo feature.
cargo run --example smol_keep_alive
An HTTP server that translates requests into FastCGI and back.
cargo run --features http,tokio --example axum_proxy_server
docker run --rm --name php-fpm \
-v "$PWD:$PWD" -p 9000:9000 \
php:7.1.30-fpm \
-c /usr/local/etc/php/php.ini-development