1918 lines
59 KiB
OCaml
1918 lines
59 KiB
OCaml
|
|
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
|
||
|
|
]
|