ocaml http/1, http/2 and websocket client and server library
at main 15 kB view raw
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 4let generate_payload size = String.make size 'X' 5 6let 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 13let 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 21let 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 27module 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 143end 144 145module 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 230end 231 232module 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 316end 317 318let () = 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