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

Add expert mode for cohttp-lwt #647

Merged
merged 7 commits into from
Nov 21, 2018
Merged

Add expert mode for cohttp-lwt #647

merged 7 commits into from
Nov 21, 2018

Conversation

andreas
Copy link
Contributor

@andreas andreas commented Nov 5, 2018

This PR adds "expert mode" to cohttp-lwt similar to what exists for cohttp-async (#488).

My primary use case is for websocket support like #488. The changes also relate to other issues regarding streaming though (#534, #529), and this approach could be used to solve those use cases. An Expert-response does currently not allow omitting a body (a decision inherited from #488), but if that was permitted, then it could be used to for writing/streaming bigarrays efficiently (mentioned here).

I've rewritten the response/response handling, such that it no longer uses Lwt_stream. Though the abstraction of mapping a stream of requests to a stream of responses sounds elegant, I think it's actually simpler without it. In particular the use of refs (early_close) and mutexes (read_m) is no longer required. I'm happy to receive feedback on the choice though.

I've not added any tests, but I'd be happy to if the overall approach is approved.

/cc @mattjbray

Copy link
Member

@avsm avsm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach looks great, and would be of course good to support websockets. I left some comments about the approach in the PR comments.

cohttp-lwt/src/s.ml Show resolved Hide resolved
cohttp-lwt/src/s.ml Outdated Show resolved Hide resolved
@avsm
Copy link
Member

avsm commented Nov 8, 2018

This approach and interface lgtm! Some tests to validate it stays working for websocket would be great before merging if possible.

@mattjbray
Copy link
Contributor

This solves my streaming use case (catch exceptions while writing the body) so 👍 from me. This will close #534 and #529.

@andreas
Copy link
Contributor Author

andreas commented Nov 8, 2018

Some tests to validate it stays working for websocket would be great before merging if possible.

Will work on adding tests 👍

This solves my streaming use case (catch exceptions while writing the body)

The current formulation of `Expert always writes an empty body for the initial response (source -- this is the behavior in Cohttp_async from #488). When switching protocols, this is fine. However, the streaming use case is not supported with this behavior. We could either choose to not write an empty body for an `Expertresponse (change of semantics), or expand the variant to have an optional body:

type response_action =
  [ `Expert of Cohttp.Response.t
+               * Body.t option
               * (IO.ic
                  -> IO.oc
                  -> unit Lwt.t)
  | `Response of Cohttp.Response.t * Body.t ]

In general, I would urge that we keep the type response_action and its semantics in sync between Cohttp_lwt and Cohttp_async. This is essential for being able to write a websocket library that works with both without requiring adapters left and right. Changing `Expert in Cohttp_async would be a breaking change though 😢

I'm a little concerned about this PR snowballing into a lot more changes though, as I'd like to see this PR merged...

@avsm
Copy link
Member

avsm commented Nov 8, 2018

One option is to be explicit with the purpose of the response_action, and have:

type response_action =
  [ 
  `Switch_protocol of Cohttp.Response.t              
               * (IO.ic
                  -> IO.oc
                  -> unit Lwt.t)
 | `Stream of Cohttp.Response.t * Body.t 
              * (IO.ic
                  -> IO.oc
                  -> unit Lwt.t)
  | `Response of Cohttp.Response.t * Body.t ]

Although I'm a little confused as to the purpose of the empty body write in the Async case. What's that actually sending out to the wire -- an empty HTTP chunked encoding ?

@andreas
Copy link
Contributor Author

andreas commented Nov 9, 2018

Although I'm a little confused as to the purpose of the empty body write in the Async case. What's that actually sending out to the wire -- an empty HTTP chunked encoding ?

I'm not sure why writing the empty body was part of #488 in the first place. It appears the main goal of that PR was to enable switching protocols, which can work despite writing the empty body when encoding is set to Transfer.Unknown (see below).

