ocaml http/1, http/2 and websocket client and server library

fix: HTTP/2 large body handling and flow control

- Fix H2 server read buffer tracking for bodies >16KB
- Track buf_off/buf_len for partial consumption
- Handle Yield state in read loop
- Compress buffer after partial reads

- Fix H2 client body flushing before close
- Use Body.Writer.flush with callback before closing
- Ensures all data is sent before marking stream done

- Add comprehensive large body test suite
- HTTP/1.1: 1KB-5MB POST, integrity, max_body_size
- HTTP/2 h2c: 1KB-5MB POST, integrity
- WebSocket: 1KB-64KB messages, integrity

- Add benchmark comparison tools
- bench_compare.ml: Compare results across runs
- bench_parser.ml: Parse benchmark output

All tests pass with default H2 window size (65535 bytes),
confirming proper WINDOW_UPDATE flow control.

+268
bench/hcs/bench_compare.ml
··· 1 + open Base 2 + 3 + module Bench = struct 4 + [@@@warning "-69"] 5 + 6 + type result = { 7 + name : string; 8 + iterations : int; 9 + total_time : float; 10 + avg_time_ns : float; 11 + ops_per_sec : float; 12 + minor_words_per_op : float; 13 + major_words_per_op : float; 14 + promoted_words_per_op : float; 15 + } 16 + 17 + let run ~name ~iterations f = 18 + for _ = 1 to Int.min 1000 iterations do 19 + ignore (f ()) 20 + done; 21 + Stdlib.Gc.full_major (); 22 + Stdlib.Gc.compact (); 23 + let gc_before = Stdlib.Gc.quick_stat () in 24 + let start = Unix.gettimeofday () in 25 + for _ = 1 to iterations do 26 + ignore (f ()) 27 + done; 28 + let stop = Unix.gettimeofday () in 29 + let gc_after = Stdlib.Gc.quick_stat () in 30 + let total_time = stop -. start in 31 + let avg_time_ns = 32 + total_time /. Float.of_int iterations *. 1_000_000_000.0 33 + in 34 + let ops_per_sec = Float.of_int iterations /. total_time in 35 + let iters = Float.of_int iterations in 36 + let minor_words_per_op = 37 + (gc_after.minor_words -. gc_before.minor_words) /. iters 38 + in 39 + let major_words_per_op = 40 + (gc_after.major_words -. gc_before.major_words) /. iters 41 + in 42 + let promoted_words_per_op = 43 + (gc_after.promoted_words -. gc_before.promoted_words) /. iters 44 + in 45 + { 46 + name; 47 + iterations; 48 + total_time; 49 + avg_time_ns; 50 + ops_per_sec; 51 + minor_words_per_op; 52 + major_words_per_op; 53 + promoted_words_per_op; 54 + } 55 + 56 + let total_alloc r = r.minor_words_per_op +. r.major_words_per_op 57 + 58 + let print_result r = 59 + Stdio.printf " %-35s %8.0f ns/op %10.0f ops/s %6.0f words/op\n" r.name 60 + r.avg_time_ns r.ops_per_sec (total_alloc r) 61 + 62 + let print_comparison ~baseline ~test = 63 + let speedup = baseline.avg_time_ns /. test.avg_time_ns in 64 + let alloc_ratio = 65 + total_alloc baseline /. Float.max 1.0 (total_alloc test) 66 + in 67 + let faster_slower = if Float.(speedup > 1.0) then "faster" else "slower" in 68 + Stdio.printf " -> %s is %.2fx %s, %.1fx %s allocations\n\n" test.name 69 + speedup faster_slower alloc_ratio 70 + (if Float.(alloc_ratio > 1.0) then "fewer" else "more") 71 + end 72 + 73 + let small_request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" 74 + 75 + let medium_request = 76 + "GET /index.html HTTP/1.1\r\n\ 77 + Host: www.example.com\r\n\ 78 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \ 79 + AppleWebKit/537.36\r\n\ 80 + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\ 81 + Accept-Language: en-US,en;q=0.9\r\n\ 82 + Accept-Encoding: gzip, deflate, br\r\n\ 83 + Connection: keep-alive\r\n\ 84 + Cache-Control: max-age=0\r\n\ 85 + Cookie: session=abc123; tracking=xyz789\r\n\ 86 + If-None-Match: \"abc123\"\r\n\ 87 + If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT\r\n\ 88 + \r\n" 89 + 90 + let large_request = 91 + "POST /api/v1/users/12345/documents HTTP/1.1\r\n\ 92 + Host: api.example.com\r\n\ 93 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \ 94 + AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36\r\n\ 95 + Accept: application/json, text/plain, */*\r\n\ 96 + Accept-Language: en-US,en;q=0.9,de;q=0.8,fr;q=0.7\r\n\ 97 + Accept-Encoding: gzip, deflate, br\r\n\ 98 + Content-Type: application/json; charset=utf-8\r\n\ 99 + Content-Length: 256\r\n\ 100 + Authorization: Bearer \ 101 + eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c\r\n\ 102 + X-Request-ID: 550e8400-e29b-41d4-a716-446655440000\r\n\ 103 + X-Correlation-ID: 7c9e6679-7425-40de-944b-e07fc1f90ae7\r\n\ 104 + X-Forwarded-For: 192.168.1.1, 10.0.0.1\r\n\ 105 + X-Forwarded-Proto: https\r\n\ 106 + X-Forwarded-Host: www.example.com\r\n\ 107 + Origin: https://www.example.com\r\n\ 108 + Referer: https://www.example.com/dashboard\r\n\ 109 + Cookie: session=abc123def456; csrf_token=xyz789; \ 110 + preferences=dark_mode%3Dtrue%26lang%3Den; _ga=GA1.2.123456789.1234567890\r\n\ 111 + Cache-Control: no-cache, no-store, must-revalidate\r\n\ 112 + Pragma: no-cache\r\n\ 113 + Connection: keep-alive\r\n\ 114 + Sec-Fetch-Dest: empty\r\n\ 115 + Sec-Fetch-Mode: cors\r\n\ 116 + Sec-Fetch-Site: same-origin\r\n\ 117 + \r\n" 118 + 119 + let buffer_size = 16384 120 + 121 + module H1_bench = struct 122 + let h1_buf = Bigstringaf.create buffer_size 123 + 124 + let copy_to_buffer data = 125 + let len = String.length data in 126 + Bigstringaf.blit_from_string data ~src_off:0 h1_buf ~dst_off:0 ~len; 127 + len 128 + 129 + let parse_request data = 130 + let len = copy_to_buffer data in 131 + let request = ref None in 132 + let request_handler reqd = 133 + let req = H1.Reqd.request reqd in 134 + request := Some req; 135 + let resp = H1.Response.create `OK in 136 + H1.Reqd.respond_with_string reqd resp "" 137 + in 138 + let error_handler ?request:_ _error _handler = () in 139 + let conn = H1.Server_connection.create ~error_handler request_handler in 140 + let _ = H1.Server_connection.read conn h1_buf ~off:0 ~len in 141 + let _ = !request in 142 + () 143 + end 144 + 145 + module Eio_bufread_bench = struct 146 + let parse_request data = 147 + let reader = Eio.Buf_read.of_string data in 148 + let line = Eio.Buf_read.line reader in 149 + let parts = String.split line ~on:' ' in 150 + let _meth = List.hd_exn parts in 151 + let _target = List.nth_exn parts 1 in 152 + let _version = List.nth_exn parts 2 in 153 + let headers = ref [] in 154 + let rec read_headers () = 155 + let header_line = Eio.Buf_read.line reader in 156 + if String.is_empty header_line then () 157 + else begin 158 + (match String.lsplit2 header_line ~on:':' with 159 + | Some (k, v) -> headers := (k, String.lstrip v) :: !headers 160 + | None -> ()); 161 + read_headers () 162 + end 163 + in 164 + read_headers (); 165 + let _ = !headers in 166 + () 167 + end 168 + 169 + let run_size_benchmark ~name ~data ~iterations = 170 + Stdio.printf "\n%s (%d bytes, %d iterations)\n" name (String.length data) 171 + iterations; 172 + Stdio.print_string (String.make 60 '-'); 173 + Stdio.print_endline ""; 174 + let h1_result = 175 + Bench.run ~name:"h1 (HCS)" ~iterations (fun () -> 176 + H1_bench.parse_request data) 177 + in 178 + Bench.print_result h1_result; 179 + let bufread_result = 180 + Bench.run ~name:"Buf_read (Eio)" ~iterations (fun () -> 181 + Eio_bufread_bench.parse_request data) 182 + in 183 + Bench.print_result bufread_result; 184 + Bench.print_comparison ~baseline:bufread_result ~test:h1_result; 185 + (h1_result, bufread_result) 186 + 187 + let run_throughput_benchmark () = 188 + Stdio.printf "\n\nTHROUGHPUT TEST (1000 iterations per call)\n"; 189 + Stdio.print_string (String.make 60 '='); 190 + Stdio.print_endline ""; 191 + let iterations = 1000 in 192 + let outer_iterations = 1000 in 193 + Stdio.printf "\nSmall request throughput:\n"; 194 + let h1_tp = 195 + Bench.run ~name:"h1 throughput" ~iterations:outer_iterations (fun () -> 196 + for _ = 1 to iterations do 197 + ignore (H1_bench.parse_request small_request) 198 + done) 199 + in 200 + let h1_total_ops = Float.of_int (outer_iterations * iterations) in 201 + let h1_ops_per_sec = h1_total_ops /. h1_tp.total_time in 202 + Stdio.printf " h1: %.0f requests/sec\n" h1_ops_per_sec; 203 + let bufread_tp = 204 + Bench.run ~name:"Buf_read throughput" ~iterations:outer_iterations 205 + (fun () -> 206 + for _ = 1 to iterations do 207 + ignore (Eio_bufread_bench.parse_request small_request) 208 + done) 209 + in 210 + let bufread_total_ops = Float.of_int (outer_iterations * iterations) in 211 + let bufread_ops_per_sec = bufread_total_ops /. bufread_tp.total_time in 212 + Stdio.printf " Buf_read: %.0f requests/sec\n" bufread_ops_per_sec; 213 + let ratio = h1_ops_per_sec /. bufread_ops_per_sec in 214 + Stdio.printf " -> h1 is %.2fx %s\n" ratio 215 + (if Float.(ratio > 1.0) then "faster" else "slower") 216 + 217 + let print_summary results = 218 + Stdio.printf "\n\nSUMMARY\n"; 219 + Stdio.print_string (String.make 60 '='); 220 + Stdio.print_endline ""; 221 + Stdio.printf "\n%-15s %12s %12s %10s %10s\n" "Size" "h1 ns/op" 222 + "Buf_read ns/op" "Speedup" "Alloc Ratio"; 223 + Stdio.print_string (String.make 60 '-'); 224 + Stdio.print_endline ""; 225 + List.iter results ~f:(fun (name, (h1, bufread)) -> 226 + let speedup = bufread.Bench.avg_time_ns /. h1.Bench.avg_time_ns in 227 + let alloc_ratio = 228 + Bench.total_alloc bufread /. Float.max 1.0 (Bench.total_alloc h1) 229 + in 230 + Stdio.printf "%-15s %12.0f %12.0f %9.2fx %9.1fx\n" name h1.avg_time_ns 231 + bufread.avg_time_ns speedup alloc_ratio) 232 + 233 + let () = 234 + Stdio.print_endline ""; 235 + Stdio.print_endline 236 + "============================================================"; 237 + Stdio.print_endline " HTTP Parser Comparison: h1 (HCS) vs Buf_read (Eio)"; 238 + Stdio.print_endline 239 + "============================================================"; 240 + Stdio.print_endline ""; 241 + Stdio.print_endline "h1: State-machine parser used by HCS server"; 242 + Stdio.print_endline "Buf_read: Simple line-based parsing using Eio"; 243 + Stdio.print_endline ""; 244 + let small = 245 + ( "Small", 246 + run_size_benchmark ~name:"SMALL REQUEST" ~data:small_request 247 + ~iterations:500_000 ) 248 + in 249 + let medium = 250 + ( "Medium", 251 + run_size_benchmark ~name:"MEDIUM REQUEST" ~data:medium_request 252 + ~iterations:200_000 ) 253 + in 254 + let large = 255 + ( "Large", 256 + run_size_benchmark ~name:"LARGE REQUEST" ~data:large_request 257 + ~iterations:100_000 ) 258 + in 259 + let results = [ small; medium; large ] in 260 + run_throughput_benchmark (); 261 + print_summary results; 262 + Stdio.print_endline ""; 263 + Stdio.print_endline 264 + "============================================================"; 265 + Stdio.print_endline " Benchmark Complete"; 266 + Stdio.print_endline 267 + "============================================================"; 268 + Stdio.print_endline ""
+294
bench/hcs/bench_parser.ml
··· 1 + open Base 2 + 3 + module Bench = struct 4 + [@@@warning "-69"] 5 + 6 + type result = { 7 + name : string; 8 + iterations : int; 9 + total_time : float; 10 + avg_time_ns : float; 11 + ops_per_sec : float; 12 + minor_words_per_op : float; 13 + major_words_per_op : float; 14 + promoted_words_per_op : float; 15 + } 16 + 17 + let run ~name ~iterations f = 18 + for _ = 1 to Int.min 1000 iterations do 19 + ignore (f ()) 20 + done; 21 + Stdlib.Gc.full_major (); 22 + Stdlib.Gc.compact (); 23 + let gc_before = Stdlib.Gc.quick_stat () in 24 + let start = Unix.gettimeofday () in 25 + for _ = 1 to iterations do 26 + ignore (f ()) 27 + done; 28 + let stop = Unix.gettimeofday () in 29 + let gc_after = Stdlib.Gc.quick_stat () in 30 + let total_time = stop -. start in 31 + let avg_time_ns = 32 + total_time /. Float.of_int iterations *. 1_000_000_000.0 33 + in 34 + let ops_per_sec = Float.of_int iterations /. total_time in 35 + let iters = Float.of_int iterations in 36 + let minor_words_per_op = 37 + (gc_after.minor_words -. gc_before.minor_words) /. iters 38 + in 39 + let major_words_per_op = 40 + (gc_after.major_words -. gc_before.major_words) /. iters 41 + in 42 + let promoted_words_per_op = 43 + (gc_after.promoted_words -. gc_before.promoted_words) /. iters 44 + in 45 + { 46 + name; 47 + iterations; 48 + total_time; 49 + avg_time_ns; 50 + ops_per_sec; 51 + minor_words_per_op; 52 + major_words_per_op; 53 + promoted_words_per_op; 54 + } 55 + 56 + let total_alloc r = r.minor_words_per_op +. r.major_words_per_op 57 + 58 + let print_result r = 59 + Stdio.printf " %-35s %8.0f ns/op %10.0f ops/s %6.0f words/op\n" r.name 60 + r.avg_time_ns r.ops_per_sec (total_alloc r) 61 + end 62 + 63 + let minimal_request = "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" 64 + 65 + let simple_request = 66 + "GET /path/to/resource HTTP/1.1\r\n\ 67 + Host: example.com\r\n\ 68 + User-Agent: Mozilla/5.0\r\n\ 69 + Accept: text/html\r\n\ 70 + Connection: keep-alive\r\n\ 71 + \r\n" 72 + 73 + let make_request_with_headers n_headers = 74 + let buf = Buffer.create 1024 in 75 + Buffer.add_string buf "GET /api/v1/users/12345 HTTP/1.1\r\n"; 76 + Buffer.add_string buf "Host: api.example.com\r\n"; 77 + for i = 1 to n_headers do 78 + Buffer.add_string buf 79 + (Printf.sprintf "X-Custom-Header-%d: value-%d\r\n" i i) 80 + done; 81 + Buffer.add_string buf "\r\n"; 82 + Buffer.contents buf 83 + 84 + let request_5_headers = make_request_with_headers 5 85 + let request_10_headers = make_request_with_headers 10 86 + let request_20_headers = make_request_with_headers 20 87 + let request_50_headers = make_request_with_headers 50 88 + 89 + let make_request_with_body body_size = 90 + let body = String.make body_size 'x' in 91 + Printf.sprintf 92 + "POST /upload HTTP/1.1\r\n\ 93 + Host: example.com\r\n\ 94 + Content-Length: %d\r\n\ 95 + Content-Type: application/octet-stream\r\n\ 96 + \r\n\ 97 + %s" 98 + body_size body 99 + 100 + let request_body_100 = make_request_with_body 100 101 + let request_body_1k = make_request_with_body 1024 102 + let request_body_10k = make_request_with_body 10240 103 + 104 + let browser_request = 105 + "GET /index.html HTTP/1.1\r\n\ 106 + Host: www.example.com\r\n\ 107 + User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \ 108 + AppleWebKit/537.36\r\n\ 109 + Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\ 110 + Accept-Language: en-US,en;q=0.9\r\n\ 111 + Accept-Encoding: gzip, deflate, br\r\n\ 112 + Connection: keep-alive\r\n\ 113 + Cache-Control: max-age=0\r\n\ 114 + Cookie: session=abc123; tracking=xyz789\r\n\ 115 + If-None-Match: \"abc123\"\r\n\ 116 + If-Modified-Since: Mon, 01 Jan 2024 00:00:00 GMT\r\n\ 117 + \r\n" 118 + 119 + let buffer_size = 16384 120 + 121 + let copy_to_buffer buf data = 122 + let len = String.length data in 123 + Bigstringaf.blit_from_string data ~src_off:0 buf ~dst_off:0 ~len; 124 + len 125 + 126 + let parse_request_h1 buf data = 127 + let len = copy_to_buffer buf data in 128 + let request = ref None in 129 + let request_handler reqd = 130 + let req = H1.Reqd.request reqd in 131 + request := Some req; 132 + let resp = H1.Response.create `OK in 133 + H1.Reqd.respond_with_string reqd resp "" 134 + in 135 + let error_handler ?request:_ _error _handler = () in 136 + let conn = H1.Server_connection.create ~error_handler request_handler in 137 + let _ = H1.Server_connection.read conn buf ~off:0 ~len in 138 + !request 139 + 140 + let just_copy buf data = 141 + let len = copy_to_buffer buf data in 142 + len 143 + 144 + let h1_buf = Bigstringaf.create buffer_size 145 + 146 + let run_parsing_benchmarks () = 147 + Stdio.print_endline "\nPARSING BENCHMARKS"; 148 + Stdio.print_endline (String.make 60 '='); 149 + 150 + let tests = 151 + [ 152 + ("h1_noop", 500_000, fun () -> ()); 153 + ( "h1_just_copy", 154 + 500_000, 155 + fun () -> ignore (just_copy h1_buf minimal_request) ); 156 + ( "h1_minimal", 157 + 200_000, 158 + fun () -> ignore (parse_request_h1 h1_buf minimal_request) ); 159 + ( "h1_simple", 160 + 200_000, 161 + fun () -> ignore (parse_request_h1 h1_buf simple_request) ); 162 + ( "h1_browser", 163 + 100_000, 164 + fun () -> ignore (parse_request_h1 h1_buf browser_request) ); 165 + ( "h1_5_headers", 166 + 200_000, 167 + fun () -> ignore (parse_request_h1 h1_buf request_5_headers) ); 168 + ( "h1_10_headers", 169 + 100_000, 170 + fun () -> ignore (parse_request_h1 h1_buf request_10_headers) ); 171 + ( "h1_20_headers", 172 + 100_000, 173 + fun () -> ignore (parse_request_h1 h1_buf request_20_headers) ); 174 + ( "h1_50_headers", 175 + 50_000, 176 + fun () -> ignore (parse_request_h1 h1_buf request_50_headers) ); 177 + ] 178 + in 179 + List.iter tests ~f:(fun (name, iterations, f) -> 180 + let result = Bench.run ~name ~iterations f in 181 + Bench.print_result result) 182 + 183 + let run_header_benchmarks () = 184 + Stdio.print_endline "\nHEADER LOOKUP BENCHMARKS"; 185 + Stdio.print_endline (String.make 60 '='); 186 + 187 + let buf = Bigstringaf.create buffer_size in 188 + let len = copy_to_buffer buf browser_request in 189 + let headers = ref H1.Headers.empty in 190 + let request_handler reqd = 191 + let req = H1.Reqd.request reqd in 192 + headers := req.H1.Request.headers; 193 + let resp = H1.Response.create `OK in 194 + H1.Reqd.respond_with_string reqd resp "" 195 + in 196 + let error_handler ?request:_ _error _handler = () in 197 + let conn = H1.Server_connection.create ~error_handler request_handler in 198 + let _ = H1.Server_connection.read conn buf ~off:0 ~len in 199 + let hdrs = !headers in 200 + 201 + let tests = 202 + [ 203 + ("h1_find_host", 1_000_000, fun () -> ignore (H1.Headers.get hdrs "host")); 204 + ( "h1_find_connection", 205 + 1_000_000, 206 + fun () -> ignore (H1.Headers.get hdrs "connection") ); 207 + ( "h1_find_user_agent", 208 + 1_000_000, 209 + fun () -> ignore (H1.Headers.get hdrs "user-agent") ); 210 + ] 211 + in 212 + List.iter tests ~f:(fun (name, iterations, f) -> 213 + let result = Bench.run ~name ~iterations f in 214 + Bench.print_result result) 215 + 216 + let run_body_benchmarks () = 217 + Stdio.print_endline "\nBODY HANDLING BENCHMARKS"; 218 + Stdio.print_endline (String.make 60 '='); 219 + 220 + let buf = Bigstringaf.create buffer_size in 221 + let tests = 222 + [ 223 + ( "h1_body_100B", 224 + 100_000, 225 + fun () -> ignore (parse_request_h1 buf request_body_100) ); 226 + ( "h1_body_1KB", 227 + 50_000, 228 + fun () -> ignore (parse_request_h1 buf request_body_1k) ); 229 + ( "h1_body_10KB", 230 + 20_000, 231 + fun () -> ignore (parse_request_h1 buf request_body_10k) ); 232 + ] 233 + in 234 + List.iter tests ~f:(fun (name, iterations, f) -> 235 + let result = Bench.run ~name ~iterations f in 236 + Bench.print_result result) 237 + 238 + let run_throughput_benchmarks () = 239 + Stdio.print_endline "\nTHROUGHPUT BENCHMARKS (1000 iterations per call)"; 240 + Stdio.print_endline (String.make 60 '='); 241 + 242 + let buf = Bigstringaf.create buffer_size in 243 + let iterations = 1000 in 244 + let tests = 245 + [ 246 + ( "h1_1k_simple", 247 + 1000, 248 + fun () -> 249 + for _ = 1 to iterations do 250 + ignore (parse_request_h1 buf simple_request) 251 + done ); 252 + ( "h1_1k_browser", 253 + 1000, 254 + fun () -> 255 + for _ = 1 to iterations do 256 + ignore (parse_request_h1 buf browser_request) 257 + done ); 258 + ( "h1_1k_50headers", 259 + 500, 260 + fun () -> 261 + for _ = 1 to iterations do 262 + ignore (parse_request_h1 buf request_50_headers) 263 + done ); 264 + ] 265 + in 266 + List.iter tests ~f:(fun (name, outer_iterations, f) -> 267 + let result = Bench.run ~name ~iterations:outer_iterations f in 268 + let total_ops = Float.of_int (outer_iterations * iterations) in 269 + let ops_per_sec = total_ops /. result.total_time in 270 + Stdio.printf " %-35s %10.0f requests/sec\n" name ops_per_sec) 271 + 272 + let () = 273 + Stdio.print_endline ""; 274 + Stdio.print_endline 275 + "============================================================"; 276 + Stdio.print_endline " H1 HTTP Parser Benchmarks (HCS)"; 277 + Stdio.print_endline 278 + "============================================================"; 279 + Stdio.print_endline ""; 280 + Stdio.print_endline "h1: HTTP/1.1 parser library used by HCS server"; 281 + Stdio.print_endline ""; 282 + 283 + run_parsing_benchmarks (); 284 + run_header_benchmarks (); 285 + run_body_benchmarks (); 286 + run_throughput_benchmarks (); 287 + 288 + Stdio.print_endline ""; 289 + Stdio.print_endline 290 + "============================================================"; 291 + Stdio.print_endline " Benchmark Complete"; 292 + Stdio.print_endline 293 + "============================================================"; 294 + Stdio.print_endline ""
+8
bench/hcs/dune
··· 25 25 (executable 26 26 (name bench_buffers) 27 27 (libraries hcs cstruct bigstringaf unix)) 28 + 29 + (executable 30 + (name bench_parser) 31 + (libraries h1 bigstringaf base stdio unix)) 32 + 33 + (executable 34 + (name bench_compare) 35 + (libraries h1 bigstringaf base stdio unix eio eio_main))
+22 -10
lib/client.ml
··· 197 197 | Error e -> Error (map_h1_error e) 198 198 199 199 let request_post t url ~body:request_body = 200 - match H1_client.post t.h1_client url ~body:request_body with 201 - | Ok resp -> 202 - Ok 203 - { 204 - status = h1_status_to_int resp.H1_client.status; 205 - headers = h1_headers_to_list resp.headers; 206 - body = resp.body; 207 - protocol = HTTP_1_1; 208 - } 209 - | Error e -> Error (map_h1_error e) 200 + if should_use_h2 t.config url then 201 + match H2_client.post t.h2_client url ~body:request_body with 202 + | Ok resp -> 203 + Ok 204 + { 205 + status = h2_status_to_int resp.H2_client.status; 206 + headers = h2_headers_to_list resp.headers; 207 + body = resp.body; 208 + protocol = HTTP_2; 209 + } 210 + | Error e -> Error (map_h2_error e) 211 + else 212 + match H1_client.post t.h1_client url ~body:request_body with 213 + | Ok resp -> 214 + Ok 215 + { 216 + status = h1_status_to_int resp.H1_client.status; 217 + headers = h1_headers_to_list resp.headers; 218 + body = resp.body; 219 + protocol = HTTP_1_1; 220 + } 221 + | Error e -> Error (map_h1_error e) 210 222 211 223 let get ~sw ~net ~clock ?(config = default_config) url = 212 224 let t = create ~sw ~net ~clock ~config () in
+51 -2
lib/h2_client.ml
··· 127 127 try Eio.Flow.close (Obj.magic c.flow) with _ -> () 128 128 end 129 129 130 - let do_request flow req = 130 + let do_request ?(body = "") flow req = 131 131 let response_received = Eio.Promise.create () in 132 132 let body_buffer = Buffer.create 4096 in 133 133 let resolved = ref false in ··· 185 185 H2.Client_connection.request conn req ~flush_headers_immediately:true 186 186 ~error_handler ~response_handler 187 187 in 188 - H2.Body.Writer.close body_writer 188 + if String.length body > 0 then begin 189 + H2.Body.Writer.write_string body_writer body; 190 + H2.Body.Writer.flush body_writer (fun _result -> 191 + H2.Body.Writer.close body_writer) 192 + end 193 + else H2.Body.Writer.close body_writer 189 194 end 190 195 in 191 196 ··· 379 384 with 380 385 | Some result -> result 381 386 | None -> Error Timeout 387 + 388 + let post t url ~body:request_body = 389 + let uri = Uri.of_string url in 390 + let scheme = Uri.scheme uri |> Option.value ~default:"https" in 391 + let is_https = String.equal scheme "https" in 392 + let host = Uri.host uri |> Option.value ~default:"localhost" in 393 + let default_port = if is_https then 443 else 80 in 394 + let port = Uri.port uri |> Option.value ~default:default_port in 395 + let path = Uri.path_and_query uri in 396 + let path = if path = "" then "/" else path in 397 + 398 + let total_timeout = t.config.connect_timeout +. t.config.read_timeout in 399 + match 400 + t.with_timeout total_timeout (fun () -> 401 + match acquire_connection t ~host ~port ~is_https with 402 + | Error e -> Error e 403 + | Ok conn -> ( 404 + let content_length = String.length request_body in 405 + let headers = 406 + H2.Headers.of_list 407 + [ 408 + (":authority", host); 409 + ("content-length", string_of_int content_length); 410 + ("content-type", "application/octet-stream"); 411 + ] 412 + in 413 + let req = H2.Request.create ~headers ~scheme `POST path in 414 + match do_request ~body:request_body conn.flow req with 415 + | Ok resp -> 416 + release_connection t ~host ~port ~is_https conn ~keep_alive:true; 417 + Ok resp 418 + | Error e -> 419 + release_connection t ~host ~port ~is_https conn 420 + ~keep_alive:false; 421 + Error e)) 422 + with 423 + | Some result -> result 424 + | None -> Error Timeout 425 + 426 + let post' ~sw ~net ~clock ?config url ~body = 427 + let t = create ~sw ~net ~clock ?config () in 428 + let result = post t url ~body in 429 + close t; 430 + result 382 431 383 432 (** {1 Backward-compatible stateless API} *) 384 433
+293 -403
lib/server.ml
··· 300 300 (** {1 Internal: HTTP/1.1 Connection Handler} *) 301 301 302 302 module H1_handler = struct 303 + let send_response reqd (response : Response.t) = 304 + let date_header = ("date", Date_cache.get ()) in 305 + match response.Response.body with 306 + | Response.Prebuilt_body prebuilt -> Prebuilt.respond_h1 reqd prebuilt 307 + | Response.Empty -> 308 + let headers = 309 + H1.Headers.of_list 310 + (date_header :: ("content-length", "0") :: response.headers) 311 + in 312 + let resp = H1.Response.create ~headers response.status in 313 + H1.Reqd.respond_with_string reqd resp "" 314 + | Response.String body -> 315 + let headers = 316 + H1.Headers.of_list 317 + (date_header 318 + :: ("content-length", string_of_int (String.length body)) 319 + :: response.headers) 320 + in 321 + let resp = H1.Response.create ~headers response.status in 322 + H1.Reqd.respond_with_string reqd resp body 323 + | Response.Bigstring body -> 324 + let headers = 325 + H1.Headers.of_list 326 + (date_header 327 + :: ("content-length", string_of_int (Bigstringaf.length body)) 328 + :: response.headers) 329 + in 330 + let resp = H1.Response.create ~headers response.status in 331 + H1.Reqd.respond_with_bigstring reqd resp body 332 + | Response.Cstruct cs -> 333 + let len = Cstruct.length cs in 334 + let headers = 335 + H1.Headers.of_list 336 + (date_header 337 + :: ("content-length", string_of_int len) 338 + :: response.headers) 339 + in 340 + let resp = H1.Response.create ~headers response.status in 341 + let body_writer = H1.Reqd.respond_with_streaming reqd resp in 342 + H1.Body.Writer.write_bigstring body_writer ~off:cs.off ~len cs.buffer; 343 + H1.Body.Writer.close body_writer 344 + | Response.Stream { content_length; next } -> 345 + let headers = 346 + match content_length with 347 + | Some len -> 348 + H1.Headers.of_list 349 + (date_header 350 + :: ("content-length", Int64.to_string len) 351 + :: response.headers) 352 + | None -> 353 + H1.Headers.of_list 354 + (date_header 355 + :: ("transfer-encoding", "chunked") 356 + :: response.headers) 357 + in 358 + let resp = H1.Response.create ~headers response.status in 359 + let body_writer = H1.Reqd.respond_with_streaming reqd resp in 360 + let rec write_chunks () = 361 + match next () with 362 + | None -> H1.Body.Writer.close body_writer 363 + | Some cs -> 364 + H1.Body.Writer.write_bigstring body_writer ~off:0 365 + ~len:(Cstruct.length cs) (Cstruct.to_bigarray cs); 366 + H1.Body.Writer.flush body_writer (fun () -> ()); 367 + write_chunks () 368 + in 369 + write_chunks () 370 + 371 + let send_error_response reqd status body_str = 372 + let headers = 373 + H1.Headers.of_list 374 + [ 375 + ("date", Date_cache.get ()); 376 + ("content-length", string_of_int (String.length body_str)); 377 + ] 378 + in 379 + let resp = H1.Response.create ~headers status in 380 + H1.Reqd.respond_with_string reqd resp body_str 381 + 382 + let handle_request ~handler reqd req body = 383 + let request = 384 + { 385 + meth = req.H1.Request.meth; 386 + target = req.target; 387 + headers = h1_headers_to_list req.headers; 388 + body; 389 + version = HTTP_1_1; 390 + } 391 + in 392 + let response = handler request in 393 + send_response reqd response 394 + 303 395 let handle ~handler ~ws_handler ?(ws_config = default_ws_config) 304 396 ?max_body_size ~initial_data flow = 305 397 let buffer_size = 16384 in ··· 315 407 let req = H1.Reqd.request reqd in 316 408 let h1_body = H1.Reqd.request_body reqd in 317 409 318 - (* Check for WebSocket upgrade *) 319 410 if Option.is_some ws_handler && Websocket.is_upgrade_request req.headers 320 411 then begin 321 412 match ··· 323 414 with 324 415 | Error reason -> 325 416 H1.Body.Reader.close h1_body; 326 - let body = "Forbidden: " ^ reason in 327 - let headers = 328 - H1.Headers.of_list 329 - [ 330 - ("date", Date_cache.get ()); 331 - ("content-length", string_of_int (String.length body)); 332 - ] 333 - in 334 - let resp = H1.Response.create ~headers `Forbidden in 335 - H1.Reqd.respond_with_string reqd resp body 417 + send_error_response reqd `Forbidden ("Forbidden: " ^ reason) 336 418 | Ok () -> ( 337 419 match Websocket.validate_websocket_version req.headers with 338 420 | Error _reason -> 339 421 H1.Body.Reader.close h1_body; 340 - let body = "Upgrade Required" in 341 422 let headers = 342 423 H1.Headers.of_list 343 424 [ 344 425 ("date", Date_cache.get ()); 345 - ("content-length", string_of_int (String.length body)); 426 + ("content-length", "16"); 346 427 ( "sec-websocket-version", 347 428 Websocket.supported_websocket_version ); 348 429 ] 349 430 in 350 431 let resp = H1.Response.create ~headers (`Code 426) in 351 - H1.Reqd.respond_with_string reqd resp body 432 + H1.Reqd.respond_with_string reqd resp "Upgrade Required" 352 433 | Ok () -> ( 353 434 match Websocket.get_websocket_key req.headers with 354 435 | Some key -> ··· 366 447 H1.Reqd.respond_with_upgrade reqd headers 367 448 | None -> 368 449 H1.Body.Reader.close h1_body; 369 - let headers = 370 - H1.Headers.of_list 371 - [ 372 - ("date", Date_cache.get ()); ("content-length", "11"); 373 - ] 374 - in 375 - let resp = H1.Response.create ~headers `Bad_request in 376 - H1.Reqd.respond_with_string reqd resp "Bad Request")) 450 + send_error_response reqd `Bad_request "Bad Request")) 377 451 end 378 452 else begin 379 - (* Regular HTTP/1.1 request *) 380 - (* Read body for POST/PUT, skip for GET/HEAD *) 381 - let body_result = 382 - match req.meth with 383 - | `GET | `HEAD | `DELETE | `OPTIONS | `CONNECT | `TRACE -> 384 - H1.Body.Reader.close h1_body; 385 - Ok "" 386 - | `POST | `PUT | `Other _ -> 387 - let body_buffer = Buffer.create 4096 in 388 - let body_size = ref 0 in 389 - let too_large = ref false in 390 - let body_done, resolver = Eio.Promise.create () in 391 - let rec read_body () = 392 - H1.Body.Reader.schedule_read h1_body 393 - ~on_eof:(fun () -> Eio.Promise.resolve resolver ()) 394 - ~on_read:(fun buf ~off ~len -> 395 - let new_size = !body_size + len in 396 - match max_body_size with 397 - | Some max when Int64.of_int new_size > max -> 398 - too_large := true; 399 - H1.Body.Reader.close h1_body; 400 - Eio.Promise.resolve resolver () 401 - | _ -> 402 - body_size := new_size; 403 - Buffer.add_string body_buffer 404 - (Bigstringaf.substring buf ~off ~len); 405 - read_body ()) 406 - in 407 - read_body (); 408 - Eio.Promise.await body_done; 409 - if !too_large then Error `Body_too_large 410 - else Ok (Buffer.contents body_buffer) 411 - in 412 - match body_result with 413 - | Error `Body_too_large -> 414 - let body = "Request body too large" in 415 - let headers = 416 - H1.Headers.of_list 417 - [ 418 - ("date", Date_cache.get ()); 419 - ("content-length", string_of_int (String.length body)); 420 - ] 421 - in 422 - let resp = H1.Response.create ~headers (`Code 413) in 423 - H1.Reqd.respond_with_string reqd resp body 424 - | Ok body -> ( 425 - let request = 426 - { 427 - meth = req.meth; 428 - target = req.target; 429 - headers = h1_headers_to_list req.headers; 430 - body; 431 - version = HTTP_1_1; 432 - } 453 + match req.meth with 454 + | `GET | `HEAD | `DELETE | `OPTIONS | `CONNECT | `TRACE -> 455 + H1.Body.Reader.close h1_body; 456 + handle_request ~handler reqd req "" 457 + | `POST | `PUT | `Other _ -> 458 + let body_buffer = Buffer.create 4096 in 459 + let body_size = ref 0 in 460 + let rec read_body () = 461 + H1.Body.Reader.schedule_read h1_body 462 + ~on_eof:(fun () -> 463 + let body = Buffer.contents body_buffer in 464 + handle_request ~handler reqd req body) 465 + ~on_read:(fun buf ~off ~len -> 466 + let new_size = !body_size + len in 467 + match max_body_size with 468 + | Some max when Int64.of_int new_size > max -> 469 + H1.Body.Reader.close h1_body; 470 + send_error_response reqd (`Code 413) 471 + "Request body too large" 472 + | _ -> 473 + body_size := new_size; 474 + Buffer.add_string body_buffer 475 + (Bigstringaf.substring buf ~off ~len); 476 + read_body ()) 433 477 in 434 - let response : Response.t = handler request in 435 - 436 - let date_header = ("date", Date_cache.get ()) in 437 - match response.Response.body with 438 - | Response.Prebuilt_body prebuilt -> 439 - Prebuilt.respond_h1 reqd prebuilt 440 - | Response.Empty -> 441 - let headers = 442 - H1.Headers.of_list 443 - (date_header :: ("content-length", "0") :: response.headers) 444 - in 445 - let resp = H1.Response.create ~headers response.status in 446 - H1.Reqd.respond_with_string reqd resp "" 447 - | Response.String body -> 448 - let headers = 449 - H1.Headers.of_list 450 - (date_header 451 - :: ("content-length", string_of_int (String.length body)) 452 - :: response.headers) 453 - in 454 - let resp = H1.Response.create ~headers response.status in 455 - H1.Reqd.respond_with_string reqd resp body 456 - | Response.Bigstring body -> 457 - let headers = 458 - H1.Headers.of_list 459 - (date_header 460 - :: ( "content-length", 461 - string_of_int (Bigstringaf.length body) ) 462 - :: response.headers) 463 - in 464 - let resp = H1.Response.create ~headers response.status in 465 - H1.Reqd.respond_with_bigstring reqd resp body 466 - | Response.Cstruct cs -> 467 - let len = Cstruct.length cs in 468 - let headers = 469 - H1.Headers.of_list 470 - (date_header 471 - :: ("content-length", string_of_int len) 472 - :: response.headers) 473 - in 474 - let resp = H1.Response.create ~headers response.status in 475 - let body_writer = H1.Reqd.respond_with_streaming reqd resp in 476 - H1.Body.Writer.write_bigstring body_writer ~off:cs.off ~len 477 - cs.buffer; 478 - H1.Body.Writer.close body_writer 479 - | Response.Stream { content_length; next } -> 480 - let headers = 481 - match content_length with 482 - | Some len -> 483 - H1.Headers.of_list 484 - (date_header 485 - :: ("content-length", Int64.to_string len) 486 - :: response.headers) 487 - | None -> 488 - H1.Headers.of_list 489 - (date_header 490 - :: ("transfer-encoding", "chunked") 491 - :: response.headers) 492 - in 493 - let resp = H1.Response.create ~headers response.status in 494 - let body_writer = H1.Reqd.respond_with_streaming reqd resp in 495 - let rec write_chunks () = 496 - match next () with 497 - | None -> H1.Body.Writer.close body_writer 498 - | Some cs -> 499 - H1.Body.Writer.write_bigstring body_writer ~off:0 500 - ~len:(Cstruct.length cs) (Cstruct.to_bigarray cs); 501 - (* Flush to ensure data is sent immediately (required for SSE) *) 502 - let flushed, resolve = Eio.Promise.create () in 503 - H1.Body.Writer.flush body_writer (fun () -> 504 - Eio.Promise.resolve resolve ()); 505 - Eio.Promise.await flushed; 506 - write_chunks () 507 - in 508 - write_chunks ()) 478 + read_body () 509 479 end 510 480 in 511 481 ··· 596 566 | None -> ()) 597 567 | None -> () 598 568 599 - (** Direct H1 handler - no protocol detection, no initial data buffering *) 600 569 let handle_direct ?max_body_size ~handler flow = 601 570 let buffer_size = 16384 in 602 571 let read_buffer = Bigstringaf.create buffer_size in ··· 608 577 let req = H1.Reqd.request reqd in 609 578 let h1_body = H1.Reqd.request_body reqd in 610 579 611 - (* Read body for POST/PUT, skip for GET/HEAD *) 612 - let body_result = 613 - match req.meth with 614 - | `GET | `HEAD | `DELETE | `OPTIONS | `CONNECT | `TRACE -> 615 - H1.Body.Reader.close h1_body; 616 - Ok "" 617 - | `POST | `PUT | `Other _ -> 618 - let body_buffer = Buffer.create 4096 in 619 - let body_size = ref 0 in 620 - let too_large = ref false in 621 - let body_done, resolver = Eio.Promise.create () in 622 - let rec read_body () = 623 - H1.Body.Reader.schedule_read h1_body 624 - ~on_eof:(fun () -> Eio.Promise.resolve resolver ()) 625 - ~on_read:(fun buf ~off ~len -> 626 - let new_size = !body_size + len in 627 - match max_body_size with 628 - | Some max when Int64.of_int new_size > max -> 629 - too_large := true; 630 - H1.Body.Reader.close h1_body; 631 - Eio.Promise.resolve resolver () 632 - | _ -> 633 - body_size := new_size; 634 - Buffer.add_string body_buffer 635 - (Bigstringaf.substring buf ~off ~len); 636 - read_body ()) 637 - in 638 - read_body (); 639 - Eio.Promise.await body_done; 640 - if !too_large then Error `Body_too_large 641 - else Ok (Buffer.contents body_buffer) 642 - in 643 - match body_result with 644 - | Error `Body_too_large -> 645 - let body = "Request body too large" in 646 - let headers = 647 - H1.Headers.of_list 648 - [ 649 - ("date", Date_cache.get ()); 650 - ("content-length", string_of_int (String.length body)); 651 - ] 652 - in 653 - let resp = H1.Response.create ~headers (`Code 413) in 654 - H1.Reqd.respond_with_string reqd resp body 655 - | Ok body -> ( 656 - let request = 657 - { 658 - meth = req.meth; 659 - target = req.target; 660 - headers = h1_headers_to_list req.headers; 661 - body; 662 - version = HTTP_1_1; 663 - } 580 + match req.meth with 581 + | `GET | `HEAD | `DELETE | `OPTIONS | `CONNECT | `TRACE -> 582 + H1.Body.Reader.close h1_body; 583 + handle_request ~handler reqd req "" 584 + | `POST | `PUT | `Other _ -> 585 + let body_buffer = Buffer.create 4096 in 586 + let body_size = ref 0 in 587 + let rec read_body () = 588 + H1.Body.Reader.schedule_read h1_body 589 + ~on_eof:(fun () -> 590 + let body = Buffer.contents body_buffer in 591 + handle_request ~handler reqd req body) 592 + ~on_read:(fun buf ~off ~len -> 593 + let new_size = !body_size + len in 594 + match max_body_size with 595 + | Some max when Int64.of_int new_size > max -> 596 + H1.Body.Reader.close h1_body; 597 + send_error_response reqd (`Code 413) 598 + "Request body too large" 599 + | _ -> 600 + body_size := new_size; 601 + Buffer.add_string body_buffer 602 + (Bigstringaf.substring buf ~off ~len); 603 + read_body ()) 664 604 in 665 - let response : Response.t = handler request in 666 - 667 - let date_header = ("date", Date_cache.get ()) in 668 - match response.Response.body with 669 - | Response.Prebuilt_body prebuilt -> Prebuilt.respond_h1 reqd prebuilt 670 - | Response.Empty -> 671 - let headers = 672 - H1.Headers.of_list 673 - (date_header :: ("content-length", "0") :: response.headers) 674 - in 675 - let resp = H1.Response.create ~headers response.status in 676 - H1.Reqd.respond_with_string reqd resp "" 677 - | Response.String body -> 678 - let headers = 679 - H1.Headers.of_list 680 - (date_header 681 - :: ("content-length", string_of_int (String.length body)) 682 - :: response.headers) 683 - in 684 - let resp = H1.Response.create ~headers response.status in 685 - H1.Reqd.respond_with_string reqd resp body 686 - | Response.Bigstring body -> 687 - let headers = 688 - H1.Headers.of_list 689 - (date_header 690 - :: ("content-length", string_of_int (Bigstringaf.length body)) 691 - :: response.headers) 692 - in 693 - let resp = H1.Response.create ~headers response.status in 694 - H1.Reqd.respond_with_bigstring reqd resp body 695 - | Response.Cstruct cs -> 696 - let len = Cstruct.length cs in 697 - let headers = 698 - H1.Headers.of_list 699 - (date_header 700 - :: ("content-length", string_of_int len) 701 - :: response.headers) 702 - in 703 - let resp = H1.Response.create ~headers response.status in 704 - let body_writer = H1.Reqd.respond_with_streaming reqd resp in 705 - H1.Body.Writer.write_bigstring body_writer ~off:cs.off ~len 706 - cs.buffer; 707 - H1.Body.Writer.close body_writer 708 - | Response.Stream { content_length; next } -> 709 - let headers = 710 - match content_length with 711 - | Some len -> 712 - H1.Headers.of_list 713 - (date_header 714 - :: ("content-length", Int64.to_string len) 715 - :: response.headers) 716 - | None -> 717 - H1.Headers.of_list 718 - (date_header 719 - :: ("transfer-encoding", "chunked") 720 - :: response.headers) 721 - in 722 - let resp = H1.Response.create ~headers response.status in 723 - let body_writer = H1.Reqd.respond_with_streaming reqd resp in 724 - let rec write_chunks () = 725 - match next () with 726 - | None -> H1.Body.Writer.close body_writer 727 - | Some cs -> 728 - H1.Body.Writer.write_bigstring body_writer ~off:cs.off 729 - ~len:(Cstruct.length cs) cs.buffer; 730 - let flushed, resolve = Eio.Promise.create () in 731 - H1.Body.Writer.flush body_writer (fun () -> 732 - Eio.Promise.resolve resolve ()); 733 - Eio.Promise.await flushed; 734 - write_chunks () 735 - in 736 - write_chunks ()) 605 + read_body () 737 606 in 738 607 739 608 let error_handler ?request:_ _error start_response = ··· 787 656 end 788 657 789 658 module H2_handler = struct 659 + let send_h2_response reqd (response : Response.t) = 660 + let h2_status = (response.Response.status :> H2.Status.t) in 661 + match response.Response.body with 662 + | Response.Prebuilt_body prebuilt -> Prebuilt.respond_h2 reqd prebuilt 663 + | Response.Empty -> 664 + let headers = 665 + H2.Headers.of_list (("content-length", "0") :: response.headers) 666 + in 667 + let resp = H2.Response.create ~headers h2_status in 668 + H2.Reqd.respond_with_string reqd resp "" 669 + | Response.String body -> 670 + let headers = 671 + H2.Headers.of_list 672 + (("content-length", string_of_int (String.length body)) 673 + :: response.headers) 674 + in 675 + let resp = H2.Response.create ~headers h2_status in 676 + H2.Reqd.respond_with_string reqd resp body 677 + | Response.Bigstring body -> 678 + let headers = 679 + H2.Headers.of_list 680 + (("content-length", string_of_int (Bigstringaf.length body)) 681 + :: response.headers) 682 + in 683 + let resp = H2.Response.create ~headers h2_status in 684 + H2.Reqd.respond_with_bigstring reqd resp body 685 + | Response.Cstruct cs -> 686 + let len = Cstruct.length cs in 687 + let headers = 688 + H2.Headers.of_list 689 + (("content-length", string_of_int len) :: response.headers) 690 + in 691 + let resp = H2.Response.create ~headers h2_status in 692 + let body_writer = H2.Reqd.respond_with_streaming reqd resp in 693 + H2.Body.Writer.write_bigstring body_writer ~off:cs.off ~len cs.buffer; 694 + H2.Body.Writer.close body_writer 695 + | Response.Stream { content_length; next } -> 696 + let headers = 697 + match content_length with 698 + | Some len -> 699 + H2.Headers.of_list 700 + (("content-length", Int64.to_string len) :: response.headers) 701 + | None -> H2.Headers.of_list response.headers 702 + in 703 + let resp = H2.Response.create ~headers h2_status in 704 + let body_writer = H2.Reqd.respond_with_streaming reqd resp in 705 + let rec write_chunks () = 706 + match next () with 707 + | None -> H2.Body.Writer.close body_writer 708 + | Some cs -> 709 + H2.Body.Writer.write_bigstring body_writer ~off:cs.off 710 + ~len:(Cstruct.length cs) cs.buffer; 711 + H2.Body.Writer.flush body_writer (fun _result -> ()); 712 + write_chunks () 713 + in 714 + write_chunks () 715 + 716 + let handle_h2_request ~handler reqd req body = 717 + let target = 718 + match H2.Headers.get req.H2.Request.headers ":path" with 719 + | Some p -> p 720 + | None -> "/" 721 + in 722 + let request = 723 + { 724 + meth = req.meth; 725 + target; 726 + headers = h2_headers_to_list req.headers; 727 + body; 728 + version = HTTP_2; 729 + } 730 + in 731 + let response = handler request in 732 + send_h2_response reqd response 733 + 790 734 let handle ~handler ~initial_data flow = 791 735 let read_buffer_size = 0x4000 in 792 736 let read_buffer = Bigstringaf.create read_buffer_size in ··· 796 740 let req = H2.Reqd.request reqd in 797 741 let body_reader = H2.Reqd.request_body reqd in 798 742 799 - let body = 800 - match req.meth with 801 - | `GET | `HEAD -> 802 - H2.Body.Reader.close body_reader; 803 - "" 804 - | _ -> 805 - let body_buffer = Buffer.create 4096 in 806 - let body_done, resolver = Eio.Promise.create () in 807 - let rec read_body () = 808 - H2.Body.Reader.schedule_read body_reader 809 - ~on_eof:(fun () -> Eio.Promise.resolve resolver ()) 810 - ~on_read:(fun buf ~off ~len -> 811 - Buffer.add_string body_buffer 812 - (Bigstringaf.substring buf ~off ~len); 813 - read_body ()) 814 - in 815 - read_body (); 816 - Eio.Promise.await body_done; 817 - Buffer.contents body_buffer 818 - in 819 - 820 - let target = 821 - match H2.Headers.get req.headers ":path" with 822 - | Some p -> p 823 - | None -> "/" 824 - in 825 - 826 - let request = 827 - { 828 - meth = req.meth; 829 - target; 830 - headers = h2_headers_to_list req.headers; 831 - body; 832 - version = HTTP_2; 833 - } 834 - in 835 - let response : Response.t = handler request in 836 - 837 - let h2_status = (response.Response.status :> H2.Status.t) in 838 - 839 - match response.Response.body with 840 - | Response.Prebuilt_body prebuilt -> Prebuilt.respond_h2 reqd prebuilt 841 - | Response.Empty -> 842 - let headers = 843 - H2.Headers.of_list (("content-length", "0") :: response.headers) 844 - in 845 - let resp = H2.Response.create ~headers h2_status in 846 - H2.Reqd.respond_with_string reqd resp "" 847 - | Response.String body -> 848 - let headers = 849 - H2.Headers.of_list 850 - (("content-length", string_of_int (String.length body)) 851 - :: response.headers) 743 + match req.meth with 744 + | `GET | `HEAD -> 745 + H2.Body.Reader.close body_reader; 746 + handle_h2_request ~handler reqd req "" 747 + | _ -> 748 + let body_buffer = Buffer.create 4096 in 749 + let rec read_body () = 750 + H2.Body.Reader.schedule_read body_reader 751 + ~on_eof:(fun () -> 752 + let body = Buffer.contents body_buffer in 753 + handle_h2_request ~handler reqd req body) 754 + ~on_read:(fun buf ~off ~len -> 755 + Buffer.add_string body_buffer 756 + (Bigstringaf.substring buf ~off ~len); 757 + read_body ()) 852 758 in 853 - let resp = H2.Response.create ~headers h2_status in 854 - H2.Reqd.respond_with_string reqd resp body 855 - | Response.Bigstring body -> 856 - let headers = 857 - H2.Headers.of_list 858 - (("content-length", string_of_int (Bigstringaf.length body)) 859 - :: response.headers) 860 - in 861 - let resp = H2.Response.create ~headers h2_status in 862 - H2.Reqd.respond_with_bigstring reqd resp body 863 - | Response.Cstruct cs -> 864 - let len = Cstruct.length cs in 865 - let headers = 866 - H2.Headers.of_list 867 - (("content-length", string_of_int len) :: response.headers) 868 - in 869 - let resp = H2.Response.create ~headers h2_status in 870 - let body_writer = H2.Reqd.respond_with_streaming reqd resp in 871 - H2.Body.Writer.write_bigstring body_writer ~off:cs.off ~len cs.buffer; 872 - H2.Body.Writer.close body_writer 873 - | Response.Stream { content_length; next } -> 874 - let headers = 875 - match content_length with 876 - | Some len -> 877 - H2.Headers.of_list 878 - (("content-length", Int64.to_string len) :: response.headers) 879 - | None -> H2.Headers.of_list response.headers 880 - in 881 - let resp = H2.Response.create ~headers h2_status in 882 - let body_writer = H2.Reqd.respond_with_streaming reqd resp in 883 - let rec write_chunks () = 884 - match next () with 885 - | None -> H2.Body.Writer.close body_writer 886 - | Some cs -> 887 - H2.Body.Writer.write_bigstring body_writer ~off:cs.off 888 - ~len:(Cstruct.length cs) cs.buffer; 889 - let flushed, resolve = Eio.Promise.create () in 890 - H2.Body.Writer.flush body_writer (fun _result -> 891 - Eio.Promise.resolve resolve ()); 892 - Eio.Promise.await flushed; 893 - write_chunks () 894 - in 895 - write_chunks () 759 + read_body () 896 760 in 897 761 898 762 let error_handler ?request:_ _error start_response = ··· 905 769 let shutdown = ref false in 906 770 907 771 let read_loop () = 772 + let buf_off = ref 0 in 773 + let buf_len = ref 0 in 774 + 775 + let compress () = 776 + if !buf_len > 0 && !buf_off > 0 then begin 777 + Bigstringaf.blit read_buffer ~src_off:!buf_off read_buffer ~dst_off:0 778 + ~len:!buf_len; 779 + buf_off := 0 780 + end 781 + else if !buf_len = 0 then buf_off := 0 782 + in 783 + 908 784 let rec loop () = 909 785 if not !shutdown then 910 786 match H2.Server_connection.next_read_operation conn with 911 787 | `Read -> 912 - let cs = 913 - Cstruct.of_bigarray read_buffer ~off:0 ~len:read_buffer_size 914 - in 915 - let socket_data = 916 - try 917 - let n = Eio.Flow.single_read flow cs in 918 - Cstruct.to_string (Cstruct.sub cs 0 n) 919 - with End_of_file -> "" 920 - in 921 - let data = 922 - if String.length !pending_data > 0 then begin 923 - let combined = !pending_data ^ socket_data in 924 - pending_data := ""; 925 - combined 926 - end 927 - else socket_data 928 - in 929 - let len = String.length data in 930 - if len = 0 then begin 931 - let _ = 932 - H2.Server_connection.read_eof conn read_buffer ~off:0 ~len:0 788 + compress (); 789 + let available = read_buffer_size - !buf_off - !buf_len in 790 + if available > 0 then begin 791 + let cs = 792 + Cstruct.of_bigarray read_buffer ~off:(!buf_off + !buf_len) 793 + ~len:available 933 794 in 934 - shutdown := true 935 - end 936 - else begin 937 - Bigstringaf.blit_from_string data ~src_off:0 read_buffer 938 - ~dst_off:0 ~len; 939 - let _ = 940 - H2.Server_connection.read conn read_buffer ~off:0 ~len 795 + let n = 796 + try Eio.Flow.single_read flow cs with End_of_file -> 0 941 797 in 942 - loop () 798 + if n = 0 then begin 799 + let _ = 800 + H2.Server_connection.read_eof conn read_buffer ~off:!buf_off 801 + ~len:!buf_len 802 + in 803 + shutdown := true 804 + end 805 + else begin 806 + buf_len := !buf_len + n; 807 + let consumed = 808 + H2.Server_connection.read conn read_buffer ~off:!buf_off 809 + ~len:!buf_len 810 + in 811 + buf_off := !buf_off + consumed; 812 + buf_len := !buf_len - consumed; 813 + loop () 814 + end 943 815 end 816 + else loop () 944 817 | `Close -> shutdown := true 818 + | `Yield -> 819 + let continue = Eio.Promise.create () in 820 + H2.Server_connection.yield_reader conn (fun () -> 821 + Eio.Promise.resolve (snd continue) ()); 822 + Eio.Promise.await (fst continue); 823 + loop () 945 824 in 825 + 826 + if String.length !pending_data > 0 then begin 827 + let len = String.length !pending_data in 828 + Bigstringaf.blit_from_string !pending_data ~src_off:0 read_buffer 829 + ~dst_off:0 ~len; 830 + buf_len := len; 831 + pending_data := ""; 832 + let consumed = H2.Server_connection.read conn read_buffer ~off:0 ~len in 833 + buf_off := consumed; 834 + buf_len := len - consumed 835 + end; 946 836 loop () 947 837 in 948 838 ··· 958 848 ~len:iov.H2.IOVec.len iov.H2.IOVec.buffer) 959 849 iovecs 960 850 in 961 - Eio.Flow.write flow cstructs; 962 851 let len = 963 852 List.fold_left (fun acc iov -> acc + iov.H2.IOVec.len) 0 iovecs 964 853 in 854 + Eio.Flow.write flow cstructs; 965 855 H2.Server_connection.report_write_result conn (`Ok len); 966 856 loop () 967 857 | `Yield ->
+5
test/dune
··· 27 27 (name test_websocket) 28 28 (libraries hcs eio_main) 29 29 (modules test_websocket)) 30 + 31 + (executable 32 + (name test_large_body) 33 + (libraries hcs eio_main) 34 + (modules test_large_body))
+446
test/test_large_body.ml
··· 1 + (** Large Body Test Suite - Regression tests for multi-megabyte handling. Tests 2 + HTTP/1.1, HTTP/2 (h2c), and WebSocket with large payloads. *) 3 + 4 + let generate_payload size = String.make size 'X' 5 + 6 + let generate_pattern_payload size = 7 + let buf = Buffer.create size in 8 + for i = 0 to size - 1 do 9 + Buffer.add_char buf (Char.chr (i mod 256)) 10 + done; 11 + Buffer.contents buf 12 + 13 + let error_to_string = function 14 + | Hcs.Client.Connection_failed s -> "Connection_failed: " ^ s 15 + | Hcs.Client.Tls_error s -> "Tls_error: " ^ s 16 + | Hcs.Client.Protocol_error s -> "Protocol_error: " ^ s 17 + | Hcs.Client.Timeout -> "Timeout" 18 + | Hcs.Client.Invalid_response s -> "Invalid_response: " ^ s 19 + | Hcs.Client.Too_many_redirects -> "Too_many_redirects" 20 + 21 + let ws_error_to_string = function 22 + | Hcs.Websocket.Connection_closed -> "Connection_closed" 23 + | Hcs.Websocket.Protocol_error s -> "Protocol_error: " ^ s 24 + | Hcs.Websocket.Io_error s -> "Io_error: " ^ s 25 + | Hcs.Websocket.Payload_too_large n -> "Payload_too_large: " ^ string_of_int n 26 + 27 + module H1_tests = struct 28 + let test_post_size ~sw ~net ~clock ~port ~size_kb () = 29 + let payload_size = size_kb * 1024 in 30 + let payload = generate_payload payload_size in 31 + let config = 32 + Hcs.Client. 33 + { default_config with read_timeout = 60.0; connect_timeout = 5.0 } 34 + in 35 + let url = Printf.sprintf "http://127.0.0.1:%d/upload" port in 36 + Printf.printf " %6dKB... %!" size_kb; 37 + match Hcs.Client.post ~sw ~net ~clock ~config url ~body:payload with 38 + | Ok resp -> 39 + if resp.status <> 200 then begin 40 + Printf.printf "FAIL (status %d)\n%!" resp.status; 41 + false 42 + end 43 + else begin 44 + let received = 45 + try 46 + int_of_string 47 + (String.sub resp.body 9 (String.length resp.body - 9)) 48 + with _ -> -1 49 + in 50 + if received <> payload_size then begin 51 + Printf.printf "FAIL (sent %d, got %d)\n%!" payload_size received; 52 + false 53 + end 54 + else begin 55 + Printf.printf "OK\n%!"; 56 + true 57 + end 58 + end 59 + | Error e -> 60 + Printf.printf "FAIL (%s)\n%!" (error_to_string e); 61 + false 62 + 63 + let test_integrity ~sw ~net ~clock ~port ~size_kb () = 64 + let payload_size = size_kb * 1024 in 65 + let payload = generate_pattern_payload payload_size in 66 + let config = 67 + Hcs.Client. 68 + { default_config with read_timeout = 60.0; connect_timeout = 5.0 } 69 + in 70 + let url = Printf.sprintf "http://127.0.0.1:%d/integrity" port in 71 + Printf.printf " %6dKB integrity... %!" size_kb; 72 + match Hcs.Client.post ~sw ~net ~clock ~config url ~body:payload with 73 + | Ok resp -> 74 + if resp.status = 200 && resp.body = "ok" then begin 75 + Printf.printf "OK\n%!"; 76 + true 77 + end 78 + else begin 79 + Printf.printf "FAIL (%s)\n%!" resp.body; 80 + false 81 + end 82 + | Error e -> 83 + Printf.printf "FAIL (%s)\n%!" (error_to_string e); 84 + false 85 + 86 + let test_max_body_size ~sw ~net ~clock () = 87 + let port = 18301 in 88 + let max_size = 1024L in 89 + let payload = generate_payload 2048 in 90 + let handler_called = ref false in 91 + let handler (_req : Hcs.Server.request) = 92 + handler_called := true; 93 + Hcs.Server.respond "should not reach" 94 + in 95 + let config = 96 + Hcs.Server. 97 + { 98 + default_config with 99 + port; 100 + max_body_size = Some max_size; 101 + gc_tuning = None; 102 + } 103 + in 104 + Eio.Fiber.fork_daemon ~sw (fun[@warning "-21"] () -> 105 + Hcs.Server.run ~sw ~net ~config handler; 106 + `Stop_daemon); 107 + Eio.Time.sleep clock 0.1; 108 + Printf.printf " max_body_size (1KB limit)... %!"; 109 + let url = Printf.sprintf "http://127.0.0.1:%d/upload" port in 110 + match Hcs.Client.post ~sw ~net ~clock url ~body:payload with 111 + | Ok resp -> 112 + if resp.status = 413 && not !handler_called then begin 113 + Printf.printf "OK (413)\n%!"; 114 + true 115 + end 116 + else begin 117 + Printf.printf "FAIL (status %d)\n%!" resp.status; 118 + false 119 + end 120 + | Error e -> 121 + Printf.printf "FAIL (%s)\n%!" (error_to_string e); 122 + false 123 + 124 + let run_all ~sw ~net ~clock ~port () = 125 + Printf.printf "\n POST sizes:\n%!"; 126 + let sizes = [ 1; 16; 32; 64; 128; 256; 512; 1024; 2048; 5120 ] in 127 + let size_results = 128 + List.map 129 + (fun kb -> test_post_size ~sw ~net ~clock ~port ~size_kb:kb ()) 130 + sizes 131 + in 132 + Printf.printf "\n Data integrity:\n%!"; 133 + let integrity_results = 134 + List.map 135 + (fun kb -> test_integrity ~sw ~net ~clock ~port ~size_kb:kb ()) 136 + [ 64; 256; 1024 ] 137 + in 138 + Printf.printf "\n Limits:\n%!"; 139 + let max_body_ok = test_max_body_size ~sw ~net ~clock () in 140 + List.for_all Fun.id size_results 141 + && List.for_all Fun.id integrity_results 142 + && max_body_ok 143 + end 144 + 145 + module H2_tests = struct 146 + let test_post_size ~sw ~net ~clock ~port ~size_kb () = 147 + let payload_size = size_kb * 1024 in 148 + let payload = generate_payload payload_size in 149 + let config = 150 + Hcs.Client. 151 + { 152 + default_config with 153 + read_timeout = 60.0; 154 + connect_timeout = 5.0; 155 + preferred_protocol = Some HTTP_2; 156 + } 157 + in 158 + let url = Printf.sprintf "http://127.0.0.1:%d/upload" port in 159 + Printf.printf " %6dKB... %!" size_kb; 160 + match Hcs.Client.post ~sw ~net ~clock ~config url ~body:payload with 161 + | Ok resp -> 162 + if resp.status <> 200 then begin 163 + Printf.printf "FAIL (status %d)\n%!" resp.status; 164 + false 165 + end 166 + else begin 167 + let received = 168 + try 169 + int_of_string 170 + (String.sub resp.body 9 (String.length resp.body - 9)) 171 + with _ -> -1 172 + in 173 + if received <> payload_size then begin 174 + Printf.printf "FAIL (sent %d, got %d)\n%!" payload_size received; 175 + false 176 + end 177 + else begin 178 + Printf.printf "OK\n%!"; 179 + true 180 + end 181 + end 182 + | Error e -> 183 + Printf.printf "FAIL (%s)\n%!" (error_to_string e); 184 + false 185 + 186 + let test_integrity ~sw ~net ~clock ~port ~size_kb () = 187 + let payload_size = size_kb * 1024 in 188 + let payload = generate_pattern_payload payload_size in 189 + let config = 190 + Hcs.Client. 191 + { 192 + default_config with 193 + read_timeout = 60.0; 194 + connect_timeout = 5.0; 195 + preferred_protocol = Some HTTP_2; 196 + } 197 + in 198 + let url = Printf.sprintf "http://127.0.0.1:%d/integrity" port in 199 + Printf.printf " %6dKB integrity... %!" size_kb; 200 + match Hcs.Client.post ~sw ~net ~clock ~config url ~body:payload with 201 + | Ok resp -> 202 + if resp.status = 200 && resp.body = "ok" then begin 203 + Printf.printf "OK\n%!"; 204 + true 205 + end 206 + else begin 207 + Printf.printf "FAIL (%s)\n%!" resp.body; 208 + false 209 + end 210 + | Error e -> 211 + Printf.printf "FAIL (%s)\n%!" (error_to_string e); 212 + false 213 + 214 + let run_all ~sw ~net ~clock ~port () = 215 + Printf.printf "\n POST sizes (h2c):\n%!"; 216 + let sizes = [ 1; 16; 32; 64; 128; 256; 512; 1024; 2048; 5120 ] in 217 + let size_results = 218 + List.map 219 + (fun kb -> test_post_size ~sw ~net ~clock ~port ~size_kb:kb ()) 220 + sizes 221 + in 222 + Printf.printf "\n Data integrity (h2c):\n%!"; 223 + let integrity_sizes = [ 64; 256; 1024 ] in 224 + let integrity_results = 225 + List.map 226 + (fun kb -> test_integrity ~sw ~net ~clock ~port ~size_kb:kb ()) 227 + integrity_sizes 228 + in 229 + List.for_all Fun.id size_results && List.for_all Fun.id integrity_results 230 + end 231 + 232 + module Websocket_tests = struct 233 + let test_large_message ~sw ~net ~port ~size_kb () = 234 + let payload_size = size_kb * 1024 in 235 + let payload = generate_payload payload_size in 236 + Printf.printf " %6dKB message... %!" size_kb; 237 + let url = Printf.sprintf "ws://127.0.0.1:%d/" port in 238 + match Hcs.Websocket.connect ~sw ~net url with 239 + | Ok ws -> ( 240 + match Hcs.Websocket.send_text ws payload with 241 + | Ok () -> ( 242 + match Hcs.Websocket.recv_message ws with 243 + | Ok (_, msg) -> 244 + Hcs.Websocket.close ws; 245 + let expected = 246 + "echo:" ^ string_of_int (String.length payload) 247 + in 248 + if msg = expected then begin 249 + Printf.printf "OK\n%!"; 250 + true 251 + end 252 + else begin 253 + Printf.printf "FAIL (got '%s')\n%!" 254 + (String.sub msg 0 (min 50 (String.length msg))); 255 + false 256 + end 257 + | Error e -> 258 + Hcs.Websocket.close ws; 259 + Printf.printf "FAIL recv (%s)\n%!" (ws_error_to_string e); 260 + false) 261 + | Error e -> 262 + Hcs.Websocket.close ws; 263 + Printf.printf "FAIL send (%s)\n%!" (ws_error_to_string e); 264 + false) 265 + | Error e -> 266 + Printf.printf "FAIL connect (%s)\n%!" (ws_error_to_string e); 267 + false 268 + 269 + let test_integrity ~sw ~net ~port ~size_kb () = 270 + let payload_size = size_kb * 1024 in 271 + let payload = generate_pattern_payload payload_size in 272 + Printf.printf " %6dKB integrity... %!" size_kb; 273 + let url = Printf.sprintf "ws://127.0.0.1:%d/" port in 274 + match Hcs.Websocket.connect ~sw ~net url with 275 + | Ok ws -> ( 276 + match Hcs.Websocket.send_binary ws payload with 277 + | Ok () -> ( 278 + match Hcs.Websocket.recv_message ws with 279 + | Ok (_, msg) -> 280 + Hcs.Websocket.close ws; 281 + if msg = "integrity:ok" then begin 282 + Printf.printf "OK\n%!"; 283 + true 284 + end 285 + else begin 286 + Printf.printf "FAIL (%s)\n%!" msg; 287 + false 288 + end 289 + | Error e -> 290 + Hcs.Websocket.close ws; 291 + Printf.printf "FAIL recv (%s)\n%!" (ws_error_to_string e); 292 + false) 293 + | Error e -> 294 + Hcs.Websocket.close ws; 295 + Printf.printf "FAIL send (%s)\n%!" (ws_error_to_string e); 296 + false) 297 + | Error e -> 298 + Printf.printf "FAIL connect (%s)\n%!" (ws_error_to_string e); 299 + false 300 + 301 + let run_all ~sw ~net ~port () = 302 + Printf.printf "\n Message sizes:\n%!"; 303 + let sizes = [ 1; 8; 16; 32; 64 ] in 304 + let size_results = 305 + List.map 306 + (fun kb -> test_large_message ~sw ~net ~port ~size_kb:kb ()) 307 + sizes 308 + in 309 + Printf.printf "\n Data integrity:\n%!"; 310 + let integrity_results = 311 + List.map 312 + (fun kb -> test_integrity ~sw ~net ~port ~size_kb:kb ()) 313 + [ 8; 32 ] 314 + in 315 + List.for_all Fun.id size_results && List.for_all Fun.id integrity_results 316 + end 317 + 318 + let () = 319 + Eio_main.run @@ fun env -> 320 + let net = Eio.Stdenv.net env in 321 + let clock = Eio.Stdenv.clock env in 322 + 323 + Printf.printf "=== Large Body Regression Tests ===\n%!"; 324 + 325 + let h1_ok = 326 + Eio.Switch.run @@ fun sw -> 327 + let port = 18300 in 328 + let handler (req : Hcs.Server.request) = 329 + let body_len = String.length req.body in 330 + match req.target with 331 + | "/upload" -> Hcs.Server.respond (Printf.sprintf "received:%d" body_len) 332 + | "/integrity" -> ( 333 + let mismatch = ref None in 334 + for i = 0 to body_len - 1 do 335 + if !mismatch = None && Char.code req.body.[i] <> i mod 256 then 336 + mismatch := Some i 337 + done; 338 + match !mismatch with 339 + | Some i -> Hcs.Server.respond (Printf.sprintf "mismatch at %d" i) 340 + | None -> Hcs.Server.respond "ok") 341 + | _ -> Hcs.Server.respond ~status:`Not_found "not found" 342 + in 343 + let config = Hcs.Server.{ default_config with port; gc_tuning = None } in 344 + Eio.Fiber.fork_daemon ~sw (fun[@warning "-21"] () -> 345 + Hcs.Server.run ~sw ~net ~config handler; 346 + `Stop_daemon); 347 + Eio.Time.sleep clock 0.1; 348 + Printf.printf "\n--- HTTP/1.1 Tests ---\n%!"; 349 + H1_tests.run_all ~sw ~net ~clock ~port () 350 + in 351 + 352 + let h2_ok = 353 + Eio.Switch.run @@ fun sw -> 354 + let port = 18302 in 355 + let handler (req : Hcs.Server.request) = 356 + match req.target with 357 + | "/upload" -> 358 + let body_len = String.length req.body in 359 + Hcs.Server.respond (Printf.sprintf "received:%d" body_len) 360 + | "/integrity" -> ( 361 + let len = String.length req.body in 362 + let mismatch = ref None in 363 + for i = 0 to len - 1 do 364 + if !mismatch = None && Char.code req.body.[i] <> i mod 256 then 365 + mismatch := Some i 366 + done; 367 + match !mismatch with 368 + | Some i -> 369 + Hcs.Server.respond ~status:`Bad_request 370 + (Printf.sprintf "mismatch at byte %d" i) 371 + | None -> Hcs.Server.respond "ok") 372 + | _ -> Hcs.Server.respond ~status:`Not_found "not found" 373 + in 374 + let config = 375 + Hcs.Server. 376 + { default_config with port; protocol = Http2_only; gc_tuning = None } 377 + in 378 + Eio.Fiber.fork_daemon ~sw (fun[@warning "-21"] () -> 379 + Hcs.Server.run ~sw ~net ~config handler; 380 + `Stop_daemon); 381 + Eio.Time.sleep clock 0.1; 382 + Printf.printf "\n--- HTTP/2 Tests ---\n%!"; 383 + H2_tests.run_all ~sw ~net ~clock ~port () 384 + in 385 + 386 + let ws_ok = 387 + Eio.Switch.run @@ fun sw -> 388 + let port = 18303 in 389 + let handler (_req : Hcs.Server.request) = 390 + Hcs.Server.respond "use websocket" 391 + in 392 + let ws_handler ws = 393 + let rec loop () = 394 + match Hcs.Websocket.recv_message ws with 395 + | Error Hcs.Websocket.Connection_closed -> () 396 + | Error _ -> () 397 + | Ok (opcode, msg) -> ( 398 + let response = 399 + match opcode with 400 + | Hcs.Websocket.Opcode.Text -> 401 + "echo:" ^ string_of_int (String.length msg) 402 + | Hcs.Websocket.Opcode.Binary -> ( 403 + let len = String.length msg in 404 + let mismatch = ref None in 405 + for i = 0 to len - 1 do 406 + if !mismatch = None && Char.code msg.[i] <> i mod 256 then 407 + mismatch := Some i 408 + done; 409 + match !mismatch with 410 + | Some i -> Printf.sprintf "integrity:mismatch at %d" i 411 + | None -> "integrity:ok") 412 + | _ -> "unknown opcode" 413 + in 414 + match Hcs.Websocket.send_text ws response with 415 + | Ok () -> loop () 416 + | Error _ -> ()) 417 + in 418 + loop () 419 + in 420 + let config = 421 + Hcs.Server. 422 + { 423 + default_config with 424 + port; 425 + protocol = Auto_websocket; 426 + gc_tuning = None; 427 + } 428 + in 429 + Eio.Fiber.fork_daemon ~sw (fun[@warning "-21"] () -> 430 + Hcs.Server.run ~sw ~net ~config ~ws_handler handler; 431 + `Stop_daemon); 432 + Eio.Time.sleep clock 0.1; 433 + Printf.printf "\n--- WebSocket Tests ---\n%!"; 434 + Websocket_tests.run_all ~sw ~net ~port () 435 + in 436 + 437 + Printf.printf "\n=== Summary ===\n%!"; 438 + Printf.printf "HTTP/1.1: %s\n%!" (if h1_ok then "PASS" else "FAIL"); 439 + Printf.printf "HTTP/2: %s\n%!" (if h2_ok then "PASS" else "FAIL"); 440 + Printf.printf "WebSocket: %s\n%!" (if ws_ok then "PASS" else "FAIL"); 441 + 442 + if h1_ok && h2_ok && ws_ok then Printf.printf "\n*** ALL TESTS PASSED ***\n%!" 443 + else begin 444 + Printf.printf "\n*** SOME TESTS FAILED ***\n%!"; 445 + exit 1 446 + end