Skip to content

Cohttp-eio: "handle used after calling close" while reading body / Closing connection too soon? #965

@smondet

Description

@smondet

This http-client code works when the content-length is “small” but starts
failing hard once it is a few kilobytes:

let response : Http.Response.t * Eio.Buf_read.t =
  Cohttp_eio.Client.call
    eio
    ?port
    ~headers:(Http.Header.of_list headers)
    ~host ~meth ?body resource
in
let body = Cohttp_eio.Client.read_fixed response in

The exception cannot be caught (it seems to be in the loop of the Luv backend):

Uncaught exception in run loop:
Exception: Invalid_argument("read_start: handle used after calling close!")
Raised at Stdlib.invalid_arg in file "stdlib.ml", line 30, characters 20-45
Called from Eio_luv.Low_level.Stream.read_into.(fun) in file "vendor/eio/lib_eio_luv/eio_luv.ml", line 512, characters 32-62
Called from Eio_luv.wakeup in file "vendor/eio/lib_eio_luv/eio_luv.ml", line 1179, characters 4-8
Called from Eio_luv.run2.(fun) in file "vendor/eio/lib_eio_luv/eio_luv.ml", line 1194, characters 10-40
Fatal error: exception Failure("Deadlock detected: no events scheduled but main function hasn't returned")
Raised at Stdlib.failwith in file "stdlib.ml", line 29, characters 17-33
Called from Stdlib__Fun.protect in file "fun.ml", line 33, characters 8-15
Re-raised at Stdlib__Fun.protect in file "fun.ml", line 38, characters 6-52
Called from Dune__exe__Backend_server_basics in file "test/backend_server_basics.ml", line 79, characters 2-1023

Looking at the code
client.ml:100:

  • it seems that indeed Eio.Buf_read.of_flow ~initial_size ~max_size:max_int conn is lazy enough
  • so when Cohttp_eio.Client.read_fixed gets called it still tries to pull from the socket (?)
  • The doc of Eio.Net.with_tcp_connect says: “[conn] is closed after [f] returns (if it isn't already closed by then).”
let buf_write conn =
  let initial_size = 0x1000 in
  Buf_write.with_flow ~initial_size:0x1000 conn (fun writer ->
      let request = Http.Request.make ?meth ?version ~headers resource_path in
      let request = Http.Request.add_te_trailers request in
      write_request pipeline_requests request writer body;
      let reader =
        Eio.Buf_read.of_flow ~initial_size ~max_size:max_int conn
      in
      let response = response reader in
      (response, reader))
in
match conn with
| None ->
    let service =
      match port with Some p -> string_of_int p | None -> "80"
    in
    Eio.Net.with_tcp_connect ~host ~service env#net (fun conn ->
        Eio.traceln "using with_tcp_connect";
        buf_write conn)
| Some conn ->
    Eio.traceln "NOT using with_tcp_connect";
    buf_write conn

I tried to replace read_fixed with this to see what happens:

let body =
  let buf = snd response in
  let res = Buffer.create 42 in
  let rec go n =
    try
      Buffer.add_char res (Eio.Buf_read.any_char (* take_all *) buf);
      if n % 20 = 0 then traceln "not done reading: %d" n;
      go (n + 1)
    with _ ->
      traceln "done reading: %d" n;
      Buffer.contents res
  in
  go 0
in

It fails after saying +not done reading: 3960

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions