Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DATA frames are sent after RST_STREAM frames #187

Closed
jonathanjameswatson opened this issue Aug 26, 2022 · 3 comments · Fixed by #197
Closed

DATA frames are sent after RST_STREAM frames #187

jonathanjameswatson opened this issue Aug 26, 2022 · 3 comments · Fixed by #197

Comments

@jonathanjameswatson
Copy link
Contributor

jonathanjameswatson commented Aug 26, 2022

I have experienced a couple situations where the following NYI exception is raised on the client:

failwith "h2.Reqd.report_exn: NYI"

This seems to occur due to DATA frames being sent by the server to the client after it has already sent an RST_STREAM frame, which should not be followed by any other type of frame.

One circumstance in which this happens is where Reqd.report_exn is called after Reqd.respond_with_string. This works with http/af, but in h2, the resulting HEADERS, DATA and RST_STREAM frames are delivered in an incorrect order. I can fix this by commenting out the following line:

reset_stream t error_code

The RST_STREAM frame is still sent, but after the other two frames. I am not sure if this is the correct solution to the problem.

I have also run into this issue when sending a request that has the content-length header set to a string like "Not a number". The above fix does not work in this case, although this is not unexpected as reset_stream is found in a lot of places. I think either reset_steam needs to be changed, what happens after reset_stream is called needs to be changed, or reset_stream needs to be removed.

@anmonteiro
Copy link
Owner

I'll look into this in a bit, but it'd be great to have either a reproduction, or ideally a test that exposes this bug in functionality.

@jonathanjameswatson
Copy link
Contributor Author

Here is the test I started writing for when the content length is not a number:

let test_non_number_content_length_with_data () =
  let body_read_called = ref false in
  let request =
    Request.create
      ~scheme:"http"
      `GET
      "/"
      ~headers:(Headers.of_list [ "content-length", "not-a-number" ])
  in
  let request_handler reqd =
    let request_body = Reqd.request_body reqd in
    Body.Reader.schedule_read
      request_body
      ~on_eof:ignore
      ~on_read:(fun _bs ~off:_ ~len:_ -> body_read_called := true)
  in
  let t = create_and_handle_preface ~error_handler request_handler in
  read_request ~body:"request body" t request;
  let data_frame =
    { Frame.frame_header =
        { payload_length = 0
        ; stream_id = 1l
        ; flags = Flags.(default_flags |> set_end_stream)
        ; frame_type = Data
        }
    ; frame_payload = Frame.Data (Bigstringaf.of_string ~off:0 ~len:3 "foo")
    }
  in
  read_frames t [ data_frame ];
  match next_write_operation t with
  | `Write iovecs ->
    let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in
    Alcotest.(check (list int))
      "???"
      (List.map Frame.FrameType.serialize Frame.FrameType.[ RSTStream ])
      (* Or maybe [ Headers; Data ]? *)
      (List.map
         (fun Frame.{ frame_header = { frame_type; _ }; _ } ->
           Frame.FrameType.serialize frame_type)
         frames);
    report_write_result t (`Ok (IOVec.lengthv iovecs));
    Alcotest.(check bool) "Response handler called" true !body_read_called;
    writer_yields t
  | _ -> assert false

It's not a great example, as arguably an RST_STREAM frame may not need to be output at all. However, it does demonstrate the common behavior where DATA frames are output after RST_STREAM frames.

Here is a test I have written for the first problem:

let test_reset_stream () =
  let request = Request.create ~scheme:"http" `GET "/" in
  let request_handler reqd =
    Reqd.respond_with_string
      reqd
      (Response.create `Internal_server_error)
      "An error occurred";
    Reqd.report_exn reqd Not_found
  in
  let t = create_and_handle_preface ~error_handler request_handler in
  read_request ~body:"request body" t request;
  match next_write_operation t with
  | `Write iovecs ->
    let frames = parse_frames (Write_operation.iovecs_to_string iovecs) in
    Alcotest.(check (list int))
      "???"
      (List.map
         Frame.FrameType.serialize
         Frame.FrameType.[ Headers; Data; RSTStream ])
      (* Not entirely sure what frames should be received here. *)
      (List.map
         (fun Frame.{ frame_header = { frame_type; _ }; _ } ->
           Frame.FrameType.serialize frame_type)
         frames);
    report_write_result t (`Ok (IOVec.lengthv iovecs));
    writer_yields t
  | _ -> assert false

However, it may be that using report_exn after respond_with_string is not a correct use of Reqd, and it just happens to not cause any problems when used with http/af.

@anmonteiro
Copy link
Owner

Thanks for the repro. I added a fix for the server in #194. Will take a look at the client portion soon

anmonteiro added a commit to anmonteiro/opam-repository that referenced this issue Mar 17, 2023
…2-async (0.10.0)

CHANGES:

- hpack: fix a case where hpack would raise an array out of bounds exception
  ([anmonteiro/ocaml-h2#183](anmonteiro/ocaml-h2#183))
  ([@jonathanjameswatson](https://github.com/jonathanjameswatson))
- h2: (client) handle multiple RST_STREAM frames
  ([anmonteiro/ocaml-h2#184](anmonteiro/ocaml-h2#184))
  ([@jonathanjameswatson](https://github.com/jonathanjameswatson))
- h2: (client) Fix a race condition with `~flush_headers_immediately:false` and
  empty request bodies
  ([anmonteiro/ocaml-h2#186](anmonteiro/ocaml-h2#186))
- h2: Make `H2.Reqd.error_code` part of the public interface
  ([anmonteiro/ocaml-h2#188](anmonteiro/ocaml-h2#188))
- h2: Add `~request_method` argument to `H2.Method.body_length`
  ([anmonteiro/ocaml-h2#190](anmonteiro/ocaml-h2#190))
  ([@jonathanjameswatson](https://github.com/jonathanjameswatson))
- h2: Don't send any frames on a stream after an `RST_STREAM` frame
  ([anmonteiro/ocaml-h2#187](anmonteiro/ocaml-h2#187),
  [anmonteiro/ocaml-h2#194](anmonteiro/ocaml-h2#194))
- h2: call error handler on the client if the remote peer closes the
  commmunication channel
  ([anmonteiro/ocaml-h2#177](anmonteiro/ocaml-h2#177),
  [anmonteiro/ocaml-h2#196](anmonteiro/ocaml-h2#194))
- h2: when reprioritizing a stream, respect its new priority (accounts for
  inferred default priority when a dependent stream is not in the tree
  ([RFC7540§5.3.1](https://www.rfc-editor.org/rfc/rfc7540.html#section-5.3.1)))
  ([anmonteiro/ocaml-h2#200](anmonteiro/ocaml-h2#200))
- h2: don't remove parent streams from the scheduler if they have children
  ([anmonteiro/ocaml-h2#201](anmonteiro/ocaml-h2#201))
- h2: don't schedule streams as dependencies of others marked for removal
  ([anmonteiro/ocaml-h2#205](anmonteiro/ocaml-h2#205))
- h2: revise scheduling algorithm to avoid starvation
  ([anmonteiro/ocaml-h2#199](anmonteiro/ocaml-h2#199),
  [anmonteiro/ocaml-h2#204](anmonteiro/ocaml-h2#204), reported in
  [anmonteiro/ocaml-h2#162](anmonteiro/ocaml-h2#162), thanks
  [@quernd](https://github.com/quernd))
- h2-eio: adapt to the next gluten-eio version
  ([anmonteiro/ocaml-h2#210](anmonteiro/ocaml-h2#210))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants