FastCGI implementation in OCaml

Compare changes

Choose any two refs to compare.

+60 -13
bin/fcgi_server.ml
···
open Cmdliner
+
(* Handler function that processes FastCGI requests *)
+
let handler ~stdout ~stderr request =
+
Eio.traceln "Processing request: %a" Fastcgi.Request.pp request;
+
+
(* Get request parameters *)
+
let params = request.Fastcgi.Request.params in
+
let method_ = Fastcgi.Record.KV.find_opt "REQUEST_METHOD" params |> Option.value ~default:"GET" in
+
let uri = Fastcgi.Record.KV.find_opt "REQUEST_URI" params |> Option.value ~default:"/" in
+
let script_name = Fastcgi.Record.KV.find_opt "SCRIPT_NAME" params |> Option.value ~default:"" in
+
+
(* Log request info *)
+
Eio.traceln " Method: %s" method_;
+
Eio.traceln " URI: %s" uri;
+
Eio.traceln " Script: %s" script_name;
+
+
(* Generate simple HTTP response *)
+
let response_body =
+
Printf.sprintf
+
"<!DOCTYPE html>\n\
+
<html>\n\
+
<head><title>FastCGI OCaml Server</title></head>\n\
+
<body>\n\
+
<h1>FastCGI OCaml Server</h1>\n\
+
<p>Request processed successfully!</p>\n\
+
<ul>\n\
+
<li>Method: %s</li>\n\
+
<li>URI: %s</li>\n\
+
<li>Script: %s</li>\n\
+
</ul>\n\
+
<h2>All Parameters:</h2>\n\
+
<pre>%s</pre>\n\
+
</body>\n\
+
</html>\n"
+
method_ uri script_name
+
(let params_seq = Fastcgi.Record.KV.to_seq params in
+
let params_list = List.of_seq params_seq in
+
String.concat "\n" (List.map (fun (k, v) -> Printf.sprintf "%s = %s" k v) params_list))
+
in
+
+
(* Write HTTP response using FastCGI STDOUT records *)
+
let response_headers =
+
Printf.sprintf
+
"Status: 200 OK\r\n\
+
Content-Type: text/html; charset=utf-8\r\n\
+
Content-Length: %d\r\n\
+
\r\n"
+
(String.length response_body)
+
in
+
stdout response_headers;
+
stderr "stderr stuff";
+
stdout response_body
+
let run port =
Eio_main.run @@ fun env ->
Eio.Switch.run @@ fun sw ->
···
let addr = `Tcp (Eio.Net.Ipaddr.V4.loopback, port) in
let server_socket = Eio.Net.listen net ~backlog:10 ~reuse_addr:true ~sw addr in
Eio.traceln "FastCGI server listening on port %d" port;
-
Eio.Net.run_server server_socket ~on_error:(fun ex -> Eio.traceln "Error: %s" (Printexc.to_string ex))
-
@@ fun flow addr ->
-
Eio.traceln "Accepted connection from %a" Eio.Net.Sockaddr.pp addr;
-
(* Here you would handle the FastCGI protocol, but for simplicity, we just echo a string. *)
-
let req = Fastcgi.Request.read_request_from_flow ~sw flow in
-
match req with
-
| Error msg ->
-
Eio.traceln "Failed to read request: %s" msg;
-
Eio.Flow.close flow
-
| Ok req ->
-
Eio.traceln "Received request: %a" Fastcgi.Request.pp req;
-
Eio.Flow.close flow
+
+
(* Run the FastCGI server *)
+
Fastcgi.run server_socket
+
~on_error:(fun ex ->
+
Eio.traceln "Error: %s" (Printexc.to_string ex);
+
Eio.traceln "bt: %s" (Printexc.get_backtrace ()))
+
handler
let port =
let doc = "Port to listen on" in
···
let info = Cmd.info "fcgi-server" ~doc in
Cmd.v info Term.(const run $ port)
-
let () = exit (Cmd.eval cmd)
+
let () = exit (Cmd.eval cmd)
+7
config/Caddyfile
···
+
{
+
debug
+
}
+
+
localhost:80 {
+
php_fastcgi 127.0.0.1:9000
+
}
-3
lib/dune
···
fastcgi_record
fastcgi_request
)
-
(modules_without_implementation
-
fastcgi
-
)
)
+50
lib/fastcgi.ml
···
+
module Record = Fastcgi_record
+
+
(** Request-level state machine and application interface *)
+
module Request = Fastcgi_request
+
+
(* The lifetime of the handler is that the fiber should return when the
+
stdout and stderr flows are closed, or an abort request has been received *)
+
let handle req bw cancel fn =
+
let cancel () =
+
Eio.Promise.await cancel;
+
Eio.traceln "cancelled TODO"
+
in
+
let stdout buf = Request.write_stdout_records bw req.Request.request_id buf in
+
let stderr buf = Request.write_stderr_records bw req.Request.request_id buf in
+
let run () =
+
fn ~stdout ~stderr req;
+
Request.write_end_request bw req.Request.request_id 0 Request.Request_complete
+
in
+
Eio.Fiber.first run cancel
+
+
let run ?max_connections ?additional_domains ?stop ~on_error socket handler =
+
Eio.Net.run_server socket ?max_connections ?additional_domains ?stop ~on_error
+
(fun socket peer_address ->
+
let ids = Hashtbl.create 7 in
+
Eio.Switch.run @@ fun sw ->
+
Eio.traceln "%a: accept connection" Eio.Net.Sockaddr.pp peer_address;
+
let input = Eio.Buf_read.of_flow ~max_size:max_int socket in
+
Eio.Buf_write.with_flow socket @@ fun output ->
+
let cont = ref true in
+
try while !cont do
+
match Request.read_request input with
+
| Error msg ->
+
Eio.traceln "%a: failed to read request: %s" Eio.Net.Sockaddr.pp peer_address msg;
+
failwith "done";
+
| Ok req ->
+
cont := req.Request.keep_conn;
+
Eio.traceln "%a: %b read request %a" Eio.Net.Sockaddr.pp peer_address !cont Request.pp req;
+
Eio.Fiber.fork ~sw (fun () ->
+
Eio.Switch.run ~name:"req_handler" @@ fun sw ->
+
let cancel, canceler = Eio.Promise.create () in
+
Hashtbl.add ids req.Request.request_id canceler;
+
Eio.Switch.on_release sw (fun () ->
+
Hashtbl.remove ids req.Request.request_id
+
);
+
handle req output cancel handler;
+
);
+
done
+
with Eio.Io (Eio.Net.E (Connection_reset _), _) ->
+
Eio.traceln "%a: connection reset" Eio.Net.Sockaddr.pp peer_address
+
)
+17 -1
lib/fastcgi.mli
···
This library provides a complete implementation of the FastCGI protocol
for building high-performance web applications in OCaml. *)
+
(** {1 Core Protocol Components} *)
(** Record-level protocol handling *)
module Record = Fastcgi_record
(** Request-level state machine and application interface *)
-
module Request = Fastcgi_request
+
module Request = Fastcgi_request
+
+
(** {1 High-level Request Processing} *)
+
+
(** [handle_connection ~sw flow handler] handles complete FastCGI connection.
+
Reads requests from flow, processes them with handler, multiplexes responses.
+
Continues until connection is closed. *)
+
val run :
+
?max_connections:int ->
+
?additional_domains:[> Eio.Domain_manager.ty ] Eio.Resource.t *
+
int ->
+
?stop:'a Eio__core.Promise.t ->
+
on_error:(exn -> unit) ->
+
[> [> `Generic ] Eio.Net.listening_socket_ty ] Eio.Resource.t ->
+
(stdout:(string -> unit) ->
+
stderr:(string -> unit) -> Request.t -> unit) -> 'a
+27 -11
lib/fastcgi_record.ml
···
record_type : record;
request_id : request_id;
content : string;
+
offset : int;
+
length : int;
}
let pp ?(max_content_len=100) ppf record =
+
let actual_content = String.sub record.content record.offset record.length in
let truncated_content =
-
let content = record.content in
-
let len = String.length content in
-
if len <= max_content_len then content
-
else String.sub content 0 max_content_len ^ "..." ^ Printf.sprintf " (%d more bytes)" (len - max_content_len)
+
let len = String.length actual_content in
+
if len <= max_content_len then actual_content
+
else String.sub actual_content 0 max_content_len ^ "..." ^ Printf.sprintf " (%d more bytes)" (len - max_content_len)
in
Format.fprintf ppf
-
"@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S }@]"
+
"@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S;@ offset = %d;@ length = %d }@]"
record.version
pp_record record.record_type
record.request_id
truncated_content
+
record.offset
+
record.length
(* FastCGI constants *)
let fcgi_version_1 = 1
···
ignore (Eio.Buf_read.take padding_length buf_read)
);
-
let record = { version; record_type; request_id; content } in
+
let record = { version; record_type; request_id; content; offset = 0; length = String.length content } in
Printf.eprintf "[DEBUG] Fastcgi_record.read: Complete record = %s\n%!"
(Format.asprintf "%a" (pp ~max_content_len:50) record);
record
let write buf_write record =
-
let content_length = String.length record.content in
+
let total_content_length = String.length record.content in
+
let content_offset = record.offset in
+
let content_length = record.length in
+
+
(* Validate bounds *)
+
if content_offset < 0 || content_offset > total_content_length then
+
invalid_arg "Fastcgi_record.write: offset out of bounds";
+
if content_length < 0 || content_offset + content_length > total_content_length then
+
invalid_arg "Fastcgi_record.write: length out of bounds";
(* Calculate padding for 8-byte alignment *)
let padding_length = (8 - (content_length land 7)) land 7 in
···
Eio.Buf_write.string buf_write (Bytes.to_string header);
-
(* Write content *)
+
(* Write content with offset and length *)
if content_length > 0 then
-
Eio.Buf_write.string buf_write record.content;
+
Eio.Buf_write.string buf_write record.content ~off:content_offset ~len:content_length;
(* Write padding *)
if padding_length > 0 then
Eio.Buf_write.string buf_write (String.make padding_length '\000')
-
let create ~version ~record ~request_id ~content =
-
{ version; record_type = record; request_id; content }
+
let create ?(version=1) ~record ~request_id ~content ?(offset=0) ?length () =
+
let content_length = match length with
+
| None -> String.length content - offset
+
| Some l -> l
+
in
+
{ version; record_type = record; request_id; content; offset; length = content_length }
module KV = struct
type t = (string * string) list
+11 -7
lib/fastcgi_record.mli
···
content and optional padding for alignment. *)
type t = {
version : version; (** Protocol version (always 1) *)
-
record_type : record; (** Type of this record *)
+
record_type : record; (** Type of this record *)
request_id : request_id; (** Request identifier *)
content : string; (** Record content data *)
+
offset : int; (** Offset within content string (default: 0) *)
+
length : int; (** Length to use from content (default: String.length content) *)
}
(** [pp ?max_content_len ppf record] pretty-prints a FastCGI record.
···
(** [write buf_write record] writes a FastCGI record to the output buffer.
The record header is automatically constructed from the record fields,
and appropriate padding is added to align the record on 8-byte boundaries
-
for optimal performance. *)
+
for optimal performance. Uses the record's offset and length fields to
+
determine which portion of the content to write. *)
val write : Eio.Buf_write.t -> t -> unit
-
(** [create ~version ~record ~request_id ~content] creates a new record
-
with the specified parameters. The content length is automatically
-
calculated from the content string. *)
-
val create : version:version -> record:record ->
-
request_id:request_id -> content:string -> t
+
(** [create ?version ~record ~request_id ~content ?offset ?length] creates a new record
+
with the specified parameters. Version defaults to 1 (the only supported version).
+
If offset and length are not provided, the entire content string is used. *)
+
val create : ?version:version -> record:record ->
+
request_id:request_id -> content:string ->
+
?offset:int -> ?length:int -> unit -> t
(** {1 Key-Value Pairs} *)
+38 -156
lib/fastcgi_request.ml
···
String.length record.content = 0
-
let read_params_from_flow ~sw:_ buf_read =
+
let read_params buf_read =
Printf.eprintf "[DEBUG] read_params_from_flow: Starting\n%!";
let params = ref KV.empty in
let rec loop () =
···
in
loop ()
-
let read_stdin_from_flow ~sw:_ buf_read =
+
let read_stdin buf_read =
Printf.eprintf "[DEBUG] read_stdin_from_flow: Starting\n%!";
let data = Buffer.create 1024 in
let rec loop () =
···
loop ()
(** Read DATA stream for Filter role *)
-
let read_data_from_flow buf_read =
+
let read_data buf_read =
let data_buf = Buffer.create 1024 in
let rec read_data () =
try
···
read_data ()
(** Read request streams based on role *)
-
let read_request_streams ~sw request buf_read =
+
let read_request_streams request buf_read =
Printf.eprintf "[DEBUG] read_request_streams: Processing role=%s\n%!"
(Format.asprintf "%a" pp_role request.role);
match request.role with
···
Ok request
| Responder ->
Printf.eprintf "[DEBUG] read_request_streams: Responder role, reading STDIN\n%!";
-
let* stdin_data = read_stdin_from_flow ~sw buf_read in
+
let* stdin_data = read_stdin buf_read in
Printf.eprintf "[DEBUG] read_request_streams: Got STDIN data, %d bytes\n%!" (String.length stdin_data);
Ok { request with stdin_data }
| Filter ->
Printf.eprintf "[DEBUG] read_request_streams: Filter role, reading STDIN and DATA\n%!";
-
let* stdin_data = read_stdin_from_flow ~sw buf_read in
+
let* stdin_data = read_stdin buf_read in
Printf.eprintf "[DEBUG] read_request_streams: Got STDIN data, %d bytes\n%!" (String.length stdin_data);
let request = { request with stdin_data } in
-
let* data = read_data_from_flow buf_read in
+
let* data = read_data buf_read in
Printf.eprintf "[DEBUG] read_request_streams: Got DATA stream, %d bytes\n%!" (String.length data);
Ok { request with data_stream = Some data }
-
let read_request_from_flow ~sw flow =
-
Printf.eprintf "[DEBUG] read_request_from_flow: Starting\n%!";
-
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
+
let read_request buf_read =
+
Printf.eprintf "[DEBUG] read_request: Starting\n%!";
try
(* Read BEGIN_REQUEST *)
-
Printf.eprintf "[DEBUG] read_request_from_flow: Reading BEGIN_REQUEST record\n%!";
+
Printf.eprintf "[DEBUG] read_request: Reading BEGIN_REQUEST record\n%!";
let begin_record = Fastcgi_record.read buf_read in
-
Printf.eprintf "[DEBUG] read_request_from_flow: Got BEGIN_REQUEST record: %s\n%!"
+
Printf.eprintf "[DEBUG] read_request: Got BEGIN_REQUEST record: %s\n%!"
(Format.asprintf "%a" (Fastcgi_record.pp ~max_content_len:50) begin_record);
let* request = create begin_record in
-
Printf.eprintf "[DEBUG] read_request_from_flow: Created request with role=%s, id=%d\n%!"
+
Printf.eprintf "[DEBUG] read_request: Created request with role=%s, id=%d\n%!"
(Format.asprintf "%a" pp_role request.role) request.request_id;
(* Read PARAMS stream *)
-
Printf.eprintf "[DEBUG] read_request_from_flow: Reading PARAMS stream\n%!";
-
let* params = read_params_from_flow ~sw buf_read in
-
Printf.eprintf "[DEBUG] read_request_from_flow: Got %d params\n%!" (Fastcgi_record.KV.cardinal params);
+
Printf.eprintf "[DEBUG] read_request: Reading PARAMS stream\n%!";
+
let* params = read_params buf_read in
+
Printf.eprintf "[DEBUG] read_request: Got %d params\n%!" (Fastcgi_record.KV.cardinal params);
let request = { request with params } in
(* Read remaining streams based on role *)
-
Printf.eprintf "[DEBUG] read_request_from_flow: Reading streams for role=%s\n%!"
+
Printf.eprintf "[DEBUG] read_request: Reading streams for role=%s\n%!"
(Format.asprintf "%a" pp_role request.role);
-
let result = read_request_streams ~sw request buf_read in
-
Printf.eprintf "[DEBUG] read_request_from_flow: Finished reading request\n%!";
+
let result = read_request_streams request buf_read in
+
Printf.eprintf "[DEBUG] read_request: Finished reading request\n%!";
result
with
| End_of_file ->
-
Printf.eprintf "[DEBUG] read_request_from_flow: Hit End_of_file\n%!";
+
Printf.eprintf "[DEBUG] read_request: Hit End_of_file\n%!";
Error "Unexpected end of stream"
| exn ->
-
Printf.eprintf "[DEBUG] read_request_from_flow: Exception: %s\n%!" (Printexc.to_string exn);
+
Printf.eprintf "[DEBUG] read_request: Exception: %s\n%!" (Printexc.to_string exn);
Error (Printf.sprintf "Error reading request: %s" (Printexc.to_string exn))
(** {1 Response Generation} *)
···
| Overloaded -> 2
| Unknown_role -> 3
-
let stream_records_to_string records =
-
let buf = Buffer.create 1024 in
-
List.iter (fun record ->
-
if not (is_stream_terminator record) then
-
Buffer.add_string buf record.content
-
) records;
-
Buffer.contents buf
-
-
let string_to_stream_records ~request_id ~record_type content =
+
let write_stream_records buf_write request_id record_type content =
let max_chunk = 65535 in (* FastCGI max record content length *)
let len = String.length content in
-
let records = ref [] in
-
let rec chunk_string pos =
-
if pos >= len then
-
() (* Empty terminator will be added separately *)
-
else
+
if pos < len then begin
let chunk_len = min max_chunk (len - pos) in
-
let chunk = String.sub content pos chunk_len in
-
let record = Fastcgi_record.create ~version:1 ~record:record_type ~request_id ~content:chunk in
-
records := record :: !records;
+
let record = Fastcgi_record.create ~record:record_type ~request_id ~content ~offset:pos ~length:chunk_len () in
+
Fastcgi_record.write buf_write record;
chunk_string (pos + chunk_len)
+
end
in
-
chunk_string 0;
-
-
(* Add stream terminator *)
-
let terminator = Fastcgi_record.create ~version:1 ~record:record_type ~request_id ~content:"" in
-
records := terminator :: !records;
-
-
List.rev !records
+
let terminator = Fastcgi_record.create ~record:record_type ~request_id ~content:"" () in
+
Fastcgi_record.write buf_write terminator
-
let flow_to_stream_records ~sw:_ ~request_id ~record_type flow =
-
(* Read entire flow content *)
-
let buf = Buffer.create 4096 in
-
Eio.Flow.copy flow (Eio.Flow.buffer_sink buf);
-
let content = Buffer.contents buf in
-
string_to_stream_records ~request_id ~record_type content
+
let write_stdout_records buf_write request_id content =
+
Printf.eprintf "[DEBUG] write_stdout_records: Writing %d bytes for request_id=%d\n%!"
+
(String.length content) request_id;
+
write_stream_records buf_write request_id Stdout content
-
let write_stream_records records sink =
-
(* Create a function to serialize a single record to a string *)
-
let serialize_record record =
-
let buf = Buffer.create 512 in
-
let buf_sink = Eio.Flow.buffer_sink buf in
-
Eio.Buf_write.with_flow buf_sink (fun buf_write ->
-
Fastcgi_record.write buf_write record
-
);
-
Buffer.contents buf
-
in
-
-
(* Serialize all records and write to sink *)
-
List.iter (fun record ->
-
let serialized = serialize_record record in
-
Eio.Flow.copy_string serialized sink
-
) records
+
let write_stderr_records buf_write request_id content =
+
Printf.eprintf "[DEBUG] write_stderr_records: Writing %d bytes for request_id=%d\n%!"
+
(String.length content) request_id;
+
write_stream_records buf_write request_id Stderr content
-
let make_end_request ~request_id ~app_status ~protocol_status =
+
let write_end_request buf_write request_id app_status protocol_status =
let content =
let buf = Bytes.create 8 in
Bytes.set_int32_be buf 0 (Int32.of_int app_status);
···
Bytes.set_uint8 buf 7 0; (* reserved *)
Bytes.to_string buf
in
-
Fastcgi_record.create ~version:1 ~record:End_request ~request_id ~content
-
-
let write_response ~sw request ~stdout ~stderr sink app_status =
-
(* Convert stdout flow to STDOUT records *)
-
let stdout_records = flow_to_stream_records ~sw ~request_id:request.request_id ~record_type:Stdout stdout in
-
-
(* Convert stderr flow to STDERR records *)
-
let stderr_records = flow_to_stream_records ~sw ~request_id:request.request_id ~record_type:Stderr stderr in
-
-
(* Create END_REQUEST record *)
-
let end_record = make_end_request ~request_id:request.request_id ~app_status ~protocol_status:Request_complete in
-
-
(* Write all records *)
-
let all_records = stdout_records @ stderr_records @ [end_record] in
-
write_stream_records all_records sink
-
-
let write_error_response request sink proto_status =
-
let end_record = make_end_request ~request_id:request.request_id ~app_status:1 ~protocol_status:proto_status in
-
write_stream_records [end_record] sink
-
-
let write_abort_response request sink =
-
let end_record = make_end_request ~request_id:request.request_id ~app_status:0 ~protocol_status:Request_complete in
-
write_stream_records [end_record] sink
-
-
(** {1 High-level Request Processing} *)
-
-
type handler = t ->
-
stdout:Eio.Flow.sink_ty Eio.Resource.t ->
-
stderr:Eio.Flow.sink_ty Eio.Resource.t ->
-
app_status
-
-
let process_request ~sw request handler sink =
-
(* Create in-memory flows for stdout and stderr *)
-
let stdout_buf = Buffer.create 4096 in
-
let stderr_buf = Buffer.create 1024 in
-
let stdout_sink = Eio.Flow.buffer_sink stdout_buf in
-
let stderr_sink = Eio.Flow.buffer_sink stderr_buf in
-
-
(* Call handler *)
-
let app_status = handler request ~stdout:stdout_sink ~stderr:stderr_sink in
-
-
(* Convert buffers to sources and write response *)
-
let stdout_source = Eio.Flow.string_source (Buffer.contents stdout_buf) in
-
let stderr_source = Eio.Flow.string_source (Buffer.contents stderr_buf) in
-
-
write_response ~sw request ~stdout:stdout_source ~stderr:stderr_source sink app_status
-
-
let process_request_with_flows ~sw request ~stdout ~stderr sink app_status =
-
write_response ~sw request ~stdout ~stderr sink app_status
-
-
(** {1 Connection Management} *)
-
-
let handle_connection ~sw flow handler =
-
let _buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
-
let _buf_write = Eio.Buf_write.create 4096 in
-
-
let rec loop () =
-
try
-
(* Read next request *)
-
match read_request_from_flow ~sw flow with
-
| Error msg ->
-
(* Log error and continue or close connection *)
-
Printf.eprintf "Error reading request: %s\n%!" msg
-
| Ok request ->
-
(* Process request *)
-
let response_buf = Buffer.create 4096 in
-
let response_sink = Eio.Flow.buffer_sink response_buf in
-
-
process_request ~sw request handler response_sink;
-
-
(* Write response to connection *)
-
let response_data = Buffer.contents response_buf in
-
Eio.Flow.copy (Eio.Flow.string_source response_data) flow;
-
-
(* Continue if keep_conn is true *)
-
if request.keep_conn then
-
loop ()
-
with
-
| End_of_file -> () (* Connection closed *)
-
| exn ->
-
Printf.eprintf "Connection error: %s\n%!" (Printexc.to_string exn)
-
in
-
loop ()
-
-
let serve ~sw:_ ~backlog:_ ~port:_ _handler =
-
(* This would typically use Eio.Net to create a listening socket *)
-
(* For now, we'll provide a placeholder implementation *)
-
failwith "serve: Implementation requires Eio.Net integration"
+
let record = Fastcgi_record.create ~record:End_request ~request_id ~content () in
+
Fastcgi_record.write buf_write record
+23 -123
lib/fastcgi_request.mli
···
request_id : Fastcgi_record.request_id; (** Request identifier *)
role : role; (** Application role *)
keep_conn : bool; (** Connection keep-alive flag *)
-
params : Fastcgi_record.KV.t; (** Environment parameters *)
+
params : Fastcgi_record.KV.t; (** Environment parameters *)
stdin_data : string; (** Complete STDIN content *)
-
data_stream : string option; (** DATA stream for Filter role *)
+
data_stream : string option; (** DATA stream for Filter role *)
}
(** [pp ppf request] pretty-prints a request context *)
···
(** {1 Stream Processing} *)
-
(** [read_request_from_flow ~sw flow] reads a complete FastCGI request from flow.
-
Processes BEGIN_REQUEST, PARAMS, STDIN, and DATA records until complete.
-
Returns the populated request context. *)
-
val read_request_from_flow : sw:Eio.Switch.t -> 'a Eio.Flow.source -> (t, string) result
-
-
(** [read_params_from_flow ~sw buf_read] reads PARAMS stream from buf_read until empty record.
+
(** [read_params buf_read] reads PARAMS stream from buf_read until empty record.
Returns the accumulated parameters. *)
-
val read_params_from_flow : sw:Eio.Switch.t -> Eio.Buf_read.t -> (Fastcgi_record.KV.t, string) result
+
val read_params : Eio.Buf_read.t -> (Fastcgi_record.KV.t, string) result
-
(** [read_stdin_from_flow ~sw buf_read] reads STDIN stream from buf_read until empty record.
+
(** [read_stdin buf_read] reads STDIN stream from buf_read until empty record.
Returns the accumulated data. *)
-
val read_stdin_from_flow : sw:Eio.Switch.t -> Eio.Buf_read.t -> (string, string) result
+
val read_stdin : Eio.Buf_read.t -> (string, string) result
+
+
(** [read_data buf_read] reads DATA stream from buf_read until empty record.
+
Returns the accumulated data for Filter role. *)
+
val read_data : Eio.Buf_read.t -> (string, string) result
+
+
(** [read_request buf_read] reads a complete FastCGI request from buf_read.
+
Processes BEGIN_REQUEST, PARAMS, STDIN, and DATA records until complete.
+
Returns the populated request context. *)
+
val read_request : Eio.Buf_read.t -> (t, string) result
(** {1 Response Generation} *)
···
(** [pp_protocol_status ppf status] pretty-prints protocol status *)
val pp_protocol_status : Format.formatter -> protocol_status -> unit
-
(** [write_response ~sw request ~stdout ~stderr sink] writes FastCGI response.
-
Reads from stdout and stderr flows, converts to FastCGI records, and writes to sink.
-
Automatically handles stream termination and END_REQUEST. *)
-
val write_response :
-
sw:Eio.Switch.t ->
-
t ->
-
stdout:'a Eio.Flow.source ->
-
stderr:'a Eio.Flow.source ->
-
'a Eio.Flow.sink ->
-
app_status -> unit
-
-
(** [write_error_response request sink proto_status] writes error END_REQUEST record *)
-
val write_error_response : t -> 'a Eio.Flow.sink -> protocol_status -> unit
-
-
(** [write_abort_response request sink] writes END_REQUEST for aborted request *)
-
val write_abort_response : t -> 'a Eio.Flow.sink -> unit
-
-
(** {1 High-level Request Processing} *)
-
-
(** Request handler function type *)
-
type handler = t ->
-
stdout:Eio.Flow.sink_ty Eio.Resource.t ->
-
stderr:Eio.Flow.sink_ty Eio.Resource.t ->
-
app_status
-
-
(** [process_request ~sw request handler sink] processes complete request.
-
Calls handler with flows for stdout/stderr output, then writes response to sink. *)
-
val process_request :
-
sw:Eio.Switch.t ->
-
t ->
-
handler ->
-
Eio.Flow.sink_ty Eio.Resource.t -> unit
-
-
(** [process_request_with_flows ~sw request ~stdout ~stderr sink app_status]
-
processes request using provided output flows. *)
-
val process_request_with_flows :
-
sw:Eio.Switch.t ->
-
t ->
-
stdout:'a Eio.Flow.source ->
-
stderr:'a Eio.Flow.source ->
-
'a Eio.Flow.sink ->
-
app_status -> unit
-
-
(** {1 Connection Management} *)
-
-
(** [handle_connection ~sw flow handler] handles complete FastCGI connection.
-
Reads requests from flow, processes them with handler, multiplexes responses.
-
Continues until connection is closed. *)
-
val handle_connection :
-
sw:Eio.Switch.t ->
-
Eio.Flow.two_way_ty Eio.Resource.t ->
-
handler ->
-
unit
+
(** [write_stdout_records buf_write request_id content] writes STDOUT stream records.
+
Splits content into chunks and writes with terminator. *)
+
val write_stdout_records : Eio.Buf_write.t -> Fastcgi_record.request_id -> string -> unit
-
(** [serve ~sw ~backlog ~port handler] creates FastCGI server.
-
Listens on port, accepts connections, handles each with handler. *)
-
val serve :
-
sw:Eio.Switch.t ->
-
backlog:int ->
-
port:int ->
-
handler ->
-
unit
-
-
(** {1 Utilities} *)
-
-
(** [is_stream_terminator record] returns true if record terminates a stream *)
-
val is_stream_terminator : Fastcgi_record.t -> bool
-
-
(** [stream_records_to_string records] concatenates content from stream records *)
-
val stream_records_to_string : Fastcgi_record.t list -> string
+
(** [write_stderr_records buf_write request_id content] writes STDERR stream records.
+
Splits content into chunks and writes with terminator. *)
+
val write_stderr_records : Eio.Buf_write.t -> Fastcgi_record.request_id -> string -> unit
-
(** [string_to_stream_records ~request_id ~record_type content] converts string to stream records *)
-
val string_to_stream_records :
-
request_id:Fastcgi_record.request_id ->
-
record_type:Fastcgi_record.record ->
-
string -> Fastcgi_record.t list
-
-
(** [flow_to_stream_records ~sw ~request_id ~record_type flow] converts flow to stream records *)
-
val flow_to_stream_records :
-
sw:Eio.Switch.t ->
-
request_id:Fastcgi_record.request_id ->
-
record_type:Fastcgi_record.record ->
-
'a Eio.Flow.source -> Fastcgi_record.t list
-
-
(** [write_stream_records records sink] writes stream records to flow *)
-
val write_stream_records : Fastcgi_record.t list -> 'a Eio.Flow.sink -> unit
-
-
(** {1 Example Usage} *)
-
-
(** {2 Simple Handler}
-
{[
-
let my_handler request ~stdout ~stderr =
-
(* Write CGI headers *)
-
Eio.Flow.copy_string "Content-Type: text/html\r\n\r\n" stdout;
-
-
(* Generate response based on request.params *)
-
let method_ = Fastcgi_record.KV.find "REQUEST_METHOD" request.params in
-
Eio.Flow.copy_string ("<h1>Hello from " ^ method_ ^ "</h1>") stdout;
-
-
(* Optional error logging *)
-
if method_ = "POST" then
-
Eio.Flow.copy_string "Processing POST data\n" stderr;
-
-
0 (* Success status *)
-
]}
-
-
{2 Server Setup}
-
{[
-
let () =
-
Eio_main.run @@ fun env ->
-
Eio.Switch.run @@ fun sw ->
-
Fastcgi.Request.serve ~sw ~backlog:128 ~port:9000 my_handler
-
]}
-
*)
+
(** [write_end_request buf_write request_id app_status protocol_status] writes END_REQUEST record. *)
+
val write_end_request : Eio.Buf_write.t -> Fastcgi_record.request_id -> app_status -> protocol_status -> unit