open Httpun open Helpers open Client_connection let response_error_pp_hum fmt = function | `Malformed_response str -> Format.fprintf fmt "Malformed_response: %s" str | `Invalid_response_body_length resp -> Format.fprintf fmt "Invalid_response_body_length: %s" (response_to_string resp) | `Exn exn -> Format.fprintf fmt "Exn (%s)" (Printexc.to_string exn) ;; module Response = struct include Response let pp = pp_hum let equal x y = x = y end module Alcotest = struct include Alcotest let response_error = of_pp response_error_pp_hum end let feed_string t str = let len = String.length str in let input = Bigstringaf.of_string str ~off:0 ~len in read t input ~off:0 ~len let read_string t str = let c = feed_string t str in Alcotest.(check int) "read consumes all input" (String.length str) c; ;; let read_response t r = let response_string = response_to_string r in read_string t response_string ;; let reader_ready ?(msg="Reader is ready") t = Alcotest.check read_operation msg `Read (next_read_operation t :> [`Close | `Read | `Yield]); ;; let reader_yielded ?(msg="Reader is in a yield state") t = Alcotest.check read_operation msg `Yield (next_read_operation t); ;; let reader_closed t = Alcotest.check read_operation "Reader is closed" `Close (next_read_operation t :> [`Close | `Read | `Yield]); ;; let write_string ?(msg="output written") t str = let len = String.length str in Alcotest.(check (option string)) msg (Some str) (next_write_operation t |> Write_operation.to_write_as_string); report_write_result t (`Ok len); ;; let write_request ?(msg="request written") t r = let request_string = request_to_string r in write_string ~msg t request_string ;; let writer_yielded t = Alcotest.check write_operation "Writer is in a yield state" `Yield (next_write_operation t); ;; let writer_closed t = Alcotest.check write_operation "Writer is closed" (`Close 0) (next_write_operation t); ;; let connection_is_shutdown t = Alcotest.(check bool) "connection is shutdown" true (is_closed t) ;; let default_response_handler expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let on_read _ ~off:_ ~len:_ = () in let on_eof () = () in Body.Reader.schedule_read body ~on_read ~on_eof; ;; let no_error_handler _ = assert false let test_commit_parse_after_every_header () = let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list [ "Links", "/path/to/some/website" ; "Links", "/path/to/some/website" ; "connection", "close" ]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; let response_line = "HTTP/1.1 200 OK\r\n" in let single_header = "Links: /path/to/some/website\r\n" in let r = (* Each header is 30 bytes *) response_line ^ single_header ^ single_header ^ "connection: close\r\n\r\n" in let bs = Bigstringaf.of_string r ~off:0 ~len:(String.length r) in let c = read t bs ~off:0 ~len:(String.length response_line + 15) in Alcotest.(check int) "only reads the response line" (String.length response_line) c; let c' = read t bs ~off:c ~len:(String.length single_header) in Alcotest.(check int) "parser can read a single header and commit" (String.length single_header) c'; let c'' = read_eof t bs ~off:(c + c') ~len:(String.length r - (c + c')) in Alcotest.(check int) "read_eof with the rest of the input is accepted" (String.length r - (c + c')) c''; connection_is_shutdown t; ;; let test_get () = let request' = Request.create `GET "/" in let response = Response.create `OK in (* Single GET *) let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; read_response t response; (* Single GET, request closes the connection. *) let request_close = Request.create ~headers:(Headers.of_list ["connection", "close"]) `GET "/" in let t = create () in let body = request t request_close ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request_close; writer_closed t; read_response t response; (* Single GET, response closes connection *) let response = Response.create `OK ~headers:Headers.connection_close in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; read_response t response; writer_closed t; let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; connection_is_shutdown t; (* Single GET, streaming body *) let response = Response.create `OK ~headers:Headers.encoding_chunked in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; read_response t response; read_string t "d\r\nHello, world!\r\n0\r\n\r\n"; ;; let test_head () = let error_handler_called = ref false in let request' = Request.create `HEAD "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "5"]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:(fun _ -> error_handler_called := true) in Body.Writer.close body; write_request t request'; read_response t response; let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; connection_is_shutdown t; Alcotest.(check bool) "error handler not called" false !error_handler_called; ;; let test_get_last_close () = (* Multiple GET requests, the last one closes the connection *) let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "0"]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; read_response t response; let request'' = Request.create ~headers:(Headers.of_list ["connection", "close"]) `GET "/" in let body' = request t request'' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body'; write_request t request''; read_response t response; writer_closed t; let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; connection_is_shutdown t; ;; let test_send_streaming_body () = let request' = Request.create `GET "/" ~headers:Headers.encoding_chunked in let response = Response.create `OK ~headers:Headers.encoding_chunked in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in write_request t request'; read_response t response; Body.Writer.write_string body "hello"; write_string t "5\r\nhello\r\n"; Body.Writer.write_string body "world"; Body.Writer.close body; write_string t "5\r\nworld\r\n"; write_string t "0\r\n\r\n"; writer_yielded t; ;; let test_response_eof () = let request' = Request.create `GET "/" in let response = Response.create `OK in (* not actually writen to the channel *) let error_message = ref None in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:(function | `Malformed_response msg -> error_message := Some msg | _ -> assert false) in Body.Writer.close body; write_request t request'; reader_ready t; let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; connection_is_shutdown t; Alcotest.(check (option string)) "unexpected eof" (Some "unexpected eof") !error_message ;; let test_persistent_connection_requests () = let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list [ "content-length", "0" ]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; read_response t response; writer_yielded t; reader_ready t; let body' = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body'; write_request t request'; read_response t response; ;; let test_persistent_connection_requests_pipelining () = let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list [ "content-length", "0" ]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; (* send the 2nd request without reading the response *) let response' = Response.create ~headers:(Headers.of_list [ "content-length", "0" ]) `Not_found in let body' = request t request' ~flush_headers_immediately:true ~response_handler:(fun response body -> (default_response_handler response' response body)) ~error_handler:no_error_handler in Body.Writer.close body'; write_request t request'; read_response t response; read_response t response'; ;; let test_persistent_connection_requests_pipelining_send_body () = let request' = Request.create ~headers:(Headers.of_list [ "content-length", "8" ]) `GET "/" in let response = Response.create ~headers:(Headers.of_list [ "content-length", "0" ]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in write_request t request'; (* send the 2nd request without reading the response *) let request'' = Request.create `GET "/" in let response' = Response.create ~headers:(Headers.of_list [ "content-length", "0" ]) `Not_found in let body' = request t request'' ~flush_headers_immediately:true ~response_handler:(fun response body -> (default_response_handler response' response body)) ~error_handler:no_error_handler in Body.Writer.close body'; Body.Writer.write_string body "a string"; Body.Writer.close body; write_string ~msg:"writes the body for the first request" t "a string"; write_request t request''; read_response t response; read_response t response'; ;; let test_persistent_connection_requests_body () = let request' = Request.create `GET "/" in let request'' = Request.create `GET "/second" in let response = Response.create ~headers:(Headers.of_list [ "content-length", "10" ]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; let response' = Response.create `OK in read_response t response; read_string t "ten chars."; let body' = request t request'' ~flush_headers_immediately:true ~response_handler:(default_response_handler response') ~error_handler:no_error_handler in Body.Writer.close body'; write_request t request''; read_response t response'; ;; let test_response_header_order () = let request' = Request.create `GET "/" in let headers = [ "a", "1" ; "b", "2" ; "c", "3" ] in let response = Response.create `OK ~headers:(Headers.of_list headers) in let received = ref None in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(fun response _ -> received := Some response) ~error_handler:no_error_handler in Body.Writer.close body; write_request t request'; writer_yielded t; read_response t response; match !received with | None -> assert false | Some received -> Alcotest.(check (list (pair string string))) "headers are equal" headers (Headers.to_list received.headers); ;; let test_report_exn () = let request' = Request.create `GET "/" in let response = Response.create `OK in (* not actually writen to the channel *) let error_message = ref None in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:(function | `Exn (Failure msg) -> error_message := Some msg | _ -> assert false) in Body.Writer.close body; write_request t request'; writer_yielded t; reader_ready t; report_exn t (Failure "something went wrong"); connection_is_shutdown t; Alcotest.(check (option string)) "something went wrong" (Some "something went wrong") !error_message ;; let test_input_shrunk () = let request' = Request.create `GET "/" in let response = Response.create `OK in (* not actually writen to the channel *) let error_message = ref None in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:(function | `Exn (Failure msg) -> error_message := Some msg | _ -> assert false) in Body.Writer.close body; write_request t request'; writer_yielded t; reader_ready t; let c = feed_string t "HTTP/1.1 200 OK\r\nDate" in Alcotest.(check int) "read the status line" c 17; report_exn t (Failure "something went wrong"); connection_is_shutdown t; Alcotest.(check (option string)) "something went wrong" (Some "something went wrong") !error_message ;; let test_partial_input () = let request' = Request.create `GET "/" in let response_handler response response_body = Alcotest.(check (list (pair string string))) "got expected headers" [ "Connection", "close" ] (Headers.to_rev_list response.Response.headers); Body.Reader.close response_body in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; let len = feed_string t "HTTP/1.1 200 OK\r\nC" in Alcotest.(check int) "partial read" 17 len; read_string t "Connection: close\r\n\r\n"; let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; shutdown t; reader_closed t; writer_closed t; ;; let test_empty_fixed_body () = let request' = Request.create ~headers:(Headers.of_list ["Connection", "close"]) `GET "/" in let response_handler response response_body = Alcotest.(check (list (pair string string))) "got expected headers" [ "Connection", "close" ] (Headers.to_rev_list response.Response.headers); Body.Reader.close response_body in let t = create () in let (_body: Body.Writer.t) = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; writer_closed t; reader_ready t; read_response t (Response.create ~headers:(Headers.of_list ["Connection", "close"]) `OK); let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; reader_closed t; ;; let test_fixed_body () = let request' = Request.create ~headers:(Headers.of_list ["Connection", "close"; "Content-Length", "3"]) `GET "/" in let response_handler response response_body = Alcotest.(check (list (pair string string))) "got expected headers" [ "Connection", "close" ] (Headers.to_rev_list response.Response.headers); Body.Reader.close response_body in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.write_string body "foo"; write_string t "foo"; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t (Response.create ~headers:(Headers.of_list ["Connection", "close"]) `OK); let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; reader_closed t; writer_closed t; ;; let test_fixed_body_persistent_connection () = let writer_woken_up = ref false in let request' = Request.create ~headers:(Headers.of_list ["Content-Length", "2"]) `GET "/" in let response_handler response response_body = Alcotest.(check (list (pair string string))) "got expected headers" [] (Headers.to_rev_list response.Response.headers); Body.Reader.close response_body in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); Body.Writer.write_string body "hi"; Body.Writer.close body; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; write_string t "hi"; writer_yielded t; writer_woken_up := false; yield_writer t (fun () -> writer_woken_up := true); Alcotest.(check bool) "Writer doesn't wake up immediately" false !writer_woken_up; reader_ready t; read_response t (Response.create ~headers:(Headers.of_list []) `OK); reader_ready t; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; writer_yielded t; ;; let test_empty_fixed_body_persistent_connection () = let request' = Request.create ~headers:(Headers.of_list ["Content-Length", "0"]) `GET "/" in let response_handler response response_body = Alcotest.(check (list (pair string string))) "got expected headers" [] (Headers.to_rev_list response.Response.headers); Body.Reader.close response_body in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:response_handler ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t (Response.create ~headers:(Headers.of_list []) `OK); reader_ready t; writer_yielded t; ;; let test_client_upgrade () = let writer_woken_up = ref false in let reader_woken_up = ref false in let request' = Request.create ~headers:(Headers.of_list ["Content-Length", "0"]) `GET "/" in let t = create () in let response = Response.create `Switching_protocols in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t response; reader_yielded t; yield_reader t (fun () -> reader_woken_up := true); writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); Alcotest.(check bool) "Reader hasn't woken up yet" false !reader_woken_up; Alcotest.(check bool) "Writer hasn't woken up yet" false !writer_woken_up; shutdown t; Alcotest.(check bool) "Reader woken up" true !reader_woken_up; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; connection_is_shutdown t; ;; let backpressure_response_handler continue_reading expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let rec on_read _buffer ~off:_ ~len:_ = continue_reading := (fun () -> Body.Reader.schedule_read body ~on_eof ~on_read); and on_eof () = print_endline "got eof" in Body.Reader.schedule_read body ~on_eof ~on_read ;; let test_handling_backpressure_when_read_not_scheduled () = let writer_woken_up = ref false in let reader_woken_up = ref false in let continue_reading = ref (fun () -> ()) in let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(backpressure_response_handler continue_reading response) ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t response; yield_writer t (fun () -> writer_woken_up := true); read_string t "five."; reader_yielded t; yield_reader t (fun () -> reader_woken_up := true); !continue_reading (); reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t; Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up; Alcotest.(check bool) "Writer not woken up" false !writer_woken_up; ;; let test_handling_backpressure_when_read_not_scheduled_early_yield () = let writer_woken_up = ref false in let reader_woken_up = ref false in let continue_reading = ref (fun () -> ()) in let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(backpressure_response_handler continue_reading response) ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t response; yield_reader t (fun () -> reader_woken_up := true); yield_writer t (fun () -> writer_woken_up := true); read_string t "five."; reader_yielded t; !continue_reading (); reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t; Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up; Alcotest.(check bool) "Writer not woken up" false !writer_woken_up; ;; let test_eof_with_another_pipelined_request () = let reader_woken_up = ref false in let continue_reading = ref (fun () -> ()) in let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let t = create () in let response_handler continue_reading expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let on_read _buffer ~off:_ ~len:_ = continue_reading := (fun () -> Body.Reader.close body); and on_eof () = print_endline "got eof" in Body.Reader.schedule_read body ~on_eof ~on_read in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler continue_reading response) ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t response; yield_reader t (fun () -> reader_woken_up := true); yield_writer t ignore; read_string t "five."; reader_yielded t; !continue_reading (); reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t; Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up; writer_yielded t; (* Pipeline the 2nd request. *) let expected_response = Response.create `OK in let error_handler_called = ref false in let body' = request t request' ~flush_headers_immediately:true ~response_handler:(fun response _ -> Alcotest.check (module Response) "expected response" expected_response response) ~error_handler:(fun _ -> error_handler_called := true;) in Body.Writer.close body'; write_request t request'; writer_yielded t; let str = "rest." in let len = String.length str in let input = Bigstringaf.of_string str ~off:0 ~len in let just_read = read_eof t input ~off:0 ~len in Alcotest.(check int) "Rest of the response read completely, server EOF" len just_read; reader_closed t; Alcotest.(check bool) "Error handler called on the 2nd request" true !error_handler_called; connection_is_shutdown t; ;; (* If we haven't finished receiving the full response, the error handler * should be called. *) let test_eof_handler_response_body_not_closed () = let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let t = create () in let response_handler expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let on_read _buffer ~off:_ ~len:_ = () in let on_eof () = print_endline "got eof" in Body.Reader.schedule_read body ~on_eof ~on_read in let error_handler_called = ref false in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler response) ~error_handler:(fun _ -> error_handler_called := true;) in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t response; yield_writer t ignore; reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t; let str = "rest." in let len = String.length str in let input = Bigstringaf.of_string str ~off:0 ~len in let just_read = read_eof t input ~off:0 ~len in Alcotest.(check int) "server EOF after delivering the first part of the response" len just_read; reader_closed t; Alcotest.(check bool) "Error handler called" true !error_handler_called; connection_is_shutdown t; ;; let test_eof_handler_closed_response_body () = let continue_reading = ref (fun () -> ()) in let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let t = create () in let response_handler continue_reading expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let on_read _buffer ~off:_ ~len:_ = continue_reading := (fun () -> Body.Reader.close body); and on_eof () = print_endline "got eof" in Body.Reader.schedule_read body ~on_eof ~on_read in let error_handler_called = ref false in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler continue_reading response) ~error_handler:(fun _ -> error_handler_called := true;) in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t response; yield_writer t ignore; !continue_reading (); reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t; let str = "rest." in let len = String.length str in let input = Bigstringaf.of_string str ~off:0 ~len in let just_read = read_eof t input ~off:0 ~len in Alcotest.(check int) "Rest of the response read completely, server EOF" len just_read; reader_closed t; Alcotest.(check bool) "Error handler called" true !error_handler_called; connection_is_shutdown t; ;; let test_exception_closes_reader () = let reader_woken_up = ref false in let request' = Request.create `GET "/" in let error_handler_called = ref false in let response = Response.create ~headers:(Headers.of_list [ "connection", "close"; "content-length", "10" ]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:(fun _ -> error_handler_called := true;) in Body.Writer.close body; write_request t request'; read_response t response; writer_closed t; read_string t "hello"; reader_yielded t ~msg:"Reader yields if no read scheduled"; yield_reader t (fun () -> reader_woken_up := true); report_exn t (Failure "something went wrong"); Alcotest.(check bool) "Error handler called" true !error_handler_called; Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up; connection_is_shutdown t; ;; let test_exception_closes_reader_persistent_connection () = let reader_woken_up = ref false in let writer_woken_up = ref false in let request' = Request.create `GET "/" in let error_handler_called = ref false in let response = Response.create ~headers:(Headers.of_list [ "content-length", "10" ]) `OK in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:(fun _ -> error_handler_called := true;) in Body.Writer.close body; write_request t request'; read_response t response; writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); read_string t "hello"; reader_yielded t ~msg:"Reader yields if no read scheduled"; yield_reader t (fun () -> reader_woken_up := true); report_exn t (Failure "something went wrong"); Alcotest.(check bool) "Error handler called" true !error_handler_called; Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; connection_is_shutdown t; ;; let test_exception_reading_response_body () = let request' = Request.create `GET "/" in let error_handler_called = ref false in let response = Response.create ~headers:(Headers.of_list [ "content-length", "10" ]) `OK in let t = create () in let response_handler expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let on_read _ ~off:_ ~len:_ = failwith "something went wrong" in let on_eof () = () in Body.Reader.schedule_read body ~on_read ~on_eof; in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler response) ~error_handler:(fun _ -> error_handler_called := true;) in Body.Writer.close body; write_request t request'; read_response t response; read_string t "hello"; Alcotest.(check bool) "Error handler called" true !error_handler_called; writer_closed t; reader_closed t; connection_is_shutdown t; ;; let test_exception_reading_response_body_last_chunk () = let writer_woken_up = ref false in let request' = Request.create `GET "/" in let error_handler_called = ref false in let response = Response.create ~headers:(Headers.of_list [ "content-length", "10" ]) `OK in let t = create () in let response_handler expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let on_eof () = () in let on_read _ ~off:_ ~len:_ = Body.Reader.schedule_read body ~on_read:(fun _ ~off:_ ~len:_ -> report_exn t (Failure "something went wrong")) ~on_eof in Body.Reader.schedule_read body ~on_read ~on_eof; in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler response) ~error_handler:(fun _ -> error_handler_called := true;) in Body.Writer.close body; write_request t request'; read_response t response; writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); read_string t "hello"; reader_ready t; read_string t "hello"; Alcotest.(check bool) "Error handler called" true !error_handler_called; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; writer_closed t; reader_closed t; connection_is_shutdown t; ;; let test_async_exception_reading_response_body () = let reader_woken_up = ref false in let writer_woken_up = ref false in let continue_reading = ref (fun () -> ()) in let request' = Request.create `GET "/" in let error_handler_called = ref false in let response = Response.create ~headers:(Headers.of_list [ "content-length", "10" ]) `OK in let t = create () in let response_handler expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let on_read _ ~off:_ ~len:_ = continue_reading := (fun () -> report_exn t (Failure "something went wrong")) in let on_eof () = () in Body.Reader.schedule_read body ~on_read ~on_eof; in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler response) ~error_handler:(fun _ -> error_handler_called := true;) in Body.Writer.close body; write_request t request'; read_response t response; read_string t "hello"; reader_yielded t; yield_reader t (fun () -> reader_woken_up := true); writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); !continue_reading (); Alcotest.(check bool) "Error handler called" true !error_handler_called; Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; writer_closed t; reader_closed t; connection_is_shutdown t; ;; let test_failed_response_parse () = let writer_woken_up = ref false in let request' = Request.create `GET "/" in let test response bytes_read expected_error = let error = ref None in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(fun _ _ -> assert false) ~error_handler:(fun e -> error := Some e) in Body.Writer.close body; write_request t request'; writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); reader_ready t; let len = feed_string t response in Alcotest.(check int) "bytes read" len bytes_read; reader_closed t; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; connection_is_shutdown t; Alcotest.(check (option response_error)) "Response error" (Some expected_error) !error; in test "HTTP/1.1 200\r\n\r\n" 12 (`Malformed_response ": char ' '"); let response = Response.create `OK ~headers:(Headers.encoding_fixed (-1)) in test (response_to_string response) 39 (`Invalid_response_body_length response); ;; let test_shutdown_hangs_response_body_read () = let got_eof = ref false in let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let response_handler expected_response response response_body = Alcotest.check (module Response) "expected response" expected_response response; let rec on_read _buffer ~off:_ ~len:_ = Body.Reader.schedule_read response_body ~on_eof ~on_read; and on_eof () = got_eof := true in Body.Reader.schedule_read response_body ~on_eof ~on_read in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler response) ~error_handler:no_error_handler in reader_ready t; write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; read_response t response; read_string t "five."; reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t; shutdown t; Alcotest.(check bool) "EOF delivered to the request body if the connection shuts down" true !got_eof; connection_is_shutdown t; ;; let test_response_arrives_before_body_uploaded () = let reader_woken_up = ref false in let eof_delivered = ref false in let continue_response = ref (fun () -> ()) in let request' = Request.create `GET "/" ~headers:(Headers.of_list ["content-length", "10"]) in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let t = create () in let response_handler continue_response expected_response response body = Alcotest.check (module Response) "expected response" expected_response response; let on_read _buffer ~off:_ ~len:_ = () and on_eof () = eof_delivered := true; in Body.Reader.schedule_read body ~on_eof ~on_read; continue_response := (fun () -> Body.Reader.schedule_read body ~on_eof ~on_read; continue_response := (fun () -> Body.Reader.schedule_read body ~on_eof ~on_read)) in let error_handler_called = ref false in let second_error_handler_called = ref false in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler continue_response response) ~error_handler:(fun _ -> error_handler_called := true;) in write_request t request'; writer_yielded t; Body.Writer.write_string body "hello"; write_string t "hello"; writer_yielded t; (* Never close the request body. *) reader_ready t; read_response t response; yield_writer t ignore; reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t; let str = "hello" in let len = String.length str in let input = Bigstringaf.of_string str ~off:0 ~len in let just_read = read t input ~off:0 ~len in Alcotest.(check int) "reads the first chunk" len just_read; reader_yielded t; yield_reader t (fun () -> reader_woken_up := true); (* Sneak in a 2nd request. *) let expected_response = Response.create `OK in let body' = request t request' ~flush_headers_immediately:true ~response_handler:(fun response _ -> Alcotest.check (module Response) "expected response" expected_response response) ~error_handler:(fun _ -> second_error_handler_called := true;) in Body.Writer.close body'; writer_yielded t; !continue_response (); Alcotest.(check bool) "Reader woken up" true !reader_woken_up; reader_ready t; reader_woken_up := false; let str = "hello" in let len = String.length str in let input = Bigstringaf.of_string str ~off:0 ~len in let just_read = read_eof t input ~off:0 ~len in Alcotest.(check int) "server EOF after delivering the entire response" len just_read; reader_closed t; !continue_response (); Alcotest.(check bool) "EOF delivered to the response body" true !eof_delivered; Alcotest.(check bool) "First error handler not called" false !error_handler_called; Alcotest.(check bool) "2nd Error handler called" true !second_error_handler_called; connection_is_shutdown t; ;; let test_race_condition_writer_issues_yield_after_reader_eof () = let reader_woken_up = ref false in let request' = Request.create ~headers:(Headers.of_list [ "content-length", "5" ]) `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let t = create () in let response_handler expected_response response _body = Alcotest.check (module Response) "expected response" expected_response response; in let body = request t request' ~flush_headers_immediately:true ~response_handler:(response_handler response) ~error_handler:(fun _ -> ()) in write_request t request'; Body.Writer.write_string body "hello"; Body.Writer.flush body ignore; write_string t "hello"; reader_ready t; read_response t response; read_string t (String.make 5 'a'); Body.Writer.close body; reader_yielded t; yield_reader t (fun () -> reader_woken_up := true; ignore @@ read_eof t Bigstringaf.empty ~off:0 ~len:0; reader_closed t); writer_closed t; Alcotest.(check bool) "Reader woken up" true !reader_woken_up; connection_is_shutdown t; ;; let test_multiple_responses_in_single_read () = let res_handled = ref 0 in let request' = Request.create ~headers:(Headers.of_list [ "content-length", "0" ]) `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "0"]) `OK in let t = create () in let response_handler _response _body = res_handled := !res_handled + 1; in let body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; let body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; writer_yielded t; Body.Writer.close body; reader_ready t; let responses = response_to_string response ^ response_to_string response in read_string t responses; reader_ready t; Alcotest.(check int) "fired handler of both requests" 2 !res_handled ;; let test_chunked_error () = let error_handler_called = ref false in let request' = Request.create ~headers:(Headers.of_list [ "transfer-encoding", "chunked" ]) `GET "/" in let t = create () in let response_handler _response _body = assert false in let body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:(fun _ -> error_handler_called := true) in write_request t request'; Body.Writer.write_string body "hello"; write_string t "5\r\nhello\r\n"; report_exn t (Failure "something went wrong"); Alcotest.(check bool) "First error handler called" true !error_handler_called; Alcotest.(check bool) "request body is closed" true (Body.Writer.is_closed body); writer_closed t; connection_is_shutdown t; error_handler_called := false; let t = create () in let response = Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:(fun _ -> error_handler_called := true) in write_request t request'; Body.Writer.write_string body "hello"; Body.Writer.flush body ignore; write_string t "5\r\nhello\r\n"; read_response t response; report_exn t (Failure "something went wrong"); Alcotest.(check bool) "First error handler called" true !error_handler_called; Alcotest.(check bool) "request body is closed" true (Body.Writer.is_closed body); writer_closed t; connection_is_shutdown t; ;; let test_304_not_modified () = let error_handler_called = ref false in let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list ["content-length", "5"]) `Not_modified in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:(fun _ -> error_handler_called := true) in Body.Writer.close body; write_request t request'; read_response t response; let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; connection_is_shutdown t; Alcotest.(check bool) "error handler not called" false !error_handler_called; ;; let test_empty_content_length_body_closed () = let response = Response.create `OK in let request' = Request.create `GET "/" in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Alcotest.(check bool) "request body length is 0" true ((Request.body_length request') = (`Fixed 0L)); Alcotest.(check bool) "Request body starts out closed" true (Body.Writer.is_closed body); write_request t request'; read_response t response; let c = read_eof t Bigstringaf.empty ~off:0 ~len:0 in Alcotest.(check int) "read_eof with no input returns 0" 0 c; connection_is_shutdown t; ;; let test_schedule_read_with_data_available () = let request' = Request.create `GET "/" in let response = Response.create `OK ~headers:(Headers.encoding_fixed 6) in let t = create () in let body = ref None in let response_handler response' body' = body := Some body'; Alcotest.check (module Response) "expected response" response response'; in let req_body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in Body.Writer.close req_body; write_request t request'; writer_yielded t; read_response t response; let body = Option.get !body in let schedule_read expected = let did_read = ref false in Body.Reader.schedule_read body ~on_read:(fun buf ~off ~len -> let actual = Bigstringaf.substring buf ~off ~len in did_read := true; Alcotest.(check string) "Body" expected actual) ~on_eof:(fun () -> assert false); Alcotest.(check bool) "on_read called" true !did_read; in (* We get some data on the connection, but not the full response yet. *) read_string t "Hello"; reader_yielded t; (* Schedule a read when there is already data available. on_read should be called straight away, as who knows how long it'll be before more data arrives. *) schedule_read "Hello"; read_string t "!"; schedule_read "!"; let did_eof = ref false in Body.Reader.schedule_read body ~on_read:(fun _ ~off:_ ~len:_ -> Alcotest.fail "Expected eof") ~on_eof:(fun () -> did_eof := true); Alcotest.(check bool) "on_eof called" true !did_eof; ;; let test_dont_flush_headers_immediately () = let request' = Request.create `GET "/" ~headers:Headers.(of_list ["content-length", "5"]) in let response = Response.create `OK ~headers:Headers.encoding_chunked in let t = create () in let body = request t request' ~flush_headers_immediately:false ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in (* Writer yields when `~flush_headers_immediately` is false *) writer_yielded t; (* Write to the body *) Body.Writer.write_string body "Hello"; Body.Writer.close body; (* Request line + body batched together. *) write_string t "GET / HTTP/1.1\r\ncontent-length: 5\r\n\r\nHello"; read_response t response; read_string t "d\r\nHello, world!\r\n0\r\n\r\n"; ;; let test_pipelining_no_immediate_flush () = let writer_woken_up = ref false in let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list [ "content-length", "0" ]) `OK in let t = create () in let body = request t ~flush_headers_immediately:false request' ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); Body.Writer.close body; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; writer_woken_up := false; write_request t request'; (* send the 2nd request without reading the response *) let response' = Response.create ~headers:(Headers.of_list [ "content-length", "0" ]) `Not_found in let body' = request t request' ~flush_headers_immediately:false ~response_handler:(fun response body -> (default_response_handler response' response body)) ~error_handler:no_error_handler in writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); Body.Writer.close body'; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; writer_woken_up := false; write_request t request'; read_response t response; read_response t response'; ;; let test_flush_on_close () = let writer_woken_up = ref false in let request' = Request.create `GET "/" in let response = Response.create ~headers:(Headers.of_list [ "content-length", "0" ]) `OK in let t = create () in writer_yielded t; yield_writer t (fun () -> writer_woken_up := true); let body = request t ~flush_headers_immediately:false request' ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Alcotest.(check bool) "Writer woken up" true !writer_woken_up; Body.Writer.close body; write_request t request'; read_response t response; ;; let test_request_pipelining_async () = let request' = Request.create `GET "/" ~headers:(Headers.of_list [ "content-length", "5" ]) in let response = Response.create ~headers:(Headers.of_list [ "content-length", "5" ]) `OK in let t = create () in let body = request t request' ~response_handler:(default_response_handler response) ~error_handler:no_error_handler in Body.Writer.write_string body "hello"; Body.Writer.close body; let body' = request t request' ~response_handler:(fun response body -> (default_response_handler response response body)) ~error_handler:no_error_handler in Body.Writer.write_string body' "hello"; Body.Writer.close body'; let req_string = request_to_string request' ^ "hello" in write_string t req_string; write_string t req_string; read_response t response; read_string t "hello"; read_response t response; read_string t "hello"; shutdown t; connection_is_shutdown t; ;; let test_request_pipelining_single_read () = let request' = Request.create `GET "/" ~headers:(Headers.of_list [ "content-length", "5" ]) in let response = Response.create ~headers:(Headers.of_list [ "content-length", "5" ]) `OK in let handlers_called = ref 0 in let response_handler actual_response body = incr handlers_called; Alcotest.check (module Response) "expected response" response actual_response; let rec on_read _ ~off:_ ~len:_ = Body.Reader.schedule_read body ~on_read ~on_eof; and on_eof () = () in Body.Reader.schedule_read body ~on_read ~on_eof; in let t = create () in let body = request t request' ~response_handler ~error_handler:no_error_handler in Body.Writer.write_string body "hello"; Body.Writer.close body; let body' = request t request' ~response_handler ~error_handler:no_error_handler in let req_string = request_to_string request' in write_string t (req_string ^ "hello"); write_string t req_string; let resp_string = response_to_string response in read_string t (resp_string ^ "hello" ^ resp_string) ; Alcotest.(check int) "Both response handlers called" 2 !handlers_called; writer_yielded t; let writer_woken_up = ref false in yield_writer t (fun () -> writer_woken_up := true; write_string t "hello"); Body.Writer.write_string body' "hello"; Body.Writer.close body'; Alcotest.(check bool) "Writer woken up" true !writer_woken_up; ;; let test_flush_response_before_shutdown () = let request' = Request.create `GET "/" ~headers:Headers.encoding_chunked in let response_handler _response _body = assert false in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; Body.Writer.write_string body "hello world"; shutdown t; Alcotest.(check bool) "Shutting down closes the request body" true (Body.Writer.is_closed body); writer_closed t; connection_is_shutdown t ;; let test_report_exn_during_body_read () = let request' = Request.create `GET "/" in let response = Response.create `OK in let body_done = ref false in let error_message = ref None in let t = create () in let body = request t request' ~response_handler:(fun _ body -> let on_read _ ~off:_ ~len:_ = () in let on_eof () = body_done := true in Body.Reader.schedule_read body ~on_read ~on_eof) ~error_handler:(function | `Exn (Failure msg) -> Alcotest.(check bool) "body is not complete" false !body_done; error_message := Some msg | _ -> assert false) in Body.Writer.close body; write_request t request'; writer_yielded t; reader_ready t; read_response t response; report_exn t (Failure "something went wrong"); connection_is_shutdown t; Alcotest.(check (option string)) "something went wrong" (Some "something went wrong") !error_message; Alcotest.(check bool) "body is complete" true !body_done; ;; let test_can_read_response_after_write_eof () = let request' = Request.create `GET "/" ~headers:Headers.encoding_chunked in let resp = ref None in let response_handler response body = resp := Some response; Body.Reader.close body; in let t = create () in let _body' = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; let response = Response.create `OK ~headers:(Headers.encoding_fixed 0) in report_write_result t `Closed; writer_closed t; read_response t response; reader_ready t; Alcotest.(check bool) "response read and handler called" true (Option.is_some !resp); ;; let test_read_response_before_shutdown () = let request' = Request.create `GET "/" ~headers:Headers.encoding_chunked in let response_received = ref false in let response_handler _response _body = response_received := true; in let t = create () in let body = request t request' ~flush_headers_immediately:true ~response_handler ~error_handler:no_error_handler in write_request t request'; Body.Writer.write_string body "hello world"; read_response t (Response.create ~headers:(Headers.encoding_fixed 0) `OK); Alcotest.(check bool) "Response handler called" true !response_received; shutdown t; Alcotest.(check bool) "Shutting down closes the request body" true (Body.Writer.is_closed body); writer_closed t; connection_is_shutdown t; ;; let tests = [ "commit parse after every header line", `Quick, test_commit_parse_after_every_header ; "GET" , `Quick, test_get ; "HEAD" , `Quick, test_head ; "send streaming body", `Quick, test_send_streaming_body ; "Response EOF", `Quick, test_response_eof ; "Response header order preserved", `Quick, test_response_header_order ; "report_exn" , `Quick, test_report_exn ; "input_shrunk", `Quick, test_input_shrunk ; "multiple GET, last request closes connection", `Quick, test_get_last_close ; "Persistent connection, multiple GETs", `Quick, test_persistent_connection_requests ; "Persistent connection, request pipelining", `Quick, test_persistent_connection_requests_pipelining ; "Persistent connection, first request includes body", `Quick, test_persistent_connection_requests_pipelining_send_body ; "Persistent connections, read response body", `Quick, test_persistent_connection_requests_body ; "Partial input", `Quick, test_partial_input ; "Empty fixed body shuts down writer", `Quick, test_empty_fixed_body ; "Empty fixed body in a persistent connection doesn't shut down the writer", `Quick, test_empty_fixed_body_persistent_connection ; "Fixed body shuts down writer if connection is not persistent", `Quick, test_fixed_body ; "Fixed body doesn't shut down the writer if connection is persistent",`Quick, test_fixed_body_persistent_connection ; "Client support for upgrading a connection", `Quick, test_client_upgrade ; "test yield when read isn't scheduled", `Quick, test_handling_backpressure_when_read_not_scheduled ; "test yield when read isn't scheduled, reader yields early", `Quick, test_handling_backpressure_when_read_not_scheduled_early_yield ; "EOF while a request is in the pipeline", `Quick, test_eof_with_another_pipelined_request ; "EOF after handler response body not closed", `Quick, test_eof_handler_response_body_not_closed ; "EOF after handler closed response body", `Quick, test_eof_handler_closed_response_body ; "Exception closes the reader (on a non-persistent connection)", `Quick, test_exception_closes_reader ; "Exception closes the reader (on a persistent connection)", `Quick, test_exception_closes_reader_persistent_connection ; "Exception while reading the response body", `Quick, test_exception_reading_response_body ; "Exception while reading the response body", `Quick, test_exception_reading_response_body_last_chunk ; "Aynchronous exception while reading the response body", `Quick, test_async_exception_reading_response_body ; "failed response parse", `Quick, test_failed_response_parse ; "shutdown delivers eof to response bodies", `Quick, test_shutdown_hangs_response_body_read ; "full response arrives before uploading the entire request body, 2nd request in the pipeline", `Quick, test_response_arrives_before_body_uploaded ; "reader EOF race condition causes state machine to issue writer yield", `Quick, test_race_condition_writer_issues_yield_after_reader_eof ; "multiple responses in single read", `Quick, test_multiple_responses_in_single_read ; "test chunked error", `Quick, test_chunked_error ; "304 Not Modified with Content-Length", `Quick, test_304_not_modified ; "body for a request with no payload starts out closed", `Quick, test_empty_content_length_body_closed ; "schedule read with data available", `Quick, test_schedule_read_with_data_available ; "don't flush headers immediately", `Quick, test_dont_flush_headers_immediately ; "pipelining + don't flush headers immediately", `Quick, test_pipelining_no_immediate_flush ; "flushing on close with ~set_headers_immediately:false", `Quick, test_flush_on_close ; "request pipelining flushes request body", `Quick, test_request_pipelining_async ; "request pipelining both responses in single buffer", `Quick, test_request_pipelining_single_read ; "shutting down closes request bodies", `Quick, test_flush_response_before_shutdown ; "shut down closes request body ", `Quick, test_read_response_before_shutdown ; "report exn during body read", `Quick, test_report_exn_during_body_read ; "read response after write eof", `Quick, test_can_read_response_after_write_eof ]