Decision records

ADR-0009: PHP via FastCGI using the `fastcgi-client` crate

  • Status: Accepted (2026-10-05)

Context

PHP is served through PHP-FPM over FastCGI (handoff §5). Options: write a FastCGI record codec, or use an existing crate. Handoff §1: use mature libraries.

Decision

  • Use fastcgi-client 0.12 (tokio feature): streaming responses (execute_once_stream), no unsafe, maintained (2026-07), ~2.5k lines that were reviewed before adoption. It is a small crate (≈70k downloads), so it is wrapped behind scalws-runtime-php’s own transport function, making replacement cheap.
  • Managed pools use persistent connections (FCGI_KEEP_CONN), at most max_children of them (each pins an FPM worker); external pools use one connection per request. (Revised 2026-10-05: per-request connections cost ~20 % of scalws’s CPU on the PHP path; see docs/benchmarks/2026-10-05-php-profile.md.)
  • CGI response headers (Status:, Location:, headers) are parsed with httparse (mature). The header block is capped at 64 KiB; the body is streamed.
  • SCRIPT_FILENAME is only ever a regular file found by the same confined lookup as static files (ADR-0006) and must end in .php; .php files are never served as static content. Managed pools set security.limit_extensions = .php.
  • Routing mirrors the common nginx setup: existing .php script (with PATH_INFO split at the first .php/) → FastCGI; other existing file → static; otherwise the configured front_controller (e.g. /index.php) → FastCGI; otherwise 404.
  • Managed PHP-FPM: scalws writes a pool configuration from validated values (ini keys and values are restricted to a safe character set, so the generated file cannot be injected), runs php-fpm --nodaemonize --fpm-config <file> under the supervisor (ADR-0008) and talks to its Unix socket. External pools are supported via fastcgi:.

Consequences

  • If fastcgi-client becomes unmaintained, a replacement codec must come with fuzzing and tests against real PHP-FPM before swapping it in.