Rust crate · php-fpm & FastCGI backends

Talk FastCGI from async Rust. Any runtime. Or none.

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.

futures-io, zero runtime assumptions Short connection & keep-alive Streaming stdout / stderr Optional http interop

cargo add fastcgi-client

Quick start

Pick a runtime. The client stays the same.

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.

--features tokio Short connection: one request, one connection.

examples/tokio_short_connection.rs
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(())
}
install
# 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

Why this crate

  • No task spawning, no timers, no socket creation — you own the transport.
  • Params and Request keep request building to a single builder chain.
  • Responses arrive buffered or streamed, with stdout and stderr kept apart.

How it works

One hop between your service and php-fpm.

The client encodes FastCGI records onto the stream you hand it, then decodes the multiplexed reply back into two separate channels.

HTTP Params records stdout stderr HTTP client browser · curl Your service axum · hyper · custom fastcgi-client FastCGI record codec php-fpm 127.0.0.1:9000 futures-io AsyncRead + AsyncWrite
Request records: BEGIN_REQUEST, PARAMS, STDIN
Response body, buffered or streamed
Script errors, never mixed into stdout

Features

A small surface, deliberately.

Four things the crate does, and a matrix of exactly what each cargo feature unlocks.

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.

Two connection modes

Short connection consumes the client so a socket can never be reused by accident. Keep-alive borrows it mutably and amortises setup across requests.

Streaming responses

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.

Optional http interop

With the http feature, FastCGI requests convert into http::Request without buffering the body, and responses parse into http::Response<Vec<u8>>.

Cargo features are additive: enabling tokio or http keeps everything from the default build.
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
Requesthttp::Request not available not available available
Responsehttp::Response<Vec<u8>> not available not available available
Extra dependencies pulled in not available tokio, tokio-util http

API at a glance

Eight entry points, two ownership rules.

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.

Examples

Six runnable flows in the repository.

Start php-fpm, then run any of these against it. Feature flags are exactly what Cargo.toml requires.

tokio_short_connection

tokio

One request to php-fpm with the smallest possible API surface.

cargo run --features tokio --example tokio_short_connection

View source

tokio_keep_alive

tokio

Reuses a single FastCGI connection for sequential requests.

cargo run --features tokio --example tokio_keep_alive

View source

tokio_stream_response

tokio

Counts and prints stdout chunks from a large PHP response.

cargo run --features tokio --example tokio_stream_response

View source

smol_short_connection

no feature

The same short-connection flow driven by smol::block_on.

cargo run --example smol_short_connection

View source

smol_keep_alive

no feature

Three requests over one smol TCP stream, no cargo feature.

cargo run --example smol_keep_alive

View source

axum_proxy_server

http + tokio

An HTTP server that translates requests into FastCGI and back.

cargo run --features http,tokio --example axum_proxy_server

View source

Before you run them

  • The examples expect php-fpm on 127.0.0.1:9000.
  • PHP fixtures live in tests/php, mounted at the same absolute path so SCRIPT_FILENAME resolves inside the container.
php-fpm
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