What's written on the wire depends on the encoding set on the response:

  • For Transfer.Chunked, the empty body is written out as 0\r\n\r\n. This prohibits writing further to the body.

    `Expert (
      Cohttp.Response.make ~encoding:Transfer.Chunked (),
      fun ic oc -> (* client considers body finished at this point *)
    )
    HTTP/1.1 200 OK
    Transfer-Encoding: chunked
    
    0\r\n
    \r\n
    
  • For Transfer.Fixed, nothing is written for the body. So this works for writing a fixed-size response in the IO handler function, but not for streaming responses.

    `Expert (
      Cohttp.Response.make ~encoding:Transfer.(Fixed 3L) (),
      fun ic oc -> Lwt_io.write oc "foo"
    )
    HTTP/1.1 200 OK
    Content-Length: 3
    
    foo
    
  • For Transfer.Unknown, nothing is written for the body. This is good for creating the 101 Switching Protocols response, which I guess is why this was not discussed in Expose [Reader.t] and [Writer.t] via [`Switching_protocols] for websockets #488.

    let headers = Header.of_list [
      "Upgrade", "websocket";
      "Connection", "Upgrade"
    ] in
    `Expert (
      Cohttp.Response.make ~status:`Switching_protocols ~headers ~encoding:Transfer.Unknown (),
      fun ic oc -> (* websocket stuff *)
    )
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    

    You can also abuse this to set a Transfer-Encoding header and then reply with a chunked response:

    let headers = Header.of_list [
      "Transfer-Encoding", "chunked"
    ] in
    `Expert (
      Cohttp.Response.make ~headers ~encoding:Transfer.Unknown (),
      fun ic oc ->
        Lwt_io.write oc "3\r\nfoo\r\n" >>= fun () ->
        Lwt_io.write oc "3\r\nbar\r\n" >>= fun () ->
        Lwt_io.write oc "0\r\n\r\n"
    )
    HTTP/1.1 200 OK
    Transfer-Encoding: chunked
    
    3\r\n
    foo\r\n
    
    3\r\n
    bar\r\n
    
    0\r\n
    \r\n
    

    This feel like a hack though.

One option is to be explicit with the purpose of the response_action, and have:

type response_action =
  [ 
  `Switch_protocol of Cohttp.Response.t              
               * (IO.ic
                  -> IO.oc
                  -> unit Lwt.t)
 | `Stream of Cohttp.Response.t * Body.t 
              * (IO.ic
                  -> IO.oc
                  -> unit Lwt.t)
  | `Response of Cohttp.Response.t * Body.t ]

Given the above, I have a hard time coming up with a use case for writing providing both Body.t and an IO function IO.ic -> IO.oc -> unit io: why would you both write out a body, which will subsequently be considered finished by the client, and have a function for writing further to this body?

Unless I'm missing something, I would suggest to change the semantics for `Expert to not write an empty response body.

@andreas
Copy link
Contributor Author

andreas commented Nov 13, 2018

Anything I can do to help this move forward? If we can agree on the approach, I'd be happy to continue with implementation and tests (both Async and Lwt).

@mseri
Copy link
Collaborator

mseri commented Nov 13, 2018

I think that changing the semantics of `Expert makes more sense and is cleaner and easier to reason about. We could ping @avsm to have also his opinion

@avsm
Copy link
Member

avsm commented Nov 14, 2018

Unless I'm missing something, I would suggest to change the semantics for `Expert to not write an empty response body.

Thank you for the comprehensive analysis! I agree with @andreas and @mseri that changing the semantics of Expert in makes more sense. We can merge and cut a major version bump with this change as soon as the PR is ready.

@avsm avsm mentioned this pull request Nov 14, 2018
@andreas
Copy link
Contributor Author

andreas commented Nov 14, 2018

Great, thanks! I'll proceed with the implementation and tests.

@avsm
Copy link
Member

avsm commented Nov 19, 2018

@andreas when ready, if you merge with the master branch the Travis tests should now be green as well

Copy link
Contributor Author

@andreas andreas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated the code now and added a basic test for both Lwt and Async.

cohttp-async/src/server.ml Show resolved Hide resolved
cohttp-lwt-unix/src/io.ml Show resolved Hide resolved
cohttp-lwt/src/server.ml Show resolved Hide resolved
cohttp-lwt/src/s.ml Outdated Show resolved Hide resolved
Copy link
Member

@avsm avsm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fairly minor review comments from me; I'm happy to merge as soon as you're happy with the PR @andreas!

cohttp-async/src/server.mli Outdated Show resolved Hide resolved
cohttp-async/test/test_async_integration.ml Outdated Show resolved Hide resolved
cohttp-lwt-unix/src/io.ml Show resolved Hide resolved
Lwt.catch
(fun () -> callback conn req body)
(fun exn ->
Format.eprintf "Error handling %a: %s\n%!" Request.pp_hum req (Printexc.to_string exn);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use Logs instead of directly printing to stderr?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logs is currently not a dependency of cohttp-lwt, but I can add it if you want?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I think that would be a good idea - we need to eliminate printf errors from embeddable libraries

@avsm
Copy link
Member

avsm commented Nov 20, 2018

When ready to merge, if you could push a CHANGES entry that describes the backwards compat breaking change that would be helpful. I can roll several PRs into a major version bump.

@andreas
Copy link
Contributor Author

andreas commented Nov 21, 2018

  • Replaced Format.eprintf with Log.err in Cohttp_lwt.Server
  • Added an entry CHANGES.md

@avsm avsm merged commit 9f45a10 into mirage:master Nov 21, 2018
@avsm
Copy link
Member

avsm commented Nov 21, 2018

Thanks for the contribution! I'll prepare a release after a bit more testing -- in the meanwhile, ensuring that the master branch of cohttp works for your websocket/other libraries that need this feature would be most appreciated.

@andreas andreas deleted the lwt-expert-mode branch November 21, 2018 20:27
@andreas
Copy link
Contributor Author

andreas commented Nov 21, 2018

Yay, thanks 😀

in the meanwhile, ensuring that the master branch of cohttp works for your websocket/other libraries that need this feature would be most appreciated.

I have already developed some features against this locally -- I can share the source if it's of interest 😄

@avsm
Copy link
Member

avsm commented Nov 22, 2018

That would be great! Having more examples to point to in the README is probably one of cohttp's bigger user requests :-)

@mseri
Copy link
Collaborator

mseri commented Nov 22, 2018

It would be nice to be able to add the README as an Introduction page in the odoc/ocamldoc generated documentation. The little tool that broke github (md2mld) was made on purpose to try and do it on ocaml-rpc. Do you know a better way?

@avsm
Copy link
Member

avsm commented Nov 22, 2018

md2mld seems just fine to me -- especially if it had a dune rule that just worked :-)

The other place where the README shows up is in the opam description field. Perhaps we should break up the README contents into a doc/ directly and assemble it via dune promotion rules into the opam/README/mld files. I did a crude version of that in the duniverse tool.

avsm added a commit to avsm/opam-repository that referenced this pull request Feb 4, 2019
…ohttp-top, cohttp-async and cohttp-mirage (2.0.0)

CHANGES:

Compatibility breaking interface changes:

Async: Expert response action no longer writes empty HTTP body (mirage/ocaml-cohttp#647 by @andreas)

In cohttp.0.99, a number of subpackages were turned into explicit
opam packages to simplify dependency management.
To aid migration, some compatability shims were left in place so that
the old findlib names would continue to work. They have now been removed
as of this release.  If you were still using them, then please rename
them as follows in your dune or ocamlbuild files:
- `cohttp.lwt-core` -> `cohttp-lwt`
- `cohttp.lwt` -> `cohttp-lwt-unix`
- `cohttp.js` -> `cohttp-lwt-jsoo`
- `cohttp.async` -> `cohttp-async`
- `cohttp.top` -> `cohttp-top`

Other changes and bugfixes:
* Lwt, Mirage: Add log warnings for uncaught exceptions (mirage/ocaml-cohttp#592 by @ansiwen)
* Log invalid client input and do not catch out of memory exceptions (mirage/ocaml-cohttp#652 @hannesm)
* Port opam files to opam2 and add local synopsis and descriptions.
* Lwt: Add Expert response action for servers (mirage/ocaml-cohttp#647 by @andreas)
* Use the namespaced `js_of_ocaml` interfaces from 3.3.0 onwards (mirage/ocaml-cohttp#654 @avsm)
* Use Base64 3.1.0 interfaces (mirage/ocaml-cohttp#655 @avsm)
* Clean up redundant conflicts in the `opam` files (@avsm)
mseri pushed a commit to ocaml/opam-repository that referenced this pull request Feb 6, 2019
…ohttp-top, cohttp-async and cohttp-mirage (2.0.0) (#13388)

CHANGES:

Compatibility breaking interface changes:

Async: Expert response action no longer writes empty HTTP body (mirage/ocaml-cohttp#647 by @andreas)

In cohttp.0.99, a number of subpackages were turned into explicit
opam packages to simplify dependency management.
To aid migration, some compatability shims were left in place so that
the old findlib names would continue to work. They have now been removed
as of this release.  If you were still using them, then please rename
them as follows in your dune or ocamlbuild files:
- `cohttp.lwt-core` -> `cohttp-lwt`
- `cohttp.lwt` -> `cohttp-lwt-unix`
- `cohttp.js` -> `cohttp-lwt-jsoo`
- `cohttp.async` -> `cohttp-async`
- `cohttp.top` -> `cohttp-top`

Other changes and bugfixes:
* Lwt, Mirage: Add log warnings for uncaught exceptions (mirage/ocaml-cohttp#592 by @ansiwen)
* Log invalid client input and do not catch out of memory exceptions (mirage/ocaml-cohttp#652 @hannesm)
* Port opam files to opam2 and add local synopsis and descriptions.
* Lwt: Add Expert response action for servers (mirage/ocaml-cohttp#647 by @andreas)
* Use the namespaced `js_of_ocaml` interfaces from 3.3.0 onwards (mirage/ocaml-cohttp#654 @avsm)
* Use Base64 3.1.0 interfaces (mirage/ocaml-cohttp#655 @avsm)
* Clean up redundant conflicts in the `opam` files (@avsm)
fdopen added a commit to fdopen/opam-repository-mingw that referenced this pull request Feb 7, 2019
…ohttp-top, cohttp-async and cohttp-mirage (2.0.0) (#13388)

CHANGES:

Compatibility breaking interface changes:

Async: Expert response action no longer writes empty HTTP body (mirage/ocaml-cohttp#647 by @andreas)

In cohttp.0.99, a number of subpackages were turned into explicit
opam packages to simplify dependency management.
To aid migration, some compatability shims were left in place so that
the old findlib names would continue to work. They have now been removed
as of this release.  If you were still using them, then please rename
them as follows in your dune or ocamlbuild files:
- `cohttp.lwt-core` -> `cohttp-lwt`
- `cohttp.lwt` -> `cohttp-lwt-unix`
- `cohttp.js` -> `cohttp-lwt-jsoo`
- `cohttp.async` -> `cohttp-async`
- `cohttp.top` -> `cohttp-top`

Other changes and bugfixes:
* Lwt, Mirage: Add log warnings for uncaught exceptions (mirage/ocaml-cohttp#592 by @ansiwen)
* Log invalid client input and do not catch out of memory exceptions (mirage/ocaml-cohttp#652 @hannesm)
* Port opam files to opam2 and add local synopsis and descriptions.
* Lwt: Add Expert response action for servers (mirage/ocaml-cohttp#647 by @andreas)
* Use the namespaced `js_of_ocaml` interfaces from 3.3.0 onwards (mirage/ocaml-cohttp#654 @avsm)
* Use Base64 3.1.0 interfaces (mirage/ocaml-cohttp#655 @avsm)
* Clean up redundant conflicts in the `opam` files (@avsm)
@mseri mseri mentioned this pull request Jan 8, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants