···
+
# OCaml FastCGI Specification
+
This document specifies an OCaml interface for the FastCGI protocol using the Eio effects-based IO library. The design follows Eio conventions for structured concurrency, capability-based security, and resource management.
+
1. [Overview](#overview)
+
2. [Core Types](#core-types)
+
3. [Protocol Constants](#protocol-constants)
+
4. [Record Types](#record-types)
+
5. [Request/Response Model](#requestresponse-model)
+
6. [Application Interface](#application-interface)
+
7. [Connection Management](#connection-management)
+
8. [Role Implementations](#role-implementations)
+
9. [Error Handling](#error-handling)
+
10. [Resource Management](#resource-management)
+
11. [Examples](#examples)
+
The OCaml FastCGI implementation provides a high-level, type-safe interface for building FastCGI applications that can handle multiple concurrent requests efficiently. It leverages OCaml 5's effects system through Eio for structured concurrency and resource safety.
+
### Key Design Principles
+
- **Capability-based**: All external resources (network, filesystem) are explicitly passed as capabilities
+
- **Structured concurrency**: Use Eio switches for automatic resource cleanup
+
- **Type safety**: Leverage OCaml's type system to prevent protocol errors
+
- **Performance**: Support connection multiplexing and keep-alive for efficiency
+
- **Extensibility**: Support all three FastCGI roles with room for future extensions
+
(** FastCGI protocol version *)
+
(** FastCGI record types *)
+
| Responder (** Handle HTTP requests and generate responses *)
+
| Authorizer (** Perform authorization decisions *)
+
| Filter (** Process data streams with filtering *)
+
(** Request ID for multiplexing *)
+
(** Application status code *)
+
(** Protocol status codes *)
+
| Request_complete (** Normal completion *)
+
| Cant_mpx_conn (** Cannot multiplex connection *)
+
| Overloaded (** Application overloaded *)
+
| Unknown_role (** Unknown role requested *)
+
(** Connection flags *)
+
type connection_flags = {
+
keep_conn : bool; (** Keep connection open after request *)
+
module Constants = struct
+
(** Protocol version *)
+
(** Standard file descriptor for FastCGI listening socket *)
+
let listensock_fileno = 0
+
(** Record type constants *)
+
let fcgi_begin_request = 1
+
let fcgi_abort_request = 2
+
let fcgi_end_request = 3
+
let fcgi_get_values = 9
+
let fcgi_get_values_result = 10
+
let fcgi_unknown_type = 11
+
let fcgi_authorizer = 2
+
let max_content_length = 65535
+
let max_padding_length = 255
+
(** Management record variables *)
+
let fcgi_max_conns = "FCGI_MAX_CONNS"
+
let fcgi_max_reqs = "FCGI_MAX_REQS"
+
let fcgi_mpxs_conns = "FCGI_MPXS_CONNS"
+
(** FastCGI record header *)
+
record_type : record_type;
+
request_id : request_id;
+
(** Begin request record body *)
+
type begin_request_body = {
+
flags : connection_flags;
+
(** End request record body *)
+
type end_request_body = {
+
app_status : app_status;
+
protocol_status : protocol_status;
+
(** Complete FastCGI record *)
+
header : record_header;
+
padding : bytes option;
+
(** Name-value pair for parameters *)
+
type name_value_pair = {
+
## Request/Response Model
+
(** Request context containing all information for a FastCGI request *)
+
request_id : request_id;
+
flags : connection_flags;
+
params : (string * string) list;
+
stdin : 'a Eio.Flow.source;
+
data : 'a Eio.Flow.source option; (** Only for Filter role *)
+
(** Response builder for constructing FastCGI responses *)
+
stdout : 'a Eio.Flow.sink;
+
stderr : 'a Eio.Flow.sink;
+
(** Complete response with status *)
+
type response_result = {
+
app_status : app_status;
+
protocol_status : protocol_status;
+
## Application Interface
+
(** Application handler signature *)
+
module Handler = struct
+
(** Responder handler: process HTTP request and generate response *)
+
type 'a responder = 'a request -> 'a response -> response_result
+
(** Authorizer handler: make authorization decision *)
+
type 'a authorizer = 'a request -> 'a response -> response_result
+
(** Filter handler: process data stream with filtering *)
+
type 'a filter = 'a request -> 'a response -> response_result
+
(** Generic handler that can handle any role *)
+
| Responder of 'a responder
+
| Authorizer of 'a authorizer
+
(** Application configuration *)
+
max_connections : int; (** Maximum concurrent connections *)
+
max_requests : int; (** Maximum concurrent requests *)
+
multiplex_connections : bool; (** Support connection multiplexing *)
+
handler : 'a Handler.handler; (** Application request handler *)
+
## Connection Management
+
(** Connection manager for handling FastCGI protocol *)
+
module Connection = struct
+
(** Opaque connection type *)
+
(** Create a connection from a network flow *)
+
val create : sw:Eio.Switch.t -> 'a Eio.Flow.two_way -> 'a t
+
(** Accept and process a single request on the connection *)
+
val process_request : 'a t -> 'a Handler.handler -> response_result Eio.Promise.t
+
(** Get connection statistics *)
+
(** Close the connection gracefully *)
+
val close : 'a t -> unit
+
(** FastCGI server for accepting and managing connections *)
+
(** Server configuration *)
+
| `Unix of string (** Unix domain socket path *)
+
| `Tcp of Eio.Net.Ipaddr.t * int (** TCP address and port *)
+
backlog : int; (** Listen backlog size *)
+
max_connections : int; (** Maximum concurrent connections *)
+
(** Run a FastCGI server *)
+
(** Run server with default configuration *)
+
handler:'a Handler.handler ->
+
listen_address:[`Unix of string | `Tcp of Eio.Net.Ipaddr.t * int] ->
+
## Role Implementations
+
(** Responder role implementation *)
+
module Responder = struct
+
(** CGI-style environment variables *)
+
type cgi_env = (string * string) list
+
(** HTTP request information *)
+
method_ : string; (** HTTP method (GET, POST, etc.) *)
+
uri : string; (** Request URI *)
+
query_string : string; (** Query string parameters *)
+
content_type : string option; (** Content-Type header *)
+
content_length : int option; (** Content-Length header *)
+
headers : (string * string) list; (** Additional HTTP headers *)
+
body : 'a Eio.Flow.source; (** Request body stream *)
+
(** HTTP response builder *)
+
type 'a http_response = {
+
write_status : int -> unit; (** Set HTTP status code *)
+
write_header : string -> string -> unit; (** Add response header *)
+
write_body : string -> unit; (** Write response body *)
+
write_body_chunk : bytes -> unit; (** Write body chunk *)
+
finish : unit -> unit; (** Complete response *)
+
(** Convert FastCGI request to HTTP request *)
+
val request_of_fastcgi : 'a request -> http_request
+
(** Create HTTP response writer from FastCGI response *)
+
val response_of_fastcgi : 'a response -> 'a http_response
+
(** Convenience handler wrapper for HTTP-style handlers *)
+
(http_request -> 'a http_response -> unit) ->
+
(** Authorizer role implementation *)
+
module Authorizer = struct
+
(** Authorization result *)
+
| Authorized of (string * string) list (** Authorized with variable bindings *)
+
| Unauthorized of { (** Unauthorized with error response *)
+
headers : (string * string) list;
+
(** Authorization request information *)
+
remote_addr : string option;
+
remote_user : string option;
+
auth_type : string option;
+
headers : (string * string) list;
+
(** Convert FastCGI request to authorization request *)
+
val request_of_fastcgi : 'a request -> auth_request
+
(** Convert authorization result to FastCGI response *)
+
val response_of_result : auth_result -> 'a response -> response_result
+
(** Convenience handler wrapper for authorization handlers *)
+
(auth_request -> auth_result) ->
+
(** Filter role implementation *)
+
(** Filter request with data stream *)
+
type 'a filter_request = {
+
request : 'a request; (** Base FastCGI request *)
+
data_stream : 'a Eio.Flow.source; (** File data to filter *)
+
data_last_modified : float option; (** File modification time *)
+
data_length : int option; (** Expected data length *)
+
(** Convert FastCGI request to filter request *)
+
val request_of_fastcgi : 'a request -> 'a filter_request
+
(** Convenience handler wrapper for filter handlers *)
+
('a filter_request -> 'a response -> unit) ->
+
(** FastCGI specific errors *)
+
| Protocol_error of string (** Protocol violation *)
+
| Invalid_record of string (** Malformed record *)
+
| Unsupported_version of int (** Unsupported protocol version *)
+
| Unknown_record_type of int (** Unknown record type *)
+
| Request_id_conflict of request_id (** Duplicate request ID *)
+
| Connection_closed (** Connection unexpectedly closed *)
+
| Application_error of string (** Application-specific error *)
+
exception Fastcgi_error of t
+
(** Convert error to string description *)
+
val to_string : t -> string
+
(** Raise a FastCGI error *)
+
(** Resource management utilities *)
+
module Resource = struct
+
(** Request context with automatic cleanup *)
+
type 'a request_context = {
+
response : 'a response;
+
switch : Eio.Switch.t; (** Switch for request-scoped resources *)
+
(** Create a request context with automatic resource management *)
+
val with_request_context :
+
('a request_context -> 'b) ->
+
(** Buffer management for efficient I/O *)
+
val create : size:int -> t
+
val read_into : t -> 'a Eio.Flow.source -> int
+
val write_from : t -> 'a Eio.Flow.sink -> int -> unit
+
### Basic Responder Application
+
let hello_handler request response =
+
let http_req = Responder.request_of_fastcgi request in
+
let http_resp = Responder.response_of_fastcgi response in
+
http_resp.write_status 200;
+
http_resp.write_header "Content-Type" "text/html";
+
http_resp.write_body "<h1>Hello, FastCGI!</h1>";
+
{ app_status = 0; protocol_status = Request_complete }
+
let net = Eio.Stdenv.net env in
+
multiplex_connections = true;
+
handler = Handler.Responder hello_handler;
+
listen_address = `Tcp (Eio.Net.Ipaddr.V4.loopback, 9000);
+
Switch.run @@ fun sw ->
+
Server.run ~sw ~net config
+
let () = Eio_main.run run_server
+
### File-serving Responder
+
let file_server_handler ~cwd request response =
+
let http_req = Responder.request_of_fastcgi request in
+
let http_resp = Responder.response_of_fastcgi response in
+
let path = Eio.Path.(cwd / String.drop_prefix http_req.uri 1) in
+
match Eio.Path.load path with
+
http_resp.write_status 200;
+
http_resp.write_header "Content-Type" "text/html";
+
http_resp.write_body content;
+
| exception (Eio.Io (Eio.Fs.E (Not_found _), _)) ->
+
http_resp.write_status 404;
+
http_resp.write_header "Content-Type" "text/plain";
+
http_resp.write_body "File not found";
+
http_resp.write_status 500;
+
http_resp.write_header "Content-Type" "text/plain";
+
http_resp.write_body ("Server error: " ^ Printexc.to_string ex);
+
{ app_status = 0; protocol_status = Request_complete }
+
let run_file_server env =
+
let net = Eio.Stdenv.net env in
+
let cwd = Eio.Stdenv.cwd env in
+
let handler = file_server_handler ~cwd in
+
Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net ~handler:(Handler.Responder handler)
+
~listen_address:(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9000))
+
### Authorization Application
+
let auth_handler auth_req =
+
match auth_req.remote_user with
+
| Some user when String.starts_with user "admin_" ->
+
Authorizer.Authorized [("AUTH_USER", user); ("AUTH_LEVEL", "admin")]
+
Authorizer.Authorized [("AUTH_USER", user); ("AUTH_LEVEL", "user")]
+
Authorizer.Unauthorized {
+
headers = [("WWW-Authenticate", "Basic realm=\"FastCGI\"")];
+
body = "Authentication required";
+
let run_auth_server env =
+
let net = Eio.Stdenv.net env in
+
let handler = Authorizer.auth_handler auth_handler in
+
Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net ~handler:(Handler.Authorizer handler)
+
~listen_address:(`Unix "/tmp/auth.sock")
+
let markdown_filter filter_req response =
+
let data = Eio.Flow.read_all filter_req.data_stream in
+
let html = Markdown.to_html data in (* Assuming markdown library *)
+
Eio.Flow.copy_string "Content-Type: text/html\r\n\r\n" response.stdout;
+
Eio.Flow.copy_string html response.stdout;
+
{ app_status = 0; protocol_status = Request_complete }
+
let run_filter_server env =
+
let net = Eio.Stdenv.net env in
+
let handler = Filter.filter_handler markdown_filter in
+
Switch.run @@ fun sw ->
+
Server.run_default ~sw ~net ~handler:(Handler.Filter handler)
+
~listen_address:(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9001))
+
### Connection Management Example
+
let custom_connection_handler env =
+
let net = Eio.Stdenv.net env in
+
Switch.run @@ fun sw ->
+
let server_socket = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:5
+
(`Tcp (Eio.Net.Ipaddr.V4.loopback, 9000)) in
+
let handler = Handler.Responder hello_handler in
+
Eio.Net.run_server server_socket (fun flow _addr ->
+
Switch.run @@ fun conn_sw ->
+
let conn = Connection.create ~sw:conn_sw flow in
+
let rec process_loop () =
+
match Connection.process_request conn handler with
+
| response -> process_loop ()
+
| exception End_of_file -> ()
+
Eio.traceln "Connection error: %s" (Printexc.to_string ex)
+
## Implementation Notes
+
### Wire Protocol Details
+
The implementation must handle:
+
1. **Binary record format**: 8-byte headers with version, type, request ID, content length, and padding
+
2. **Name-value pair encoding**: Variable-length encoding for parameter names and values
+
3. **Stream management**: Proper handling of stdin, stdout, stderr, and data streams
+
4. **Connection multiplexing**: Support multiple concurrent requests over single connection
+
5. **Error propagation**: Convert protocol errors to appropriate OCaml exceptions
+
### Performance Considerations
+
1. **Buffer management**: Reuse buffers to minimize allocations
+
2. **Streaming I/O**: Process requests in streaming fashion for large payloads
+
3. **Connection pooling**: Support keep-alive connections for efficiency
+
4. **Concurrent processing**: Use Eio fibers for handling multiple requests
+
### Security Considerations
+
1. **Input validation**: Validate all protocol fields and reject malformed records
+
2. **Resource limits**: Enforce limits on request size, connection count, etc.
+
3. **Capability restriction**: Use Eio's capability system to limit access
+
4. **Error information**: Avoid leaking sensitive information in error messages
+
The implementation should include:
+
1. **Protocol conformance tests**: Verify correct implementation of FastCGI protocol
+
2. **Interoperability tests**: Test with real web servers (nginx, Apache)
+
3. **Performance tests**: Measure throughput and latency under load
+
4. **Error handling tests**: Verify graceful handling of error conditions
+
5. **Mock testing**: Use Eio_mock for deterministic unit tests
+
This specification provides a comprehensive foundation for implementing a robust, efficient, and type-safe FastCGI library for OCaml using the Eio effects system.