Jan 27 2025
Check out the implementation on GitHub
Below I'll give an explanation of the protocol implementation. You can also find it on the github repo above.
This started out as an experiment in building my own tool for local laravel development. I wanted to figure out how exactly the setup we know (nginx and php-fpm) works under the hood.
It was also a good opportunity for me to practice my Go skills.
The original spec for FastCGI can be a little confusing to implement since it leaves out important details such as how http headers are to be forwarded. Not to mention that if it's your first time working with binary protocols (like it was for me), the explanations and code examples can easily go over your head at first.
If you're interested in the inner workings of FastCGI, I'm providing you an easy to understand implementation. The code is well commented and as explicit as possible. No creative programming here.
At first, the protocol might seem daunting and confusing, but it gets easier once you understand that we're just forwarding a regular HTTP request over a binary protocol to PHP FPM.
Binary protocol!? That sounds scary. It's not. The only difference from what you're used to is that instead of sending text in a specific order, we're sending bytes in a specific order.
Environment Variables
There's a few important variables we need to send so PHP-FPM knows what script to execute, how to execute it, and what the context of the request is.
SERVER_SOFTWARE
= myserverQUERY_STRING
= ex: name=John&page=2REMOTE_ADDR
= server ipREQUEST_METHOD
= GET, POST, etcREQUEST_URI
= /the/path/requestedSERVER_ADDR
= ip or domain nameSERVER_PORT
= port you're serving onSERVER_NAME
= "localhost" or whatever makes sense to send hereHTTP Headers
The request headers also get send to PHP-FPM, however, they should be send in the same way the environment variables above are sent.
Furthermore, the name needs to be changed to environment variable casing. Ex: "Content-Type" => "CONTENT_TYPE".
Some variables are expected to be prefixed with "HTTP_". In my implementation I just send both the variable without the HTTP_ prefix and with, since the documentation for which variables need the prefix is non-existent.
Request body
If we have a POST request, we aso have to forward the body to PHP-FPM. This is where the Content-Type header matters. The body could be:
So the Content-Type header lets PHP know how to decode it. However, as far as encoding the body for sending, it doesn't matter to us since it will follow the same format regardless of content type. It's on PHP to figure out what to do.
The FastCGI protocol is pretty simple. I'll be giving a simple explanation. For all the details you can check out the original spec since it's an easier read once you get the basic idea.
"Records" are just network packets named differently. It's the way we send and receive data in the FastCGI protocol.
Records have a type, which lets the FastCGI server know what kind of data is enclosed:
FCGI_BEGIN_REQUEST
FCGI_ABORT_REQUEST
FCGI_END_REQUEST
FCGI_PARAMS
FCGI_STDIN
FCGI_STDOUT
These are just names for integer constants. More on how these are used below.
Certain records such as FCGI_BEGIN_REQUEST
are sent as one record.
For longer content such as FCGI_PARAMS
(for environment variables/headers), or FCGI_STDIN
(for the request body), are sent as a stream. A stream is a sequence of records of the
same type. To end the stream, we send an empty record (zero content length) of
the same type.
Terminology
Web server: Your custom server, nginx, whatever is handling http
fpm: PHP-FPM
Record content will be denoted in the format:
{Record type, content}
Say we have an HTTP POST request with some data that we're forwarding to PHP-FPM. The flow would look like this.
Keep in mind this is not an exhaustive example. A lot more parameters will come from the browser
HTTP Request
1Content-Type: application/json2Content-Length: [whatever the length is]34{name: "Billie"}
FastCGI
1Web server: {FCGI_BEGIN_REQUEST, ""} 2 3Web server: {FCGI_PARAMS, "CONTENT_TYPE = application/json"} 4Web server: {FCGI_PARAMS, "CONTENT_LENGTH = ??"} 5Web server: {FCGI_PARAMS, ""} # End the stream 6 7Web server: {FCGI_STDIN, {name: "Billie"}} 8Web server: {FCGI_STDIN, ""} # End the stream 910# We will read this response11fpm: {FCGI_STDOUT, [this will be the http response from PHP-FPM]}12fpm: {FCGI_STDOUT, ""} # End the stream1314# After we're done reading the response, we turn it into15# an http response on our side and return it to the16# browser.