mirage / ocaml-cohttp

An OCaml library for HTTP clients and servers using Lwt or Async
Other
713 stars 174 forks source link

cohttp-eio: "large" chunked-response → exception Eio.Io Unix_error (Invalid argument, "writev", "") #1005

Open smondet opened 11 months ago

smondet commented 11 months ago

This may be related to observations in https://github.com/mirage/ocaml-cohttp/issues/1004

Using the master branch.

I've modifed cohttp-eio/examples/server1.ml like this:

let handler _socket request _body =
  match Http.Request.resource request with
  | "/" -> (Http.Response.make (), Cohttp_eio.Body.of_string text)
  | "/ok" ->
      ( Http.Response.make (),
        Cohttp_eio.Body.of_string (String.make 3_146_724 'B') )
  | "/ok2" ->
      ( Http.Response.make (),
        Cohttp_eio.Body.of_string (String.make 3_146_725 'B') )
  | "/ok3" ->
      (Http.Response.make (), Eio.Flow.string_source (String.make 3_146_724 'B'))
  | "/ko" ->
      (Http.Response.make (), Eio.Flow.string_source (String.make 3_146_725 'B'))
  | "/html" ->
      ( Http.Response.make
          ~headers:(Http.Header.of_list [ ("content-type", "text/html") ])
          (),
        (* Use a plain flow to test chunked encoding *)
        Eio.Flow.string_source text )
  | _ -> (Http.Response.make ~status:`Not_found (), Cohttp_eio.Body.of_string "")

When we call /ko, the server dies with:

Fatal error: exception Eio.Io Unix_error (Invalid argument, "writev", ""),
  handling connection from tcp:127.0.0.1:33122

Here are the corresponding curl calls:

 $ curl --verbose --fail-with-body http://localhost:2000/ok | wc -c
*   Trying 127.0.0.1:2000...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Connected to localhost (127.0.0.1) port 2000 (#0)
> GET /ok HTTP/1.1
> Host: localhost:2000
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-length: 3146724
< 
{ [32724 bytes data]
100 3072k  100 3072k    0     0   161M      0 --:--:-- --:--:-- --:--:--  166M
* Connection #0 to host localhost left intact
3146724
 $ curl --verbose --fail-with-body http://localhost:2000/ok2 | wc -c
*   Trying 127.0.0.1:2000...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Connected to localhost (127.0.0.1) port 2000 (#0)
> GET /ok2 HTTP/1.1
> Host: localhost:2000
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< content-length: 3146725
< 
{ [65492 bytes data]
100 3072k  100 3072k    0     0   179M      0 --:--:-- --:--:-- --:--:--  187M
* Connection #0 to host localhost left intact
3146725
 $ curl --verbose --fail-with-body http://localhost:2000/ok3 | wc -c
*   Trying 127.0.0.1:2000...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Connected to localhost (127.0.0.1) port 2000 (#0)
> GET /ok3 HTTP/1.1
> Host: localhost:2000
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< transfer-encoding: chunked
< 
{ [65489 bytes data]
100 3072k    0 3072k    0     0   174M      0 --:--:-- --:--:-- --:--:--  176M
* Connection #0 to host localhost left intact
3146724
 $ curl --verbose --fail-with-body http://localhost:2000/ko | wc -c
*   Trying 127.0.0.1:2000...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Connected to localhost (127.0.0.1) port 2000 (#0)
> GET /ko HTTP/1.1
> Host: localhost:2000
> User-Agent: curl/7.81.0
> Accept: */*
> 
* Empty reply from server
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
* Closing connection 0
curl: (52) Empty reply from server
0
smondet commented 11 months ago

Actually with exactly the master branch now (e5a66f1c1e7c2e5051723e09260222994dff40cf) the /ok3 enpoint also causes the failure.

With this change, we get the behavior above (/ok3 succeeds. /ko fails):

diff --git a/vendor/cohttp/cohttp-eio/src/utils.ml b/vendor/cohttp/cohttp-eio/src/utils.ml
index 8478eac4..8d1cee93 100644
--- a/vendor/cohttp/cohttp-eio/src/utils.ml
+++ b/vendor/cohttp/cohttp-eio/src/utils.ml
@@ -45,7 +45,7 @@ let flow_of_reader =
   fun read_body_chunk -> Eio.Resource.T (Reader_flow.v read_body_chunk, handler)

 let flow_to_writer flow writer write_body =
-  let input = Eio.Buf_read.of_flow ~max_size:max_int flow in
+  let input = Eio.Buf_read.of_flow ~initial_size:1024 ~max_size:max_int flow in
   let rec loop () =
     let () =
       let () = Eio.Buf_read.ensure input 1 in
smondet commented 11 months ago

With let input = Eio.Buf_read.of_flow ~initial_size:1023 ~max_size:max_int flow in

They both fail.

with let input = Eio.Buf_read.of_flow ~initial_size:1025 ~max_size:max_int flow in

They both succeed.

smondet commented 11 months ago

With:

let handler _socket request _body =
  let u = Http.Request.resource request |> Uri.of_string in
  match Uri.path u with
  | "/" ->
      (Http.Response.make (), Cohttp_eio.Body.of_string text) (* | "/body" -> *)
  | "/body" ->
      let size = Uri.get_query_param u "size" |> Option.get |> int_of_string in
      (Http.Response.make (), Cohttp_eio.Body.of_string (String.make size 'B'))
  | "/flow" ->
      let size = Uri.get_query_param u "size" |> Option.get |> int_of_string in
      (Http.Response.make (), Eio.Flow.string_source (String.make size 'B'))

We can make crash the Body.of_string version too:

curl --verbose --fail-with-body http://localhost:2000/body?size=3_000_000 | wc -c succeeds but curl --verbose --fail-with-body http://localhost:2000/body?size=6_000_000 | wc -c fails, with the same server error.

smondet commented 11 months ago

OK This might be an Eio bug (?).

Playing with the initial-buffer-sizes and this:

diff --git a/vendor/eio/lib_eio_posix/flow.ml b/vendor/eio/lib_eio_posix/flow.ml
index 2588bf11..3ac9795a 100644
--- a/vendor/eio/lib_eio_posix/flow.ml
+++ b/vendor/eio/lib_eio_posix/flow.ml
@@ -43,6 +43,8 @@ module Impl = struct
     try
       Low_level.writev t (Array.of_list bufs)
     with Unix.Unix_error (code, name, arg) ->
+      traceln "ERROR vendor/eio/lib_eio_posix/flow.ml bufs: %d"
+        (List.length bufs);
       raise (Err.wrap code name arg)

   let copy t ~src = Eio.Flow.Pi.simple_copy ~single_write t ~src

We can see that it fails when the call to posix-writev gets an array of more than 1024 buffers, which is indeed the value of IOV_MAX on my system.