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-client0.12 (tokio feature): streaming responses (execute_once_stream), nounsafe, maintained (2026-07), ~2.5k lines that were reviewed before adoption. It is a small crate (≈70k downloads), so it is wrapped behindscalws-runtime-php’s own transport function, making replacement cheap. - Managed pools use persistent connections (
FCGI_KEEP_CONN), at mostmax_childrenof 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; seedocs/benchmarks/2026-10-05-php-profile.md.) - CGI response headers (
Status:,Location:, headers) are parsed withhttparse(mature). The header block is capped at 64 KiB; the body is streamed. SCRIPT_FILENAMEis only ever a regular file found by the same confined lookup as static files (ADR-0006) and must end in.php;.phpfiles are never served as static content. Managed pools setsecurity.limit_extensions = .php.- Routing mirrors the common nginx setup: existing
.phpscript (withPATH_INFOsplit at the first.php/) → FastCGI; other existing file → static; otherwise the configuredfront_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 viafastcgi:.
Consequences
- If
fastcgi-clientbecomes unmaintained, a replacement codec must come with fuzzing and tests against real PHP-FPM before swapping it in.