FastCGI implementation in OCaml

Add content truncation to pretty printer and enhance test output

Add optional max_content_len parameter to Record.pp (default 100 bytes) to handle large records gracefully. Enhance test output with hex dumps of raw file contents and detailed structured pretty printing of parsed records and key-value pairs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+8 -2
lib/fastcgi_record.ml
···
content : string;
}
-
let pp ppf record =
+
let pp ?(max_content_len=100) ppf record =
+
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)
+
in
Format.fprintf ppf
"@[<2>{ version = %d;@ record_type = %a;@ request_id = %d;@ content = %S }@]"
record.version
pp_record record.record_type
record.request_id
-
record.content
+
truncated_content
(* FastCGI constants *)
let fcgi_version_1 = 1
+3 -2
lib/fastcgi_record.mli
···
content : string; (** Record content data *)
}
-
(** [pp ppf record] pretty-prints a FastCGI record *)
-
val pp : Format.formatter -> t -> unit
+
(** [pp ?max_content_len ppf record] pretty-prints a FastCGI record.
+
[max_content_len] limits the displayed content length (default: 100 bytes) *)
+
val pp : ?max_content_len:int -> Format.formatter -> t -> unit
(** {1 Record Operations} *)
+61 -3
test/simple_test.ml
···
open Fastcgi
+
let hex_dump ?(max_bytes=256) content =
+
let len = String.length content in
+
let display_len = min len max_bytes in
+
let bytes_per_line = 16 in
+
let lines_to_show = (display_len - 1) / bytes_per_line + 1 in
+
+
for i = 0 to lines_to_show - 1 do
+
let start = i * bytes_per_line in
+
let end_ = min (start + bytes_per_line) display_len in
+
Printf.printf " %04x: " start;
+
+
(* Print hex bytes *)
+
for j = start to end_ - 1 do
+
Printf.printf "%02x " (Char.code content.[j]);
+
if j = start + 7 then Printf.printf " "
+
done;
+
+
(* Pad remaining space *)
+
for _ = end_ to start + bytes_per_line - 1 do
+
Printf.printf " ";
+
if end_ <= start + 7 then Printf.printf " "
+
done;
+
+
Printf.printf " |";
+
+
(* Print ASCII representation *)
+
for j = start to end_ - 1 do
+
let c = content.[j] in
+
if c >= ' ' && c <= '~' then
+
Printf.printf "%c" c
+
else
+
Printf.printf "."
+
done;
+
+
Printf.printf "|\n%!"
+
done;
+
+
if len > max_bytes then
+
Printf.printf " ... (%d more bytes truncated)\n%!" (len - max_bytes)
+
+
let test_record_types () =
Printf.printf "Testing record type conversions...\n%!";
···
let test_with_binary_test_case ~fs filename expected_type expected_request_id =
Printf.printf "Testing with binary test case: %s...\n%!" filename;
-
let parsed =
+
let (raw_content, parsed) =
Eio.Path.with_open_in (fs, "test_cases/" ^ filename) @@ fun flow ->
-
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
-
Record.read buf_read
+
let buf = Buffer.create 1024 in
+
Eio.Flow.copy flow (Eio.Flow.buffer_sink buf);
+
let raw_content = Buffer.contents buf in
+
let buf_read = Eio.Buf_read.of_string raw_content in
+
let parsed = Record.read buf_read in
+
(raw_content, parsed)
in
+
+
Printf.printf "\nRaw file contents (%d bytes):\n" (String.length raw_content);
+
hex_dump raw_content;
+
+
Printf.printf "\nParsed record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
+
(* If this is a Params record, also show the decoded key-value pairs *)
+
if parsed.record_type = Params && String.length parsed.content > 0 then (
+
let params = Record.KV.decode parsed.content in
+
Printf.printf "\nDecoded parameters:\n";
+
Format.printf "%a\n" Record.KV.pp params
+
);
assert (parsed.version = 1);
assert (parsed.record_type = expected_type);
+89 -13
test/validate_all_test_cases.ml
···
open Fastcgi
open Record
+
let hex_dump ?(max_bytes=256) content =
+
let len = String.length content in
+
let display_len = min len max_bytes in
+
let bytes_per_line = 16 in
+
let lines_to_show = (display_len - 1) / bytes_per_line + 1 in
+
+
for i = 0 to lines_to_show - 1 do
+
let start = i * bytes_per_line in
+
let end_ = min (start + bytes_per_line) display_len in
+
Printf.printf " %04x: " start;
+
+
(* Print hex bytes *)
+
for j = start to end_ - 1 do
+
Printf.printf "%02x " (Char.code content.[j]);
+
if j = start + 7 then Printf.printf " "
+
done;
+
+
(* Pad remaining space *)
+
for _ = end_ to start + bytes_per_line - 1 do
+
Printf.printf " ";
+
if end_ <= start + 7 then Printf.printf " "
+
done;
+
+
Printf.printf " |";
+
+
(* Print ASCII representation *)
+
for j = start to end_ - 1 do
+
let c = content.[j] in
+
if c >= ' ' && c <= '~' then
+
Printf.printf "%c" c
+
else
+
Printf.printf "."
+
done;
+
+
Printf.printf "|\n%!"
+
done;
+
+
if len > max_bytes then
+
Printf.printf " ... (%d more bytes truncated)\n%!" (len - max_bytes)
+
+
let test_cases = [
("abort_request.bin", Abort_request, 1);
("begin_request_authorizer.bin", Begin_request, 2);
···
]
let test_binary_file ~fs filename expected_type expected_request_id =
-
Printf.printf "Testing %s... " filename;
+
Printf.printf "Testing %s...\n" filename;
-
let parsed =
+
let (raw_content, parsed) =
Eio.Path.with_open_in (fs, "test_cases/" ^ filename) @@ fun flow ->
-
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
-
Record.read buf_read
+
let buf = Buffer.create 1024 in
+
Eio.Flow.copy flow (Eio.Flow.buffer_sink buf);
+
let raw_content = Buffer.contents buf in
+
let buf_read = Eio.Buf_read.of_string raw_content in
+
let parsed = Record.read buf_read in
+
(raw_content, parsed)
in
+
Printf.printf "\nRaw file contents (%d bytes):\n" (String.length raw_content);
+
hex_dump raw_content;
+
+
Printf.printf "\nParsed record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
+
(* If this is a Params record, also show the decoded key-value pairs *)
+
if parsed.record_type = Params && String.length parsed.content > 0 then (
+
let params = Record.KV.decode parsed.content in
+
Printf.printf "\nDecoded parameters:\n";
+
Format.printf "%a\n" Record.KV.pp params
+
);
+
assert (parsed.version = 1);
assert (parsed.record_type = expected_type);
assert (parsed.request_id = expected_request_id);
-
Printf.printf "✓\n%!"
+
Printf.printf "✓ %s passed\n\n%!" filename
let test_params_decoding ~fs =
-
Printf.printf "Testing params record content decoding... ";
+
Printf.printf "Testing params record content decoding...\n";
let parsed =
Eio.Path.with_open_in (fs, "test_cases/params_get.bin") @@ fun flow ->
···
Record.read buf_read
in
+
Printf.printf "\nParsed params record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
(* Decode the params content *)
let params = Record.KV.decode parsed.content in
+
Printf.printf "\nDecoded parameters:\n";
+
Format.printf "%a\n" Record.KV.pp params;
+
(* Check some expected environment variables *)
assert (Record.KV.find "REQUEST_METHOD" params = "GET");
assert (Record.KV.find "SERVER_NAME" params = "localhost");
assert (Record.KV.find "SERVER_PORT" params = "80");
-
Printf.printf "✓\n%!"
+
Printf.printf "✓ params decoding passed\n\n%!"
let test_large_record ~fs =
-
Printf.printf "Testing large record... ";
+
Printf.printf "Testing large record...\n";
let parsed =
Eio.Path.with_open_in (fs, "test_cases/large_record.bin") @@ fun flow ->
···
Record.read buf_read
in
+
Printf.printf "\nParsed large record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
+
assert (parsed.version = 1);
assert (parsed.record_type = Stdout);
assert (parsed.request_id = 1);
assert (String.length parsed.content = 65000);
-
Printf.printf "✓\n%!"
+
Printf.printf "✓ large record test passed\n\n%!"
let test_padded_record ~fs =
-
Printf.printf "Testing padded record... ";
+
Printf.printf "Testing padded record...\n";
let parsed =
Eio.Path.with_open_in (fs, "test_cases/padded_record.bin") @@ fun flow ->
let buf_read = Eio.Buf_read.of_flow flow ~max_size:1000000 in
Record.read buf_read
in
+
+
Printf.printf "\nParsed padded record:\n";
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) parsed;
assert (parsed.version = 1);
assert (parsed.record_type = Stdout);
assert (parsed.request_id = 1);
assert (parsed.content = "Hello");
-
Printf.printf "✓\n%!"
+
Printf.printf "✓ padded record test passed\n\n%!"
let test_multiplexed_records ~fs =
-
Printf.printf "Testing multiplexed records... ";
+
Printf.printf "Testing multiplexed records...\n";
let records =
Eio.Path.with_open_in (fs, "test_cases/multiplexed_requests.bin") @@ fun flow ->
···
let records = List.rev records in
+
Printf.printf "\nParsed %d multiplexed records:\n" (List.length records);
+
List.iteri (fun i record ->
+
Printf.printf "\nRecord %d:\n" (i + 1);
+
Format.printf "%a\n" (fun ppf -> Record.pp ppf) record
+
) records;
+
(* Should have multiple records with different request IDs *)
assert (List.length records > 5);
···
assert (List.mem 1 request_ids);
assert (List.mem 2 request_ids);
-
Printf.printf "✓\n%!"
+
Printf.printf "✓ multiplexed records test passed\n\n%!"
let run_all_tests ~fs =
Printf.printf "Validating all FastCGI test case files...\n\n%!";