API details for fastcgi

This library follows the FastCGI spec. It only supports the Responder role, and does not support multiplexing (which is not supported by any servers, so is unlikely to be an issue).

Enums

These enums are used throughout the library, and have the same meanings as the FCGI_ constants #defined in the spec.

for o in Record,Role,Status: print(list(o))
[<Record.BEGIN_REQUEST: 1>, <Record.ABORT_REQUEST: 2>, <Record.END_REQUEST: 3>, <Record.PARAMS: 4>, <Record.STDIN: 5>, <Record.STDOUT: 6>, <Record.STDERR: 7>, <Record.DATA: 8>, <Record.GET_VALUES: 9>, <Record.GET_VALUES_RESULT: 10>]
[<Role.RESPONDER: 1>, <Role.AUTHORIZER: 2>, <Role.FILTER: 3>]
[<Status.REQUEST_COMPLETE: 1>, <Status.CANT_MPX_CONN: 2>, <Status.OVERLOADED: 3>, <Status.UNKNOWN_ROLE: 4>]

readlen[source]

readlen(r)

Read the length of the next fcgi parameter

t = 1_000_000_101
s = struct.pack('!L', t | (1<<31))
test_eq(readlen(BytesIO(s).read), t)

recv_record[source]

recv_record(r)

Receive one fastcgi record from r

b = BytesIO(d).read
recv_record(b)

typ,p = recv_record(b); typ,p
(<Record.PARAMS: 4>,
 b'\x12\x10REQUEST_TIME_FLOAT1605811743685432\x0f\tPATH_TRANSLATED/setup.py\t\tPATH_INFO/setup.py\x13\x01HTTP_CONTENT_LENGTH4\x0e\x04REQUEST_METHODPOST\x0b\x03REMOTE_ADDR::1\x0b\x05REMOTE_PORT52102\x0b\tSCRIPT_NAME/setup.py\x0e\tORIG_PATH_INFO/setup.py\x0b\x03HTTP_ACCEPT*/*\x0f\x08SERVER_PROTOCOLHTTP/1.1\x0c\nREQUEST_TIME1605811743\x0c\x03QUERY_STRINGa=1\t\x0eHTTP_HOSTlocalhost:6065\x11\x10HTTP_CONTENT_TYPEapplication/json\x0f\tSERVER_SOFTWAREhttp2fcgi\r\tDOCUMENT_ROOT/setup.py\x0f\tSCRIPT_FILENAME/setup.py\x0b\rREQUEST_URI/setup.py?a=1\x0b\x00AUTH_DIGEST\x0f\x0bHTTP_USER_AGENTcurl/7.71.1')
recv_record(b)
(<Record.PARAMS: 4>, b'')

params[source]

params(s:bytes)

Parse fastcgi parameters from s

params(p)
{'REQUEST_TIME_FLOAT': '1605811743685432',
 'PATH_TRANSLATED': '/setup.py',
 'PATH_INFO': '/setup.py',
 'HTTP_CONTENT_LENGTH': '4',
 'REQUEST_METHOD': 'POST',
 'REMOTE_ADDR': '::1',
 'REMOTE_PORT': '52102',
 'SCRIPT_NAME': '/setup.py',
 'ORIG_PATH_INFO': '/setup.py',
 'HTTP_ACCEPT': '*/*',
 'SERVER_PROTOCOL': 'HTTP/1.1',
 'REQUEST_TIME': '1605811743',
 'QUERY_STRING': 'a=1',
 'HTTP_HOST': 'localhost:6065',
 'HTTP_CONTENT_TYPE': 'application/json',
 'SERVER_SOFTWARE': 'http2fcgi',
 'DOCUMENT_ROOT': '/setup.py',
 'SCRIPT_FILENAME': '/setup.py',
 'REQUEST_URI': '/setup.py?a=1',
 'AUTH_DIGEST': '',
 'HTTP_USER_AGENT': 'curl/7.71.1'}

send_record[source]

send_record(typ, w, c=b'')

Send fastcgi binary record of typ to w

send_stream[source]

send_stream(typ, w, s)

Send fastcgi stream of typ to w

class _Stream(BytesIO):
    def write(self, b):
        if isinstance(b,bytes): b=b.decode()
        super().write(b)

