mte/unikernel/duniverse/httpun/lib_test/test_server_connection.ml
2025-11-11 02:07:51 +01:00

2523 lines
88 KiB
OCaml

open Httpun
open Helpers
let trace fmt = Format.ksprintf (Format.printf "%s\n%!") fmt
let request_error_pp_hum fmt = function
| `Bad_request -> Format.fprintf fmt "Bad_request"
| `Bad_gateway -> Format.fprintf fmt "Bad_gateway"
| `Internal_server_error -> Format.fprintf fmt "Internal_server_error"
| `Exn exn -> Format.fprintf fmt "Exn (%s)" (Printexc.to_string exn)
;;
module Alcotest = struct
include Alcotest
let request_error = Alcotest.of_pp request_error_pp_hum
let request = Alcotest.of_pp (fun fmt req ->
Format.fprintf fmt "%s" (request_to_string req))
;;
end
module Runtime : sig
type t
val create
: ?config:Config.t
-> ?error_handler:Server_connection.error_handler
-> Server_connection.request_handler
-> t
val current_read_operation : t -> Read_operation.t
val current_write_operation : t -> Write_operation.t
val do_read : t -> (Server_connection.t -> 'a) -> 'a
val do_write : t -> (Server_connection.t -> Bigstringaf.t IOVec.t list -> 'a) -> 'a
(** Returns a [ref] that is set to [true] after the callback was fired *)
val on_reader_unyield : t -> (unit -> unit) -> bool ref
val on_writer_unyield : t -> (unit -> unit) -> bool ref
val report_exn : t -> exn -> unit
val with_server_connection : t -> (Server_connection.t -> 'a) -> 'a
val shutdown : t -> unit
val is_closed : t -> bool
end = struct
open Server_connection
type t =
{ server_connection : Server_connection.t
; mutable read_operation : [`Initial | Read_operation.t]
; mutable write_operation : [`Initial | Write_operation.t]
; read_loop : (unit -> unit)
; write_loop : (unit -> unit)
; mutable read_unyield_hook : (unit -> unit) option
; mutable write_unyield_hook : (unit -> unit) option
}
let rec read_step t =
match next_read_operation t.server_connection with
| `Read ->
trace "reader: Read";
t.read_operation <- `Read
| `Yield ->
trace "reader: Yield";
t.read_operation <- `Yield;
yield_reader t.server_connection (fun () ->
trace "reader: Yield callback";
read_step t;
t.read_unyield_hook |> Option.iter (fun f ->
t.read_unyield_hook <- None;
f ()))
| `Close ->
trace "reader: Close";
t.read_operation <- `Close
;;
let rec write_step t =
match next_write_operation t.server_connection with
| `Write xs ->
trace "writer: Write";
t.write_operation <- `Write xs
| `Yield ->
t.write_operation <- `Yield;
trace "writer: Yield";
yield_writer t.server_connection (fun () ->
trace "writer: Yield callback";
write_step t;
t.write_unyield_hook |> Option.iter (fun f ->
t.write_unyield_hook <- None;
f ()))
| `Close n ->
trace "writer: Close";
t.write_operation <- `Close n
;;
let create ?config ?error_handler request_handler =
let request_handler r =
trace "invoked: request_handler";
request_handler r
in
let error_handler =
Option.map (fun error_handler ?request ->
trace "invoked: error_handler";
error_handler ?request) error_handler
in
let rec t =
lazy (
{ server_connection = create ?config ?error_handler request_handler
; read_operation = `Initial
; write_operation = `Initial
; read_loop = (fun () -> read_step (Lazy.force_val t))
; write_loop = (fun () -> write_step (Lazy.force_val t))
; read_unyield_hook = None
; write_unyield_hook = None
})
in
let t = Lazy.force_val t in
t.read_loop ();
t.write_loop ();
t
;;
let current_read_operation t =
match t.read_operation with
| `Initial -> assert false
| `Read | `Yield | `Close as op -> op
;;
let current_write_operation t =
match t.write_operation with
| `Initial -> assert false
| `Write _ | `Yield | `Close _ as op -> op
;;
let do_read t f =
match current_read_operation t with
| `Read ->
trace "read: start";
let res = f t.server_connection in
trace "read: finished";
t.read_loop ();
res
| `Yield | `Close as op ->
Alcotest.failf "Read attempted during operation: %a"
Read_operation.pp_hum op
;;
let with_server_connection t f = f t.server_connection
let do_write t f =
match current_write_operation t with
| `Write bufs ->
trace "write: start";
let res = f t.server_connection bufs in
trace "write: finished";
t.write_loop ();
res
| `Yield | `Close _ as op ->
Alcotest.failf "Write attempted during operation: %a"
Write_operation.pp_hum op
;;
let on_reader_unyield t f =
let called = ref false in
assert (Option.is_none t.read_unyield_hook);
t.read_unyield_hook <- Some (fun () -> called := true; f ());
called
;;
let on_writer_unyield t f =
let called = ref false in
assert (Option.is_none t.write_unyield_hook);
t.write_unyield_hook <- Some (fun () -> called := true; f ());
called
;;
let report_exn t = Server_connection.report_exn t.server_connection
let shutdown t = Server_connection.shutdown t.server_connection
let is_closed t = Server_connection.is_closed t.server_connection
end
open Runtime
let read ?(eof=false) t str ~off ~len =
do_read t (fun conn ->
if eof
then Server_connection.read_eof conn str ~off ~len
else Server_connection.read conn str ~off ~len)
;;
let read_eof = read ~eof:true
let force_read_eof t str ~off ~len =
with_server_connection t (fun conn -> Server_connection.read_eof conn str ~off ~len)
;;
let feed_string ?eof t str =
let len = String.length str in
let input = Bigstringaf.of_string str ~off:0 ~len in
read ?eof t input ~off:0 ~len
;;
let read_string ?eof t str =
let c = feed_string ?eof t str in
Alcotest.(check int) "read consumes all input" (String.length str) c;
;;
let force_read t str ~off ~len =
with_server_connection t (fun conn -> Server_connection.read conn str ~off ~len)
;;
let force_feed_string t str =
let len = String.length str in
let input = Bigstringaf.of_string str ~off:0 ~len in
force_read t input ~off:0 ~len
;;
let force_read_string t str =
let c = force_feed_string t str in
Alcotest.(check int) "read consumes all input" (String.length str) c;
;;
let read_request ?eof t r =
let request_string = request_to_string r in
read_string ?eof t request_string
;;
let reader_ready ?(msg="Reader is ready") t =
Alcotest.check read_operation msg
`Read (current_read_operation t);
;;
let reader_yielded t =
Alcotest.check read_operation "Reader is in a yield state"
`Yield (current_read_operation t);
;;
let reader_closed ?(msg="Reader is closed") t =
Alcotest.check read_operation msg
`Close (current_read_operation t);
;;
let reader_errored = reader_closed ~msg:"Error shuts down the reader"
(* Checks that the [len] prefixes of expected and the write match, and returns
the rest. *)
let write_partial_string ?(msg="output written") t expected len =
do_write t (fun conn bufs ->
let actual =
String.sub (Write_operation.iovecs_to_string bufs) 0 len
in
Alcotest.(check string) msg (String.sub expected 0 len) actual;
Server_connection.report_write_result conn (`Ok len);
String.sub expected len (String.length expected - len));
;;
let write_string ?(msg="output written") t expected =
do_write t (fun conn bufs ->
let len = String.length expected in
let actual = Write_operation.iovecs_to_string bufs in
Alcotest.(check string) msg expected actual;
Server_connection.report_write_result conn (`Ok len));
;;
let write_response ?(msg="response written") ?body t r =
let response_string = response_to_string ?body r in
write_string ~msg t response_string
;;
let write_eof t =
do_write t (fun conn _ ->
Server_connection.report_write_result conn `Closed)
;;
let writer_ready t =
let is_write =
Alcotest.testable Write_operation.pp_hum (fun a b ->
match a, b with
| `Write _, `Write _ -> true
| _ -> false)
in
Alcotest.check is_write "Writer is ready"
(`Write []) (current_write_operation t);
;;
let writer_yielded t =
Alcotest.check write_operation "Writer is in a yield state"
`Yield (current_write_operation t);
;;
let writer_closed ?(unread = 0) t =
Alcotest.check write_operation "Writer is closed"
(`Close unread) (current_write_operation t);
;;
let connection_is_shutdown t =
reader_closed t;
writer_closed t;
;;
let connection_is_closed t =
Alcotest.(check bool) "connection is closed" true (is_closed t)
let raises_writer_closed f =
(* This is raised when you write to a closed [Faraday.t] *)
Alcotest.check_raises "raises because writer is closed"
(Failure "cannot write to closed writer") f
;;
let request_handler_with_body body reqd =
Body.Reader.close (Reqd.request_body reqd);
Reqd.respond_with_string reqd (Response.create `OK) body
;;
let default_request_handler reqd =
request_handler_with_body "" reqd
;;
let echo_handler response reqd =
let request_body = Reqd.request_body reqd in
let response_body = Reqd.respond_with_streaming reqd response in
let rec on_read buffer ~off ~len =
Body.Writer.write_string response_body (Bigstringaf.substring ~off ~len buffer);
Body.Writer.flush response_body (function
| `Closed -> assert false
| `Written -> Body.Reader.schedule_read request_body ~on_eof ~on_read)
and on_eof () =
print_endline "echo handler eof";
Body.Writer.close response_body
in
Body.Reader.schedule_read request_body ~on_eof ~on_read;
;;
let streaming_handler ?(flush=false) ?(error=false) response writes reqd =
let writes = ref writes in
let request_body = Reqd.request_body reqd in
Body.Reader.close request_body;
let body = Reqd.respond_with_streaming ~flush_headers_immediately:flush reqd response in
let rec write reason =
match reason with
| `Closed -> assert false
| `Written ->
match !writes with
| [] -> (match error with
| false -> Body.Writer.close body
| true -> Reqd.report_exn reqd (Failure "exn"))
| w :: ws ->
Body.Writer.write_string body w;
writes := ws;
Body.Writer.flush body write
in
write `Written;
;;
let synchronous_raise reqd =
Reqd.report_exn reqd (Failure "caught this exception")
;;
let error_handler ?request:_ _error start_response =
let resp_body = start_response Headers.empty in
Body.Writer.write_string resp_body "got an error";
Body.Writer.close resp_body
;;
let test_initial_reader_state () =
let t = create default_request_handler in
reader_ready ~msg:"A new reader wants input" t
;;
let test_reader_is_closed_after_eof () =
let t = create default_request_handler in
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 t = create default_request_handler in
let c = read t Bigstringaf.empty ~off:0 ~len:0 in
Alcotest.(check int) "read with no input returns 0" 0 c;
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_commit_parse_after_every_header () =
let t = create default_request_handler in
let request_line = "GET / HTTP/1.1\r\n" in
let single_header = "Links: /path/to/some/website\r\n" in
let r =
(* Each header is 30 bytes *)
request_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:30 in
Alcotest.(check int) "only reads the request line" (String.length request_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'';
write_response t (Response.create `OK);
connection_is_shutdown t;
;;
let test_single_get () =
(* Single GET *)
let t = create default_request_handler in
read_request t (Request.create `GET "/");
write_response t (Response.create `OK);
(* Single GET, close the connection *)
let t = create default_request_handler in
read_request t (Request.create `GET "/" ~headers:Headers.connection_close);
write_response t (Response.create `OK);
connection_is_shutdown t;
(* Single GET, with reponse body *)
let response_body = "This is a test" in
let t = create (request_handler_with_body response_body) in
read_request t (Request.create `GET "/" ~headers:Headers.connection_close);
write_response t
~body:response_body
(Response.create `OK);
connection_is_shutdown t;
;;
let test_asynchronous_response () =
let response_body = "hello, world!" in
let response_body_length = String.length response_body in
let response =
Response.create `OK ~headers:(Headers.encoding_fixed response_body_length) in
let continue = ref (fun () -> ()) in
let t = create (fun reqd ->
continue := fun () ->
Body.Reader.close (Reqd.request_body reqd);
let data = Bigstringaf.of_string ~off:0 ~len:response_body_length response_body in
let size = Bigstringaf.length data in
let response = Response.create `OK ~headers:(Headers.encoding_fixed size) in
let response_body =
Reqd.respond_with_streaming reqd response in
Body.Writer.write_bigstring response_body data;
Body.Writer.close response_body)
in
read_request t (Request.create `GET "/");
reader_yielded t;
writer_yielded t;
!continue ();
write_response t ~body:response_body response;
read_request t (Request.create `GET "/");
reader_yielded t;
writer_yielded t;
!continue ();
write_response t ~body:response_body response
;;
let test_echo_post () =
let request = Request.create `GET "/" ~headers:Headers.encoding_chunked in
(* Echo a single chunk *)
let response = Response.create `OK ~headers:Headers.encoding_chunked in
let t = create (echo_handler response) in
read_request t request;
read_string t "e\r\nThis is a test";
write_response t
~body:"e\r\nThis is a test\r\n"
response;
read_string t "\r\n0\r\n\r\n";
write_string t "0\r\n\r\n";
writer_yielded t;
(* Echo two chunks *)
let response = Response.create `OK ~headers:Headers.encoding_chunked in
let t = create (echo_handler response) in
read_request t request;
read_string t "e\r\nThis is a test";
write_response t
~body:"e\r\nThis is a test\r\n"
response;
read_string t "\r\n21\r\n... that involves multiple chunks";
write_string t "21\r\n... that involves multiple chunks\r\n";
read_string t "\r\n0\r\n\r\n";
write_string t "0\r\n\r\n";
writer_yielded t;
(* Echo and close *)
let response =
Response.create `OK ~headers:Headers.connection_close
in
let t = create (echo_handler response) in
read_request t request;
read_string t "e\r\nThis is a test";
write_response t
~body:"This is a test"
response;
read_string t "\r\n21\r\n... that involves multiple chunks";
write_string t "... that involves multiple chunks";
read_string t "\r\n0\r\n\r\n";
connection_is_shutdown t;
;;
let test_streaming_response () =
let request = Request.create `GET "/" in
let response = Response.create `OK in
let t = create (streaming_handler response ["Hello "; "world!"]) in
read_request t request;
write_response t
~body:"Hello "
response;
write_string t "world!";
writer_yielded t;
;;
let test_asynchronous_streaming_response () =
let request = Request.create `GET "/" ~headers:Headers.connection_close in
let response = Response.create `OK in
let body = ref None in
let t = create (fun reqd ->
body := Some (Reqd.respond_with_streaming reqd response))
in
writer_yielded t;
let writer_woken_up =
on_writer_unyield t (fun () ->
write_response t ~body:"Hello " response)
in
read_request t request;
let body =
match !body with
| None -> failwith "no body found"
| Some body -> body
in
Body.Writer.write_string body "Hello ";
Alcotest.(check bool) "Writer not woken up" false !writer_woken_up;
Body.Writer.flush body ignore;
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
writer_yielded t;
let writer_woken_up =
on_writer_unyield t (fun () ->
write_string t "world!";
writer_closed t)
in
Body.Writer.write_string body "world!";
Alcotest.(check bool) "Writer not woken up" false !writer_woken_up;
Body.Writer.close body;
Alcotest.(check bool) "Writer woken up" true !writer_woken_up
;;
let test_asynchronous_streaming_response_with_immediate_flush () =
let request = Request.create `GET "/" ~headers:Headers.connection_close in
let response = Response.create `OK in
let body = ref None in
let t = create (fun reqd ->
body := Some (Reqd.respond_with_streaming reqd response ~flush_headers_immediately:true))
in
writer_yielded t;
let writer_woken_up =
on_writer_unyield t (fun () ->
write_response t response);
in
Alcotest.(check bool) "Writer not woken up"
false !writer_woken_up;
read_request t request;
let body =
match !body with
| None -> failwith "no body found"
| Some body -> body
in
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
writer_yielded t;
let writer_woken_up =
on_writer_unyield t (fun () ->
writer_closed t)
in
Body.Writer.close body;
Alcotest.(check bool) "Writer woken up" true !writer_woken_up
;;
let test_empty_fixed_streaming_response () =
let request = Request.create `GET "/" in
let response = Response.create `OK ~headers:(Headers.encoding_fixed 0) in
let t = create (streaming_handler response []) in
read_request t request;
write_response t response;
writer_yielded t;
;;
let test_empty_chunked_streaming_response () =
let request = Request.create `GET "/" in
let response = Response.create `OK ~headers:Headers.encoding_chunked in
let t = create (streaming_handler response []) in
read_request t request;
write_response t response
~body:"0\r\n\r\n";
writer_yielded t;
;;
let test_multiple_get () =
let t = create default_request_handler in
read_request t (Request.create `GET "/");
write_response t (Response.create `OK);
read_request t (Request.create `GET "/");
write_response t (Response.create `OK);
;;
let test_asynchronous_streaming_response_flush_immediately () =
let continue_response = ref (fun () -> ()) in
let request = Request.create `GET "/" in
let response = Response.create `OK in
let request_handler reqd =
let body = Reqd.respond_with_streaming ~flush_headers_immediately:true reqd response in
continue_response := (fun () ->
Body.Writer.write_string body "hello";
Body.Writer.close body)
in
let t = create request_handler in
read_request t request;
write_response t response;
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
!continue_response ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_string t "hello";
writer_yielded t;
;;
(* Writer doesn't get a chance to yield before incoming request. *)
let test_asynchronous_streaming_response_writer_doesnt_yield () =
let continue_response = ref (fun () -> ()) in
let request = Request.create `GET "/" in
let response = Response.create `OK in
let request_handler reqd =
continue_response := (fun () ->
let body = Reqd.respond_with_streaming reqd response in
Body.Writer.write_string body "hello";
Body.Writer.close body)
in
let t = create request_handler in
read_request t request;
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
!continue_response ();
write_response t ~body:"hello" response;
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
writer_yielded t;
;;
let test_connection_error () =
let t = create ~error_handler (fun _ -> assert false) in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
report_exn t (Failure "connection failure");
Alcotest.(check bool) "Writer woken up"
true !writer_woken_up;
write_response t
~msg:"Error response written"
(Response.create `Internal_server_error);
write_string t "got an error";
;;
let test_synchronous_error () =
let t = create ~error_handler synchronous_raise in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
read_request t (Request.create `GET "/");
reader_errored t;
Alcotest.(check bool) "Writer woken up"
true !writer_woken_up;
(* This shows up in two flushes because [Reqd] creates error reposnses with
[~flush_headers_immediately:true] *)
write_response t ~msg:"Error response written"
(Response.create `Internal_server_error);
write_string t "got an error";
;;
let test_synchronous_error_asynchronous_handling () =
let continue = ref (fun () -> ()) in
let error_handler ?request error start_response =
continue := (fun () ->
error_handler ?request error start_response)
in
let t = create ~error_handler synchronous_raise in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
read_request t (Request.create `GET "/");
reader_errored t;
writer_yielded t;
Alcotest.(check bool) "Writer not woken up"
false !writer_woken_up;
!continue ();
Alcotest.(check bool) "Writer woken up"
true !writer_woken_up;
(* This shows up in two flushes because [Reqd] creates error reposnses with
[~flush_headers_immediately:true] *)
write_response t ~msg:"Error response written"
(Response.create `Internal_server_error);
write_string t "got an error";
;;
let test_asynchronous_error () =
let continue = ref (fun () -> ()) in
let asynchronous_raise reqd =
continue := (fun () -> synchronous_raise reqd)
in
let t = create ~error_handler asynchronous_raise in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
read_request t (Request.create `GET "/");
Alcotest.(check bool) "Writer not woken up" false !writer_woken_up;
writer_yielded t;
reader_yielded t;
!continue ();
Alcotest.(check bool) "Writer woken up"
true !writer_woken_up;
(* This shows up in two flushes because [Reqd] creates error reposnses with
[~flush_headers_immediately:true] *)
write_response t ~msg:"Error response written"
(Response.create `Internal_server_error);
write_string t "got an error";
reader_errored t;
connection_is_shutdown t
;;
let test_asynchronous_error_asynchronous_handling () =
let continue_request = ref (fun () -> ()) in
let asynchronous_raise reqd =
continue_request := (fun () -> synchronous_raise reqd)
in
let continue_error = ref (fun () -> ()) in
let error_handler ?request error start_response =
continue_error := (fun () ->
error_handler ?request error start_response)
in
let t = create ~error_handler asynchronous_raise in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
read_request t (Request.create `GET "/");
Alcotest.(check bool) "Writer not woken up"
false !writer_woken_up;
writer_yielded t;
reader_yielded t;
!continue_request ();
Alcotest.(check bool) "Writer not woken up" false !writer_woken_up;
writer_yielded t;
!continue_error ();
Alcotest.(check bool) "Writer woken up"
true !writer_woken_up;
(* This shows up in two flushes because [Reqd] creates error responses with
[~flush_headers_immediately:true] *)
write_response t ~msg:"Error response written"
(Response.create `Internal_server_error);
write_string t "got an error";
reader_errored t;
;;
let test_asynchronous_error_asynchronous_response_body () =
let continue_request = ref (fun () -> ()) in
let asynchronous_raise reqd =
continue_request := (fun () -> synchronous_raise reqd)
in
let continue_error = ref (fun () -> ()) in
let error_handler ?request:_ _error start_response =
continue_error := (fun () ->
let resp_body = start_response Headers.empty in
continue_error := (fun () ->
Body.Writer.write_string resp_body "got an error";
Body.Writer.close resp_body))
in
let t = create ~error_handler asynchronous_raise in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
read_request t (Request.create `GET "/");
writer_yielded t;
reader_yielded t;
!continue_request ();
writer_yielded t;
!continue_error ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t
~msg:"Error response written"
(Response.create `Internal_server_error);
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
!continue_error ();
write_string t "got an error";
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
reader_errored t;
connection_is_shutdown t
;;
let test_error_while_parsing () =
let continue_error = ref (fun () -> ()) in
let error_handler ?request error start_response =
continue_error := (fun () ->
error_handler ?request error start_response)
in
let setup () =
let t = create ~error_handler (fun _ -> assert false) in
let n = feed_string t "GET / HTTP/1.1\r\n" in
Alcotest.(check int) "read bytes" 16 n;
reader_ready t;
report_exn t (Failure "runtime error during parse");
t
in
(* Handle before read *)
let t = setup () in
!continue_error ();
write_response t ~msg:"Error response written"
(Response.create `Internal_server_error);
write_string t "got an error";
writer_closed t;
(* XXX(dpatti): Runtime is in a read loop and must report something. I don't
know if this could ever deadlock or if that's a runtime concern. *)
reader_ready t;
let n = feed_string t "Host: localhost\r\n" in
(* NOTE(anmonteiro): we commit after parsing each single header, so this
differs from the upstream test. *)
Alcotest.(check int) "read bytes" 17 n;
reader_closed t;
(* Read before handle *)
let t = setup () in
reader_ready t;
let n = feed_string t "Host: localhost\r\n" in
Alcotest.(check int) "read bytes" 17 n;
reader_closed t;
!continue_error ();
write_response t ~msg:"Error response written"
(Response.create `Internal_server_error);
write_string t "got an error";
writer_closed t;
;;
let test_error_before_read () =
let request_handler _ = assert false in
let invoked_error_handler = ref false in
let error_handler ?request:_ _ _ =
invoked_error_handler := true;
in
let t = create ~error_handler request_handler in
report_exn t (Failure "immediate runtime error");
reader_ready t;
writer_yielded t;
(* XXX(dpatti): This seems wrong to me. Should we be sending responses when we
haven't even read any bytes yet? Maybe too much of an edge case to worry. *)
Alcotest.(check bool) "Error handler was invoked" true !invoked_error_handler;
;;
let test_error_left_unhandled () =
let error_handler ?request:_ _ _ = () in
let t = create ~error_handler (fun _ -> ()) in
read_request t (Request.create `GET "/");
report_exn t (Failure "runtime error");
(* If the error handler is invoked and does not try to complete a response,
the connection will hang. This is not necessarily desirable but rather a
tradeoff to let the user respond asynchronously. *)
reader_yielded t;
writer_yielded t;
;;
let test_chunked_encoding () =
let request_handler reqd =
let response = Response.create `OK ~headers:Headers.encoding_chunked in
let resp_body = Reqd.respond_with_streaming reqd response in
Body.Writer.write_string resp_body "First chunk";
Body.Writer.flush resp_body (function
| `Closed -> assert false
| `Written ->
Body.Writer.write_string resp_body "Second chunk";
Body.Writer.close resp_body);
in
let t = create ~error_handler request_handler in
writer_yielded t;
read_request t (Request.create `GET "/");
write_response t
~msg:"First chunk written"
~body:"b\r\nFirst chunk\r\n"
(Response.create `OK ~headers:Headers.encoding_chunked);
write_string t
~msg:"Second chunk"
"c\r\nSecond chunk\r\n";
write_string t
~msg:"Final chunk written"
"0\r\n\r\n";
reader_ready ~msg:"Keep-alive" t;
;;
let test_chunked_encoding_for_error () =
let error_handler ?request error start_response =
Alcotest.(check (option request)) "No parsed request"
None request;
Alcotest.(check request_error) "Request error"
`Bad_request error;
let body = start_response Headers.encoding_chunked in
Body.Writer.write_string body "Bad";
Body.Writer.flush body (function
| `Closed -> assert false
| `Written ->
Body.Writer.write_string body " request";
Body.Writer.close body);
in
let t = create ~error_handler (fun _ -> assert false) in
let c = feed_string t " X\r\n\r\n" in
Alcotest.(check int) "Partial read" 2 c;
write_response t
(Response.create `Bad_request ~headers:Headers.encoding_chunked);
write_string t "3\r\nBad\r\n";
write_string t "8\r\n request\r\n";
write_string t "0\r\n\r\n";
connection_is_shutdown t;
;;
let test_blocked_write_on_chunked_encoding () =
let request_handler reqd =
let response = Response.create `OK ~headers:Headers.encoding_chunked in
let resp_body = Reqd.respond_with_streaming reqd response in
Body.Writer.write_string resp_body "gets partially written";
Body.Writer.flush resp_body ignore;
(* Response body never gets closed but for the purposes of the test, that's
* OK. *)
in
let t = create ~error_handler request_handler in
writer_yielded t;
read_request t (Request.create `GET "/");
let response_bytes =
"HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n16\r\ngets partially written\r\n"
in
let second_write =
write_partial_string t ~msg:"first write" response_bytes 16
in
write_string t ~msg:"second write" second_write
;;
let test_respond_with_upgrade () =
let upgraded = ref false in
let upgrade_handler reqd =
Reqd.respond_with_upgrade reqd Headers.empty (fun () ->
upgraded := true)
in
let t = create ~error_handler upgrade_handler in
read_request t (Request.create `GET "/");
write_response ~msg:"Upgrade response written" t (Response.create `Switching_protocols);
Alcotest.(check bool) "Callback was called" true !upgraded;
reader_ready t;
;;
let test_unexpected_eof () =
let t = create default_request_handler in
read_request t (Request.create `GET "/");
write_eof t;
writer_closed t ~unread:19;
;;
let test_input_shrunk () =
let continue_response = ref (fun () -> ()) in
let error_handler ?request:_ _ = assert false in
let request_handler reqd =
Alcotest.(check (list (pair string string)))
"got expected headers"
[ "Host" , "example.com"
; "Connection" , "close"
; "Accept" , "application/json, text/plain, */*"
; "Accept-Language", "en-US,en;q=0.5" ]
(Headers.to_list (Reqd.request reqd).headers);
Body.Reader.close (Reqd.request_body reqd);
continue_response := (fun () ->
Reqd.respond_with_string reqd (Response.create `OK) "");
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
let writer_woken_up =
on_writer_unyield t (fun () ->
write_response t (Response.create `OK))
in
let len = feed_string t "GET /v1/b HTTP/1.1\r\nH" in
Alcotest.(check int) "partial read" 20 len;
read_string t "Host: example.com\r\n\
Connection: close\r\n\
Accept: application/json, text/plain, */*\r\n\
Accept-Language: en-US,en;q=0.5\r\n\r\n";
writer_yielded t;
Alcotest.(check bool) "Writer not woken up"
false !writer_woken_up;
reader_closed t;
!continue_response ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
writer_closed t;
;;
let malformed_request_string =
"GET / HTTP/1.1\r\nconnection: close\r\nX-Other-Header : shouldnt_have_space_before_colon\r\n\r\n"
let eof_request_string =
"GET / HTTP/1.1\r\nconnection: close\r\nX-Other-Header: EOF_after_this"
let basic_handler body reqd =
let request_body = Reqd.request_body reqd in
Body.Reader.close request_body;
Reqd.respond_with_string reqd (Response.create `OK) body;
;;
let test_malformed conn =
writer_yielded conn;
let writer_woken_up = on_writer_unyield conn ignore in
let len = String.length malformed_request_string in
let input = Bigstringaf.of_string malformed_request_string ~off:0 ~len in
let c = read conn input ~off:0 ~len in
Alcotest.(check bool) "read doesn't consume all input"
true (c < String.length malformed_request_string);
reader_errored conn;
Alcotest.(check bool) "Writer woken up" true !writer_woken_up
let test_malformed_request () =
let t = create ~error_handler (basic_handler "") in
test_malformed t;
write_response t (Response.create `Bad_request);
write_string t "got an error";
;;
let test_malformed_request_async () =
let continue = ref (fun () -> ()) in
let error_handler ?request:_ _error start_response =
let resp_body = start_response Headers.empty in
continue := (fun () ->
Body.Writer.write_string resp_body "got an error";
Body.Writer.close resp_body)
in
let t = create ~error_handler (basic_handler "") in
test_malformed t;
!continue ();
write_response t (Response.create `Bad_request);
write_string t "got an error";
;;
let test_malformed_request_async_multiple_errors () =
let continue = ref (fun () -> ()) in
let error_handler ?request:_ _error start_response =
let resp_body = start_response Headers.empty in
continue := (fun () ->
Body.Writer.write_string resp_body "got an error";
Body.Writer.close resp_body)
in
let t = create ~error_handler (basic_handler "") in
test_malformed t;
!continue ();
let len = String.length malformed_request_string in
let input = Bigstringaf.of_string malformed_request_string ~off:0 ~len in
let c = force_read t input ~off:0 ~len in
Alcotest.(check int) "read doesn't consume more input" 0 c;
write_response t (Response.create `Bad_request);
write_string t "got an error";
;;
let read_string_eof t str =
let len = String.length str in
let input = Bigstringaf.of_string str ~off:0 ~len in
read_eof t input ~off:0 ~len;
;;
let test_malformed_request_eof () =
let t = create ~error_handler (basic_handler "") in
test_malformed t;
write_response t (Response.create `Bad_request);
write_string t "got an error";
;;
let streaming_error_handler
?(headers=Headers.empty) continue_error
?request:_ _error start_response =
let resp_body = start_response headers in
continue_error := (fun () ->
Body.Writer.write_string resp_body "got an error\n";
Body.Writer.flush resp_body (function
| `Closed -> assert false
| `Written ->
continue_error := (fun () ->
Body.Writer.write_string resp_body "more output";
Body.Writer.close resp_body)))
;;
let test_malformed_request_streaming_error_response () =
let continue_error = ref (fun () -> ()) in
let error_handler ?request error start_response =
continue_error := (fun () ->
streaming_error_handler continue_error ?request error start_response)
in
let t = create ~error_handler (basic_handler "") in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
let c = read_string_eof t eof_request_string in
Alcotest.(check int) "read consumes all input"
(String.length eof_request_string) c;
reader_errored t;
!continue_error ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t
(Response.create `Bad_request ~headers:Headers.empty);
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
!continue_error ();
write_string t ~msg:"First part of the response body written" "got an error\n";
Alcotest.(check bool) "Writer woken up once more input is available"
true !writer_woken_up;
!continue_error ();
write_string t ~msg:"Rest of the error response written" "more output";
writer_closed t;
connection_is_shutdown t;
;;
let chunked_error_handler continue_error ?request:_ _error start_response =
let resp_body =
start_response (Headers.of_list ["transfer-encoding", "chunked"])
in
Body.Writer.write_string resp_body "chunk 1\n";
Body.Writer.flush resp_body (function
| `Closed -> assert false
| `Written ->
continue_error := (fun () ->
Body.Writer.write_string resp_body "chunk 2\n";
Body.Writer.flush resp_body (function
| `Closed -> assert false
| `Written ->
continue_error := (fun () ->
Body.Writer.write_string resp_body "chunk 3\n";
Body.Writer.close resp_body))))
;;
let test_malformed_request_chunked_error_response () =
let continue_error = ref (fun () -> ()) in
let error_handler ?request error start_response =
continue_error := (fun () ->
chunked_error_handler continue_error ?request error start_response)
in
let t = create ~error_handler (basic_handler "") in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
let c = read_string_eof t eof_request_string in
Alcotest.(check int) "read consumes all input"
(String.length eof_request_string) c;
reader_errored t;
Alcotest.(check bool) "Writer hasn't woken up yet" false !writer_woken_up;
!continue_error ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t
~msg:"First chunk written"
(Response.create `Bad_request
~headers:(Headers.of_list ["transfer-encoding", "chunked"]));
write_string t "8\r\nchunk 1\n\r\n";
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
!continue_error ();
write_string t
~msg:"Second chunk"
"8\r\nchunk 2\n\r\n";
!continue_error ();
write_string t
~msg:"Second chunk"
"8\r\nchunk 3\n\r\n";
write_string t
~msg:"Final chunk written"
"0\r\n\r\n";
Alcotest.(check bool) "Writer woken up once more input is available"
true !writer_woken_up;
writer_closed t;
connection_is_shutdown t;
;;
(* This may happen when writing an asynchronous error response on a broken
* pipe. *)
let test_malformed_request_double_report_exn () =
let continue_error = ref (fun () -> ()) in
let error_handler ?request error start_response =
continue_error := (fun () ->
streaming_error_handler continue_error ?request error start_response)
in
let t = create ~error_handler (basic_handler "") in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
let c = read_string_eof t eof_request_string in
Alcotest.(check int) "read consumes all input"
(String.length eof_request_string) c;
reader_errored t;
Alcotest.(check bool) "Writer hasn't woken up yet" false !writer_woken_up;
!continue_error ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_eof t;
report_exn t (Failure "broken pipe");
writer_closed t ~unread:28;
connection_is_closed t;
;;
let test_immediate_flush_empty_body () =
let response = Response.create `OK in
let request_handler reqd =
let resp_body = Reqd.respond_with_streaming
~flush_headers_immediately:true reqd response
in
Body.Writer.close resp_body;
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/");
write_response t response
;;
let test_empty_body_no_immediate_flush () =
let response = Response.create `OK in
let request_handler reqd =
let resp_body = Reqd.respond_with_streaming
~flush_headers_immediately:false reqd response
in
Body.Writer.close resp_body;
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/");
write_response t ~body:"" response;
writer_yielded t;
;;
let test_yield_before_starting_a_response () =
let response = Response.create `OK in
let continue_response = ref (fun () -> ()) in
let request_handler reqd =
Body.Reader.close (Reqd.request_body reqd);
continue_response := (fun () ->
let resp_body = Reqd.respond_with_streaming reqd response in
Body.Writer.close resp_body)
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/");
reader_yielded t;
let reader_woken_up = on_reader_unyield t ignore in
Alcotest.(check bool) "Reader hasn't woken up yet" false !reader_woken_up;
!continue_response ();
write_response t ~body:"" response;
writer_yielded t;
Alcotest.(check bool) "Reader woken up" true !reader_woken_up;
;;
let test_respond_before_reading_entire_body () =
let response = Response.create `OK in
let continue_response = ref (fun () -> ()) in
let request_handler reqd =
(* Important that we never close the request body for this test. *)
continue_response := (fun () ->
let resp_body = Reqd.respond_with_streaming reqd response in
Body.Writer.close resp_body)
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["content-length", "2"]));
reader_yielded t;
let reader_woken_up = on_reader_unyield t ignore in
Alcotest.(check bool) "Reader hasn't woken up yet" false !reader_woken_up;
!continue_response ();
write_response t ~body:"" response;
writer_yielded t;
Alcotest.(check bool) "Reader woken up" true !reader_woken_up;
;;
let backpressure_request_handler continue_reading reqd =
let request_body = Reqd.request_body reqd in
let rec on_read _buffer ~off:_ ~len:_ =
continue_reading := (fun () ->
Body.Reader.schedule_read request_body ~on_eof ~on_read);
and on_eof () = print_endline ("got eof" ^ (string_of_bool (Body.Reader.is_closed request_body))) in
Body.Reader.schedule_read request_body ~on_eof ~on_read
let test_handling_backpressure_when_read_not_scheduled () =
let continue_reading = ref (fun () -> ()) in
let t = create ~error_handler (backpressure_request_handler continue_reading) in
reader_ready t;
writer_yielded t;
let request =
Request.create
`GET
~headers:(Headers.of_list ["content-length", "10"])
"/"
in
read_request t request;
read_string t "five.";
reader_yielded t;
let reader_woken_up = on_reader_unyield t ignore in
!continue_reading ();
Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up;
reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t;
writer_yielded t;
;;
let test_handling_backpressure_when_read_not_scheduled_early_yield () =
let continue_reading = ref (fun () -> ()) in
let t = create ~error_handler (backpressure_request_handler continue_reading) in
reader_ready t;
writer_yielded t;
let request =
Request.create
`GET
~headers:(Headers.of_list ["content-length", "10"])
"/"
in
read_request t request;
let reader_woken_up = on_reader_unyield t ignore in
read_string t "five.";
reader_yielded t;
!continue_reading ();
Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up;
reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t;
writer_yielded t;
;;
let test_input_shrunk_chunked () =
let continue_response = ref (fun () -> ()) in
let error_handler ?request:_ _ = assert false in
let request_handler reqd =
Alcotest.(check (list (pair string string)))
"got expected headers"
[ "Host" , "example.com"
; "Transfer-Encoding", "chunked" ]
(Headers.to_list (Reqd.request reqd).headers);
Body.Reader.close (Reqd.request_body reqd);
continue_response := (fun () ->
Reqd.respond_with_string reqd (Response.create `OK) "");
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
(* never happens*)
let _writer_woken_up =
on_writer_unyield t (fun () -> write_response t (Response.create `OK))
in
let len = feed_string t "GET /v1/b HTTP/1.1\r\nH" in
Alcotest.(check int) "partial read" 20 len;
read_string t "Host: example.com\r\nTransfer-Encoding: chunked\r\n\r\n";
let str = "5\r\ninput\r\n" in
let len = String.length str in
let input = Bigstringaf.of_string str ~off:0 ~len in
let just_read = force_read t input ~off:0 ~len in
Alcotest.(check int) "partial read" (len - 2) just_read;
let just_read = force_read_eof t input ~off:(len - 2) ~len:2 in
Alcotest.(check int) "eof partial read, doesn't get terminating chunk" 2 just_read;
writer_yielded t;
(* TODO: test error handling. *)
(* reader_closed t;
!continue_response ();
writer_closed t; *)
;;
let test_respond_before_reading_entire_body_chunked_eof () =
let response = Response.create `OK in
let continue_response = ref (fun () -> ()) in
let request_handler reqd =
(* Important that we never close the request body for this test. *)
continue_response := (fun () ->
let resp_body = Reqd.respond_with_streaming reqd response in
Body.Writer.close resp_body)
in
let error_handler ?request:_ _error _start_response =
Alcotest.fail "Expected error_handler not to be called because the response was already sent"
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["transfer-encoding", "chunked"]));
let reader_woken_up = on_reader_unyield t ignore in
Alcotest.(check bool) "Reader hasn't woken up yet" false !reader_woken_up;
let writer_woken_up = on_writer_unyield t ignore in
!continue_response ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t ~body:"" response;
writer_yielded t;
Alcotest.(check bool) "Reader woken up" true !reader_woken_up;
reader_ready t;
let str = "5\r\ninput\r\n" 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) "malformed chunked encoding read completely" len just_read;
writer_closed t;
reader_errored t;
;;
let test_finish_response_after_read_eof () =
let response = Response.create `OK in
let continue_response = ref (fun () -> ()) in
let request_handler reqd =
(* Important that we never close the request body for this test. *)
let resp_body = Reqd.respond_with_streaming reqd response in
continue_response := (fun () ->
Body.Writer.close resp_body)
in
let error_handler ?request:_ _error _start_response =
Alcotest.fail "Expected error_handler not to be called because the response was already sent"
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["transfer-encoding", "chunked"]));
let reader_woken_up = on_reader_unyield t ignore in
Alcotest.(check bool) "Reader hasn't woken up yet" false !reader_woken_up;
let writer_woken_up = on_writer_unyield t ignore in
let str = "5\r\ninput\r\n" in
let len = String.length str in
let input = Bigstringaf.of_string str ~off:0 ~len in
let just_read = force_read_eof t input ~off:0 ~len in
Alcotest.(check int) "malformed chunked encoding read completely" len just_read;
let (_ : Read_operation.t) =
with_server_connection t (fun t -> Server_connection.next_read_operation t)
in
reader_errored t;
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t ~body:"" response;
!continue_response ();
writer_closed t;
reader_closed t;
;;
let test_respond_before_reading_entire_body_no_error () =
let response = Response.create `OK in
let continue_response = ref (fun () -> ()) in
let request_handler reqd =
(* Important that we never close the request body for this test. *)
continue_response := (fun () ->
let resp_body = Reqd.respond_with_streaming reqd response in
Body.Writer.close resp_body)
in
let error_handler ?request:_ _error _start_response = assert false in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["content-length", "10"]));
let reader_woken_up = on_reader_unyield t ignore in
Alcotest.(check bool) "Reader hasn't woken up yet" false !reader_woken_up;
let writer_woken_up = on_writer_unyield t ignore in
force_read_string t "data.";
!continue_response ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t ~body:"" response;
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
reader_ready t;
(* Yield writer before feeding eof.
*
* Note: writer here is done. It yields before we feed more to the reader
* to allow for it to complete. *)
read_string t "final";
(* Ready for the next request *)
reader_ready t;
Alcotest.(check bool) "Writer hasn't woken up yet, still yielding" false !writer_woken_up;
;;
let test_streaming_response_before_reading_entire_body_no_error () =
let response = Response.create `OK in
let continue_response = ref (fun () -> ()) in
let request_handler reqd =
(* Important that we never close the request body for this test. *)
continue_response := (fun () ->
let resp_body = Reqd.respond_with_streaming reqd response in
continue_response := (fun () ->
Body.Writer.write_string resp_body "hello";
Body.Writer.flush resp_body (function
| `Closed -> assert false
| `Written ->
continue_response := (fun () ->
Body.Writer.close resp_body))))
in
let error_handler ?request:_ _error _start_response = assert false in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["content-length", "10"]));
let reader_woken_up = on_reader_unyield t ignore in
Alcotest.(check bool) "Reader hasn't woken up yet" false !reader_woken_up;
let writer_woken_up = on_writer_unyield t ignore in
force_read_string t "data.";
!continue_response ();
Alcotest.(check bool) "Writer not woken up" false !writer_woken_up;
!continue_response ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t ~body:"hello" response;
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
!continue_response ();
(* Important that the writer wakes up after closing it, so that it gets a
* chance to close the request body, and thus advance the remaining request
* body bytes to prepare the parser for the next request. *)
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
writer_yielded t;
reader_ready t;
read_string t "final";
(* Ready for the next request *)
reader_ready t;
;;
let test_failed_request_parse () =
let error_handler_fired = ref false in
let error_handler ?request error start_response =
error_handler_fired := true;
Alcotest.(check (option request)) "No parsed request"
None request;
Alcotest.(check request_error) "Request error"
`Bad_request error;
start_response Headers.empty |> Body.Writer.close;
in
let request_handler _reqd = assert false in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
let len = feed_string t "GET /v1/b HTTP/1.1\r\nHost : example.com\r\n\r\n" in
(* Reads through the end of "Host" *)
Alcotest.(check int) "partial read" 24 len;
reader_closed t;
Alcotest.(check bool) "Error handler fired"
true !error_handler_fired;
Alcotest.(check bool) "Writer woken up"
true !writer_woken_up;
write_response t (Response.create `Bad_request);
;;
let test_bad_request () =
(* A `Bad_request is returned in a number of cases surrounding
transfer-encoding or content-length headers. *)
let request = Request.create `GET "/" ~headers:(Headers.encoding_fixed (-1)) in
let error_handler_fired = ref false in
let error_handler ?request:request' error start_response =
error_handler_fired := true;
Alcotest.(check (option request)) "Parsed request"
(Some request) request';
Alcotest.(check request_error) "Request error"
`Bad_request error;
start_response Headers.empty |> Body.Writer.close;
in
let request_handler _reqd = assert false in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
read_request t request;
reader_closed t;
Alcotest.(check bool) "Error handler fired"
true !error_handler_fired;
Alcotest.(check bool) "Writer woken up"
true !writer_woken_up;
write_response t (Response.create `Bad_request);
;;
let test_shutdown_hangs_request_body_read () =
let got_eof = ref false in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let rec on_read _buffer ~off:_ ~len:_ =
Body.Reader.schedule_read request_body ~on_eof ~on_read;
and on_eof () = got_eof := true in
Body.Reader.schedule_read request_body ~on_eof ~on_read
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
let request =
Request.create
`GET
~headers:(Headers.of_list ["content-length", "10"])
"/"
in
read_request t request;
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;
do_read t ignore;
connection_is_shutdown t;
;;
let test_finish_response_after_read_eof_well_formed () =
let response = Response.create ~headers:(Headers.of_list ["content-length", "5"]) `OK in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let rec on_read _buffer ~off:_ ~len:_ =
Body.Reader.schedule_read request_body ~on_eof ~on_read;
and on_eof = fun () ->
Reqd.respond_with_string reqd response "hello"
in
Body.Reader.schedule_read request_body ~on_eof ~on_read
in
let error_handler ?request:_ _error _start_response =
Alcotest.fail "Expected error_handler not to be called because the response was already sent"
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["content-length", "5"]));
let writer_woken_up = on_writer_unyield t ignore in
let str = "hello" in
let bs = Bigstringaf.of_string ~off:0 ~len:(String.length str) str in
let just_read = read_eof t bs ~off:0 ~len:(String.length str) in
Alcotest.(check int) "EOF read" (String.length str) just_read;
reader_closed t;
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t ~body:"hello" response;
connection_is_shutdown t;
;;
let test_finish_response_after_read_eof_malformed () =
let error_handler_called = ref false in
let response = Response.create ~headers:(Headers.of_list ["content-length", "5"]) `OK in
let continue_response = ref (fun () -> ()) in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let rec on_read _buffer ~off:_ ~len:_ =
Body.Reader.schedule_read request_body ~on_eof ~on_read;
and on_eof = (fun () ->
continue_response := (fun() ->
try Reqd.respond_with_string reqd response "hello"
with | exn -> Reqd.report_exn reqd exn))
in
Body.Reader.schedule_read request_body ~on_eof ~on_read
in
let error_handler ?request:_ _error start_response =
error_handler_called := true;
let body = start_response Headers.empty in
Body.Writer.close body
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["content-length", "10"]));
let writer_woken_up = on_writer_unyield t ignore in
let str = "hello" in
let bs = Bigstringaf.of_string ~off:0 ~len:(String.length str) str in
let just_read = read_eof t bs ~off:0 ~len:(String.length str) in
Alcotest.(check int) "EOF read" (String.length str) just_read;
reader_closed t;
Alcotest.(check bool) "Error handler was called" true !error_handler_called;
!continue_response ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response t ~body:"" (Response.create `Bad_request);
writer_closed t;
reader_closed t;
;;
let test_request_body_eof_response_not_sent () =
let error_handler_called = ref false in
let response = Response.create ~headers:(Headers.of_list ["content-length", "5"]) `OK in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let rec on_read _buffer ~off:_ ~len:_ =
Body.Reader.schedule_read request_body ~on_eof ~on_read;
and on_eof = fun () ->
try Reqd.respond_with_string reqd response "hello"
with | exn -> Reqd.report_exn reqd exn
in
Body.Reader.schedule_read request_body ~on_eof ~on_read
in
let error_handler ?request:_ _error start_response =
error_handler_called := true;
let body = start_response Headers.empty in
Body.Writer.close body
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["content-length", "10"]));
let str = "hello" in
let bs = Bigstringaf.of_string ~off:0 ~len:(String.length str) str in
let just_read = read_eof t bs ~off:0 ~len:(String.length str) in
Alcotest.(check int) "EOF read" (String.length str) just_read;
reader_closed t;
Alcotest.(check bool) "Error handler was called" true !error_handler_called;
write_response t ~body:"" (Response.create `Bad_request);
writer_closed t;
reader_closed t;
;;
let test_request_body_eof_response_not_sent_empty_eof () =
let error_handler_called = ref false in
let response = Response.create ~headers:(Headers.of_list ["content-length", "5"]) `OK in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let rec on_read _buffer ~off:_ ~len:_ =
Body.Reader.schedule_read request_body ~on_eof ~on_read;
and on_eof = fun () ->
try Reqd.respond_with_string reqd response "hello"
with | exn -> Reqd.report_exn reqd exn
in
Body.Reader.schedule_read request_body ~on_eof ~on_read
in
let error_handler ?request:_ _error start_response =
error_handler_called := true;
let body = start_response Headers.empty in
Body.Writer.close body
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t (Request.create `GET "/" ~headers:(Headers.of_list ["content-length", "10"]));
read_string t "hello";
let just_read = read_eof t Bigstringaf.empty ~off:0 ~len:0 in
Alcotest.(check int) "EOF read" 0 just_read;
reader_closed t;
Alcotest.(check bool) "Error handler was called" true !error_handler_called;
write_response t ~body:"" (Response.create `Bad_request);
writer_closed t;
reader_closed t;
;;
let test_race_condition_writer_issues_yield_after_reader_eof () =
let continue_response = ref (fun () -> ()) in
let response =
Response.create ~headers:(Headers.of_list ["content-length", "10"]) `OK
in
let response_handler reqd =
let request_body = Reqd.request_body reqd in
Body.Reader.schedule_read request_body
~on_eof:ignore
~on_read:(fun _ ~off:_ ~len:_ ->
Body.Reader.schedule_read request_body
~on_read:(fun _ ~off:_ ~len:_ -> ())
~on_eof:(fun () ->
let resp_body = Reqd.respond_with_streaming reqd response in
Body.Writer.write_string resp_body (String.make 10 'a');
Body.Writer.flush resp_body (function
| `Closed -> assert false
| `Written ->
continue_response := (fun () ->
Body.Writer.close resp_body))))
in
let t = create ~error_handler response_handler in
let request =
Request.create ~headers:(Headers.of_list [ "content-length", "5" ]) `GET "/"
in
read_request t request;
reader_ready t;
read_string t "hello";
write_response t ~body:(String.make 10 'a') response;
reader_yielded t;
let reader_woken_up = on_reader_unyield t (fun () ->
ignore @@ read_eof t Bigstringaf.empty ~off:0 ~len:0;
reader_closed t)
in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
!continue_response ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
writer_closed t;
Alcotest.(check bool) "Reader woken up" true !reader_woken_up;
(* Also wakes up the reader *)
connection_is_shutdown t;
;;
let test_multiple_requests_in_single_read () =
let response = Response.create `OK in
let t =
create (fun reqd ->
Reqd.respond_with_string reqd response "")
in
let reqs =
request_to_string (Request.create `GET "/") ^
request_to_string (Request.create `GET "/")
in
read_string t reqs;
write_string t (response_to_string response );
write_string t (response_to_string response );
;;
let test_multiple_async_requests_in_single_read () =
let response = Response.create `OK in
let reqs_handled = ref 0 in
let finish_handler = ref (fun () -> assert false) in
let t =
create (fun reqd ->
reqs_handled := !reqs_handled + 1;
finish_handler := (fun () ->
Reqd.respond_with_string reqd response ""))
in
let reqs =
request_to_string (Request.create `GET "/") ^
request_to_string (Request.create `GET "/")
in
read_string t reqs;
reader_yielded t;
writer_yielded t;
Alcotest.(check int) "fired handler once" 1 !reqs_handled;
!finish_handler ();
write_response t response;
Alcotest.(check int) "fired handler again" 2 !reqs_handled;
!finish_handler ();
write_response t response;
reader_ready t;
;;
let test_errored_chunked_streaming_response () =
let request = Request.create `GET "/" in
let response =
Response.create `OK
~headers:(Headers.of_list ["Transfer-encoding", "chunked"])
in
let t = create (streaming_handler ~error:true response []) in
read_request t request;
write_response t response;
connection_is_shutdown t;
;;
let test_multiple_requests_in_single_read_with_close () =
let response = Response.create `OK ~headers:Headers.connection_close in
let t =
create (fun reqd -> Reqd.respond_with_string reqd response "")
in
let reqs =
request_to_string (Request.create `GET "/") ^
request_to_string (Request.create `GET "/")
in
read_string t reqs;
write_response t response;
connection_is_shutdown t;
;;
let test_errored_content_length_streaming_response () =
let request = Request.create `GET "/" in
let response =
Response.create `OK
~headers:(Headers.of_list ["Content-Length", "10"])
in
let t = create (streaming_handler ~error:true response ["hello"]) in
read_request t request;
write_response t response ~body:"hello";
connection_is_shutdown t;
;;
let test_errored_chunked_streaming_response_async () =
let continue = ref (fun () -> ()) in
let request = Request.create `GET "/" in
let response =
Response.create `OK
~headers:(Headers.of_list ["Transfer-encoding", "chunked"])
in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
Body.Reader.close request_body;
let body = Reqd.respond_with_streaming reqd response in
Body.Writer.write_string body "hello";
Body.Writer.flush body (function
| `Closed -> assert false
| `Written ->
continue := (fun () ->
Reqd.report_exn reqd (Failure "heh")))
in
let t = create request_handler in
read_request t request;
write_response t response ~body:"5\r\nhello\r\n";
let reader_woken_up = on_reader_unyield t ignore in
let writer_woken_up = on_writer_unyield t ignore in
!continue ();
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 test_multiple_requests_in_single_read_with_eof () =
let response = Response.create `OK in
let t =
create (fun reqd -> Reqd.respond_with_string reqd response "")
in
let reqs =
request_to_string (Request.create `GET "/") ^
request_to_string (Request.create `GET "/")
in
read_string t reqs ~eof:true;
write_string t (response_to_string response);
write_string t (response_to_string response);
;;
let test_parse_failure_after_checkpoint () =
let error_queue = ref None in
let error_handler ?request:_ error _start_response =
Alcotest.(check (option reject)) "Error queue is empty" !error_queue None;
error_queue := Some error
in
let request_handler _reqd = assert false in
let t = create ~error_handler request_handler in
reader_ready t;
read_string t "GET index.html HTTP/1.1\r\n";
let result = feed_string t " index.html HTTP/1.1\r\n\r\n" in
Alcotest.(check int) "Bad header not consumed" result 0;
reader_closed t;
match !error_queue with
| None -> Alcotest.fail "Expected error"
| Some error -> Alcotest.(check request_error) "Error" error `Bad_request
;;
let test_parse_failure_at_eof () =
let error_queue = ref None in
let continue = ref (fun () -> ()) in
let error_handler ?request error start_response =
Alcotest.(check (option reject)) "Error queue is empty" !error_queue None;
Alcotest.(check (option reject)) "Request was not parsed" request None;
error_queue := Some error;
continue := (fun () ->
let resp_body = start_response Headers.empty in
Body.Writer.write_string resp_body "got an error";
Body.Writer.close resp_body);
in
let request_handler _reqd = assert false in
let t = create ~error_handler request_handler in
reader_ready t;
read_string t "GET index.html HTTP/1.1\r\n";
let result = feed_string ~eof:true t " index.html HTTP/1.1\r\n\r\n" in
Alcotest.(check int) "Bad header not consumed" result 0;
reader_closed t;
(match !error_queue with
| None -> Alcotest.fail "Expected error"
| Some error -> Alcotest.(check request_error) "Error" error `Bad_request);
!continue ();
write_response t (Response.create `Bad_request);
write_string t "got an error";
writer_closed t;
;;
let test_response_finished_before_body_read () =
let response = Response.create `OK ~headers:(Headers.encoding_fixed 4) in
let rev_body_chunks = ref [] in
let request_handler reqd =
let rec read_body () =
Body.Reader.schedule_read
(Reqd.request_body reqd)
~on_read:(fun buf ~off ~len ->
rev_body_chunks := Bigstringaf.substring buf ~off ~len :: !rev_body_chunks;
read_body ())
~on_eof:ignore;
in
read_body ();
Reqd.respond_with_string reqd response "done"
in
let t = create request_handler in
read_request t (Request.create `GET "/" ~headers:(Headers.encoding_fixed 12));
write_response t response ~body:"done";
(* Finish the request and send another *)
read_string t "hello, ";
read_string t "world";
Alcotest.(check (list string)) "received body" ["world"; "hello, "] !rev_body_chunks;
read_request t (Request.create `GET "/");
write_response t response ~body:"done";
;;
let test_pipelined_requests_answer_before_reading_body () =
let response = Response.create `OK ~headers:(Headers.encoding_fixed 0) in
let request_handler reqd =
let response =
Response.create ~headers:(Headers.of_list ["content-length", "0"]) `OK
in
Reqd.respond_with_string reqd response ""
in
let t = create request_handler in
read_request t (Request.create `GET "/" ~headers:(Headers.encoding_fixed 5));
write_response t response;
(* Finish the request and send another *)
read_string t "helloGET / HTTP/1.1\r\nhost: localhost\r\ncontent-length: 5\r\n\r\n";
write_response t response;
;;
let test_body_flush_fairness () =
let rev_body_chunks = ref [] in
let continue = ref (fun () -> ()) in
let t = create (fun reqd ->
let request_body = Reqd.request_body reqd in
let rec on_read buffer ~off ~len =
rev_body_chunks := (Bigstringaf.substring buffer ~off ~len) :: !rev_body_chunks;
continue := (fun () ->
Body.Reader.schedule_read request_body ~on_eof ~on_read)
and on_eof () = print_endline "got eof" in
continue := (fun () ->
Body.Reader.schedule_read request_body ~on_eof ~on_read))
in
read_request t (Request.create `GET "/" ~headers:(Headers.encoding_fixed 20));
reader_yielded t;
writer_yielded t;
let reader_woken_up = on_reader_unyield t ignore in
!continue ();
reader_ready t;
Alcotest.(check bool) "reader woken up" true !reader_woken_up;
read_string t "hello";
!continue ();
read_string t "hello";
reader_yielded t;
let reader_woken_up = on_reader_unyield t ignore in
force_read_string t "hello";
!continue ();
Alcotest.(check bool) "reader woken up" true !reader_woken_up;
Alcotest.(check int) "Request handler processed 3 chunks" 3 (List.length !rev_body_chunks);
;;
let test_shutdown_in_request_handler () =
let request = Request.create `GET "/" in
let rec t =
lazy (create (fun _ -> shutdown (Lazy.force t)))
in
let t = Lazy.force t in
read_request t request;
reader_closed t;
writer_closed t
;;
let test_shutdown_during_asynchronous_request () =
let request = Request.create `GET "/" in
let response = Response.create `OK in
let continue = ref (fun () -> ()) in
let t = create (fun reqd ->
continue := (fun () ->
Reqd.respond_with_string reqd response ""))
in
read_request t request;
shutdown t;
raises_writer_closed !continue;
reader_closed t;
writer_closed t
;;
let test_flush_response_before_shutdown () =
let request = Request.create `GET "/" ~headers:(Headers.encoding_fixed 0) in
let response = Response.create `OK ~headers:Headers.encoding_chunked in
let continue = ref (fun () -> ()) in
let request_handler reqd =
let body = Reqd.respond_with_streaming ~flush_headers_immediately:true reqd response in
continue := (fun () ->
Body.Writer.write_string body "hello world";
Body.Writer.close body);
in
let t = create request_handler in
read_request t request;
write_response t response;
!continue ();
shutdown t;
write_string t "b\r\nhello world\r\n";
connection_is_shutdown t
;;
let test_schedule_read_with_data_available () =
let response = Response.create `OK in
let body = ref None in
let continue = ref (fun () -> ()) in
let request_handler reqd =
body := Some (Reqd.request_body reqd);
continue := (fun () ->
Reqd.respond_with_string reqd response "")
in
let t = create request_handler in
read_request t (Request.create `GET "/" ~headers:(Headers.encoding_fixed 6));
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. *)
force_read_string t "Hello";
(* Schedule a read when there is already data available. on_read should be
called synchronously *)
schedule_read "Hello";
force_read_string t "!";
schedule_read "!";
(* Also works with eof *)
Body.Reader.schedule_read body
~on_read:(fun _ ~off:_ ~len:_ -> Alcotest.fail "Expected eof")
~on_eof:(fun () -> !continue ());
write_response t response;
;;
let test_eof_called_multiple_times () =
let continue_reading = ref (fun () -> ()) in
let eof_counter = ref 0 in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let rec on_read _buffer ~off:_ ~len:_ =
continue_reading := (fun () ->
Body.Reader.schedule_read request_body ~on_eof ~on_read);
and on_eof () =
incr eof_counter;
print_endline ("got eof" ^ (string_of_bool (Body.Reader.is_closed request_body))) in
Body.Reader.schedule_read request_body ~on_eof ~on_read
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
let request =
Request.create
`GET
~headers:(Headers.of_list ["content-length", "10"])
"/"
in
read_request t request;
read_string t "five.";
reader_yielded t;
let reader_woken_up = on_reader_unyield t ignore in
!continue_reading ();
Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up;
reader_ready ~msg:"Reader wants to read if there's a read scheduled in the body" t;
read_string t "more.";
reader_yielded t;
let reader_woken_up = on_reader_unyield t !continue_reading in
!continue_reading ();
Alcotest.(check bool) "Reader wakes up if scheduling read" true !reader_woken_up;
Alcotest.(check int) "`on_eof` only called once" 1 !eof_counter;
writer_yielded t;
;;
let test_eof_called_for_empty_bodies () =
let eof_counter = ref 0 in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let on_read _buffer ~off:_ ~len:_ = assert false
and on_eof () =
incr eof_counter;
in
Body.Reader.schedule_read request_body ~on_eof ~on_read
in
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
let request =
Request.create
`GET
~headers:(Headers.of_list ["content-length", "0"])
"/"
in
read_request t request;
reader_yielded t;
Alcotest.(check int) "`on_eof` only called once" 1 !eof_counter;
writer_yielded t;
shutdown t;
let t = create ~error_handler request_handler in
reader_ready t;
writer_yielded t;
read_request t request;
reader_yielded t;
Alcotest.(check int) "`on_eof` only called once" 2 !eof_counter;
writer_yielded t;
;;
let test_error_handler_chunked_response () =
let continue_error = ref (fun () -> ()) in
let error_handler ?request error start_response =
continue_error := (fun () ->
streaming_error_handler
~headers:(Headers.of_list ["transfer-encoding", "chunked"])
continue_error ?request
error start_response)
in
let t = create ~error_handler (basic_handler "") in
writer_yielded t;
let writer_woken_up = on_writer_unyield t ignore in
let c = read_string_eof t eof_request_string in
Alcotest.(check int) "read consumes all input"
(String.length eof_request_string) c;
reader_errored t;
Alcotest.(check bool) "Writer hasn't woken up yet" false !writer_woken_up;
!continue_error ();
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
write_response
t
(Response.create
~headers:(Headers.of_list ["transfer-encoding", "chunked"])
`Bad_request);
!continue_error ();
write_string t "d\r\ngot an error\n\r\n";
report_exn t (Failure "broken pipe");
writer_closed t;
connection_is_closed t;
;;
let test_pipelined_requests_in_single_buffer_partial_body () =
let response =
Response.create ~headers:(Headers.of_list ["content-length", "5"]) `OK
in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let response_body = Reqd.respond_with_streaming reqd response
in
let rec on_read buffer ~off ~len =
Body.Writer.write_bigstring response_body buffer ~off ~len;
Body.Reader.schedule_read request_body ~on_eof ~on_read
and on_eof () =
Body.Writer.close response_body
in
Body.Reader.schedule_read (Reqd.request_body reqd) ~on_eof ~on_read;
in
let t = create request_handler in
let req = (Request.create `POST "/" ~headers:(Headers.encoding_fixed 5)) in
let reqs =
request_to_string req ^ "hello" ^
request_to_string req
in
read_string t reqs;
reader_yielded t;
write_response t ~body:"hello" response;
write_response t response;
writer_yielded t;
reader_ready t;
read_string t "hello";
write_string t "hello";
reader_ready t;
;;
let test_multiple_pipelined_requests () =
let response =
Response.create ~headers:(Headers.of_list ["content-length", "5"]) `OK
in
let request_handler reqd =
let request_body = Reqd.request_body reqd in
let response_body = Reqd.respond_with_streaming reqd response
in
let rec on_read buffer ~off ~len =
Body.Writer.write_bigstring response_body buffer ~off ~len;
Body.Reader.schedule_read request_body ~on_eof ~on_read
and on_eof () = Body.Writer.close response_body
in
Body.Reader.schedule_read (Reqd.request_body reqd) ~on_eof ~on_read;
in
let t = create request_handler in
let req = (Request.create `POST "/" ~headers:(Headers.encoding_fixed 5)) in
read_string t (request_to_string req ^ "hello");
reader_yielded t;
write_response t ~body:"hello" response;
read_request t req;
writer_yielded t;
let writer_woken_up = on_writer_unyield t (fun () ->
write_response t ~body:"hello" response)
in
reader_ready t;
let reqs =
"hello" ^ request_to_string req ^ "hello"
in
read_string t reqs;
Alcotest.(check bool) "Writer woken up" true !writer_woken_up;
reader_yielded t;
write_response t ~body:"hello" response;
;;
let test_body_flush_after_bytes_in_the_wire () =
let response =
Response.create ~headers:(Headers.of_list ["content-length", "5"]) `OK
in
let callback_called = ref false in
let callback = function
| `Closed -> assert false
| `Written -> callback_called := true
in
let request_handler ~flush_headers_immediately reqd =
let response_body =
Reqd.respond_with_streaming
~flush_headers_immediately
reqd
response
in
Body.Writer.flush response_body callback
in
let t = create (request_handler ~flush_headers_immediately:true) in
let req = Request.create `GET "/" in
read_request t req ;
Alcotest.(check bool)
"flush callback isn't called until the response has been written"
false !callback_called;
write_response t response;
Alcotest.(check bool) "flush callback" true !callback_called;
shutdown t;
callback_called := false;
let t = create (request_handler ~flush_headers_immediately:false) in
let req = Request.create `GET "/" in
read_request t req;
Alcotest.(check bool)
"flush callback isn't called until the response has been written"
false !callback_called;
write_response t response;
Alcotest.(check bool) "flush callback" true !callback_called;
shutdown t;
connection_is_shutdown t;
;;
let test_input_consumed_before_closing_req_body () =
let request = Request.create `GET "/" ~headers:Headers.encoding_chunked in
let response = Response.create `OK ~headers:Headers.encoding_chunked in
let t = create (echo_handler response) in
read_request t request;
read_string t "e\r\nThis is a test";
write_response t
~body:"e\r\nThis is a test\r\n"
response;
read_string t "\r\n0\r\n";
writer_yielded t;
read_string t "\r\n";
write_string t "0\r\n\r\n";
;;
let test_can_read_more_requests_after_write_eof () =
let request = Request.create `GET "/" ~headers:(Headers.encoding_fixed 0) in
let reqd = ref None in
let request_handler reqd' = reqd := Some reqd' in
let t = create request_handler in
let response = Response.create `OK ~headers:Headers.encoding_chunked in
read_request t request;
Reqd.respond_with_streaming (Option.get !reqd) response ~flush_headers_immediately:true
|> (ignore : Body.Writer.t -> unit);
write_eof t;
(* In many runtimes, there are separate reader and writer threads that drive
the reading and writing from httpaf independently. So just because the
writer thread has told us that the socket is closed doesn't mean we won't
get a bunch more requests delivered to us from the reader thread. We
should be ready to receive them, and call the request handler for them,
even if there is no possibility of writing responses (e.g. those requests
might be side-effecting requests like POST requests). *)
reqd := None;
writer_closed ~unread:47 t;
reader_ready t;
read_request t request;
raises_writer_closed (fun () ->
Reqd.respond_with_streaming (Option.get !reqd) response ~flush_headers_immediately:true
|> (ignore : Body.Writer.t -> unit));
Alcotest.(check bool) "request handler fired" true (Option.is_some !reqd)
;;
let test_can_read_more_requests_after_write_eof_before_send_response () =
let request = Request.create `GET "/" ~headers:(Headers.encoding_fixed 0) in
let reqd = ref None in
let request_handler reqd' = reqd := Some reqd' in
let t = create request_handler in
let response = Response.create `OK ~headers:Headers.encoding_chunked in
read_request t request;
let write_op = ref (current_write_operation t) in
with_server_connection t (fun t ->
Server_connection.report_write_result t `Closed;
write_op := Server_connection.next_write_operation t;
);
Alcotest.check write_operation "Writer is closed"
(`Close 0) !write_op;
reader_ready t;
read_request t request;
raises_writer_closed (fun () ->
Reqd.respond_with_streaming (Option.get !reqd) response ~flush_headers_immediately:true
|> (ignore : Body.Writer.t -> unit));
Alcotest.(check bool) "request handler fired" true (Option.is_some !reqd)
;;
let test_write_response_after_read_eof () =
let reqd = ref None in
let t = create (fun _reqd -> reqd := Some _reqd) in
let request_line = "GET / HTTP/1.1\r\n" in
let single_header = "Links: /path/to/some/website\r\n" in
let r =
(* Each header is 30 bytes *)
request_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:30 in
Alcotest.(check int) "only reads the request line" (String.length request_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'';
writer_yielded t;
let body = Reqd.respond_with_streaming ~flush_headers_immediately:true (Option.get !reqd) (Response.create `OK);
in
write_response t (Response.create `OK);
Body.Writer.close body;
connection_is_shutdown t;
;;
let tests =
[ "initial reader state" , `Quick, test_initial_reader_state
; "shutdown reader closed", `Quick, test_reader_is_closed_after_eof
; "commit parse after every header line", `Quick, test_commit_parse_after_every_header
; "single GET" , `Quick, test_single_get
; "multiple GETs" , `Quick, test_multiple_get
; "asynchronous response" , `Quick, test_asynchronous_response
; "asynchronous response, asynchronous body", `Quick, test_asynchronous_streaming_response_flush_immediately
; "asynchronous response, asynchronous body, writer doesn't yield", `Quick, test_asynchronous_streaming_response_writer_doesnt_yield
; "echo POST" , `Quick, test_echo_post
; "streaming response" , `Quick, test_streaming_response
; "asynchronous streaming response", `Quick, test_asynchronous_streaming_response
; "asynchronous streaming response, immediate flush", `Quick, test_asynchronous_streaming_response_with_immediate_flush
; "empty fixed streaming response", `Quick, test_empty_fixed_streaming_response
; "empty chunked streaming response", `Quick, test_empty_chunked_streaming_response
; "connection error", `Quick, test_connection_error
; "synchronous error, synchronous handling", `Quick, test_synchronous_error
; "synchronous error, asynchronous handling", `Quick, test_synchronous_error_asynchronous_handling
; "asynchronous error, synchronous handling", `Quick, test_asynchronous_error
; "asynchronous error, asynchronous handling", `Quick, test_asynchronous_error_asynchronous_handling
; "asynchronous error, asynchronous handling + asynchronous body", `Quick, test_asynchronous_error_asynchronous_response_body
; "error while parsing", `Quick, test_error_while_parsing
; "error before read", `Quick, test_error_before_read
; "error left unhandled", `Quick, test_error_left_unhandled
; "chunked encoding", `Quick, test_chunked_encoding
; "chunked encoding for error", `Quick, test_chunked_encoding_for_error
; "blocked write on chunked encoding", `Quick, test_blocked_write_on_chunked_encoding
; "respond with upgrade", `Quick, test_respond_with_upgrade
; "writer unexpected eof", `Quick, test_unexpected_eof
; "input shrunk", `Quick, test_input_shrunk
; "malformed request", `Quick, test_malformed_request
; "malformed request (async)", `Quick, test_malformed_request_async
; "multiple malformed requests?", `Quick, test_malformed_request_async_multiple_errors
; "malformed request, chunked error response", `Quick, test_malformed_request_chunked_error_response
; "malformed request, double report_exn", `Quick, test_malformed_request_double_report_exn
; "malformed request (EOF)", `Quick, test_malformed_request_eof
; "malformed request, streaming response", `Quick, test_malformed_request_streaming_error_response
; "`flush_headers_immediately` with empty body", `Quick, test_immediate_flush_empty_body
; "empty body with no immediate flush", `Quick, test_empty_body_no_immediate_flush
; "yield before starting a response", `Quick, test_yield_before_starting_a_response
; "respond before body has been read", `Quick, test_respond_before_reading_entire_body
; "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
; "test partial input chunked body", `Quick, test_input_shrunk_chunked
; "respond before reading request body, then request body EOFs", `Quick, test_respond_before_reading_entire_body_chunked_eof
; "request body EOFs before closing response body, request body not closed", `Quick, test_finish_response_after_read_eof
; "respond before reading entire request body", `Quick, test_respond_before_reading_entire_body_no_error
; "respond before reading entire request body, streaming response", `Quick, test_streaming_response_before_reading_entire_body_no_error
; "failed request parse", `Quick, test_failed_request_parse
; "bad request", `Quick, test_bad_request
; "shutdown delivers eof to request bodies", `Quick, test_shutdown_hangs_request_body_read
; "request body eof, finish response immediately", `Quick, test_finish_response_after_read_eof_well_formed
; "request body eof, async response triggers error handler", `Quick, test_finish_response_after_read_eof_malformed
; "request body response not sent", `Quick, test_request_body_eof_response_not_sent
; "request body response not sent empty eof", `Quick, test_request_body_eof_response_not_sent_empty_eof
; "reader EOF race condition causes state machine to issue writer yield", `Quick, test_race_condition_writer_issues_yield_after_reader_eof
; "multiple requests in single read", `Quick, test_multiple_requests_in_single_read
; "multiple async requests in single read", `Quick, test_multiple_async_requests_in_single_read
; "chunked-encoding streaming error", `Quick, test_errored_chunked_streaming_response
; "content-length streaming error", `Quick, test_errored_content_length_streaming_response
; "chunked-encoding async streaming error", `Quick, test_errored_chunked_streaming_response_async
; "multiple requests with connection close", `Quick, test_multiple_requests_in_single_read_with_close
; "multiple requests with eof", `Quick, test_multiple_requests_in_single_read_with_eof
; "parse failure after checkpoint", `Quick, test_parse_failure_after_checkpoint
; "parse failure at eof", `Quick, test_parse_failure_at_eof
; "response finished before body read", `Quick, test_response_finished_before_body_read
; "pipelined", `Quick, test_pipelined_requests_answer_before_reading_body
; "body has a chance to flush before the next read operation", `Quick, test_body_flush_fairness
; "shutdown in request handler", `Quick, test_shutdown_in_request_handler
; "shutdown during asynchronous request", `Quick, test_shutdown_during_asynchronous_request
; "flush response before shutdown", `Quick, test_flush_response_before_shutdown
; "schedule read with data available", `Quick, test_schedule_read_with_data_available
; "read body eof called multiple times", `Quick, test_eof_called_multiple_times
; "eof called for empty bodies", `Quick, test_eof_called_for_empty_bodies
; "error handler with chunked transfer encoding", `Quick, test_error_handler_chunked_response
; "pipelined requests in single read" ,`Quick, test_pipelined_requests_in_single_buffer_partial_body
; "multiple pipelined requests", `Quick, test_multiple_pipelined_requests
; "Body.Writer.flush waits for bytes to have been written to the wire", `Quick, test_body_flush_after_bytes_in_the_wire
; "input consumed before closing request body", `Quick, test_input_consumed_before_closing_req_body
; "can read more requests after write eof", `Quick, test_can_read_more_requests_after_write_eof
; "can read more requests after write eof (before response sent)", `Quick, test_can_read_more_requests_after_write_eof_before_send_response
; "write response after reader EOF", `Quick,test_write_response_after_read_eof
]