Jan 27 2025

FastCGI Protocol Implementation


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.

The FastCGI protocol, explanation for humans

Original FastCGI Spec

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.

What we're sending to PHP-FPM

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.

HTTP 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 Actual Protocol

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

"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:

These are just names for integer constants. More on how these are used below.

Streams vs single records

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.

Request Flow

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/json
2Content-Length: [whatever the length is]
3
4{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
9
10# We will read this response
11fpm: {FCGI_STDOUT, [this will be the http response from PHP-FPM]}
12fpm: {FCGI_STDOUT, ""} # End the stream
13
14# After we're done reading the response, we turn it into
15# an http response on our side and return it to the
16# browser.