class FcgiHandler[source]

FcgiHandler(request, client_address, server) :: StreamRequestHandler

A request handler that processes FastCGI streams and parameters

This is used in much the same way as StreamRequestHandler, except that receiving the data is handled for you before your handle method is called. All headers are available in the params dictionary. All streams are available through indexing (i.e. using __getitem__). The stdin stream contains the data sent to your handler. Write to the stdout and stderr streams to send data to the client.

Here's an example subclass:

class TestHandler(FcgiHandler):
    def handle(self):
        print('query:', self.environ['QUERY_STRING'])
        print('content type:', self.environ.get('HTTP_CONTENT_TYPE', 'N/A'))
        print('stdin:', self['stdin'].read())
        self['stdout'].write(b"Content-type: text/html\r\n\r\n<html>foobar</html>\r\n")

To test it, we'll use an http➡fcgi proxy. We can download http2fcgi and run it in the background as follows:

run('./get_http2fcgi.sh')
proc = subprocess.Popen(['./http2fcgi'])

We can now test the handler by running a server in the background...

p = Path('fcgi.sock')
if p.exists(): p.unlink()

@threaded
def _f():
    with UnixStreamServer(str(p), TestHandler) as srv: srv.handle_request()
_f()
time.sleep(0.2) # wait for server to start

...and use curl to test it:

urlread('http://localhost:6065/setup.py?a=1', foo='bar', greeting='你好')
query: a=1
content type: application/x-www-form-urlencoded
stdin: b'foo=bar&greeting=%E4%BD%A0%E5%A5%BD'
b'<html>foobar</html>\r\n'

Finally, we kill the http2fcgi background process.

proc.terminate()

Convenience methods

class TextWrapper[source]

TextWrapper(buffer, encoding=None, errors=None, newline=None, line_buffering=False, write_through=False) :: TextIOWrapper

Character and line based layer over a BufferedIOBase object, buffer.

encoding gives the name of the encoding that the stream will be decoded or encoded with. It defaults to locale.getpreferredencoding(False).

errors determines the strictness of encoding and decoding (see help(codecs.Codec) or the documentation for codecs.register) and defaults to "strict".

newline controls how line endings are handled. It can be None, '', '\n', '\r', and '\r\n'. It works as follows:

  • On input, if newline is None, universal newlines mode is enabled. Lines in the input can end in '\n', '\r', or '\r\n', and these are translated into '\n' before being returned to the caller. If it is '', universal newline mode is enabled, but line endings are returned to the caller untranslated. If it has any of the other legal values, input lines are only terminated by the given string, and the line ending is returned to the caller untranslated.

  • On output, if newline is None, any '\n' characters written are translated to the system default line separator, os.linesep. If newline is '' or '\n', no translation takes place. If newline is any of the other legal values, any '\n' characters written are translated to the given string.

If line_buffering is True, a call to flush is implied when a call to write contains a newline character.

FcgiHandler.print[source]

FcgiHandler.print(s='')

Write a str to self.stdout as bytes, converting line endings to

Instead of self.stdout.write(...) (which requires byte strings and \r\n line endings, and does not append a line ending automatically) we can use print.

FcgiHandler.err[source]

FcgiHandler.err(s='')

Write a str to self.stderr as bytes, converting line endings to

For errors, you can either write to stderr, or call err, which is like print, but for stderr.

FcgiHandler.content[source]

FcgiHandler.content()

Contents of stdin

FcgiHandler.write[source]

FcgiHandler.write(b:bytes, err=False)

Write b to stderr (if err) or stdout (otherwise)

Here we repeat the previous example, but using some of these helper functions:

class TestHandler(FcgiHandler):
    def handle(self):
        print('stdin:', self.content())
        self.write(b"Content-type: text/html\r\n\r\n<html>foobar</html>\r\n")
        self.print("<html>foobar</html>")

proc = subprocess.Popen(['./http2fcgi'])
if p.exists(): p.unlink()
t = _f()
time.sleep(0.2)
print(urlread('http://localhost:6065/setup.py?a=1', foo='bar', greeting='你好'))
proc.terminate()
stdin: foo=bar&greeting=%E4%BD%A0%E5%A5%BD
b'<html>foobar</html>\r\n<html>foobar</html>\r\n'