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
Implement WebSocket support based on Plug.Conn.Adapter.upgrade/3 #88
Conversation
@josevalim as requested, this work is done and ready for review! See phoenixframework/phoenix#5003 for an overview & merge proposal. |
mix.exs
Outdated
@@ -34,6 +34,7 @@ defmodule Plug.Cowboy.MixProject do | |||
def deps do | |||
[ | |||
{:plug, "~> 1.7"}, | |||
{:sock, "~> 0.3.0"}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of making this part of plug as Plug.Websocket
instead? Not a fan of adding a dependency that is just a single behaviour when we already have plug for HTTP interfaces.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I dropped a comment. We should not depend on Sock because we should not expose the Sock API but the Cowboy API. Then Sock sits on top!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I wrote this in the flurry earlier today but it never actually got sent)
Got it. Actually, maybe I don't :)
I follow you as far as removing Sock from this PR, and having Plug.Cowboy expose Cowboy WS handler API. That much is clear.
When you say 'Sock sits on top!', are you suggesting that a caller does something like:
def call(conn, opts) do
....
# Assuming that MyApp.Socket is what you ultimately want to handle the WS
upgrade_adapter(conn, :websocket, {Sock, {MyApp.Socket, opts}, connection_opts})
end
At which time the underlying server (say Cowboy) will call Sock via the Cowboy WS API, and Sock will then call onwards to MyApp.Socket via the Sock API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the goal is to have Sock.upgrade(conn, sock_options)
and that will call upgrade_adapter
for each adapter accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it; Sock in this case would wrap the socket lifecycle starting from upgrade.
Only thing I'm not sure is how that would look in the first PR split from phoenixframework/phoenix#5030 before we introduce Sock. Pretty sure it would still need to be cowboy specific.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, let’s not worry about Sock in that PR yet. Let’s change the plug dispatch bits and I think figuring out the rest will be straightforward!
Hi @mtrudel! Thanks for the PRs, I have started reviewing them and I will drop some feedback around. It may look a bit fragmented but we can go back to the Omnissue if necessary. For this PR, I want to note that Cowboy will not expose the Sock API. The point is exactly that each webserver has a different API and unifying them can lead us to an unhealthy common ground, especially given Bandit is more expressive than Cowboy. Therefore, the goal in |
To further clarify, for Cowboy the API will be:
Where |
@josevalim I've implemented the required changes to Plug.Cowboy to move in this direction, though it's not ending up as clean as we were hoping. As a result, I haven't pushed them to this branch since I'm not sure if this is an avenue we'll want to end up pursuing. You can see the changes at master...mtrudel:plug_cowboy:plug_upgrade_no_sock The primary problem is that we're not able to get Cowboy to call the handler directly; from the perspective of Cowboy, Moreover, to me this really starts to muddle the waters about the overall structure of the stack. If we're still having to do some sort of wrapping/adapting in here I don't really see the benefit to this change; it's just adding another layer of indirection of the exact kind that we iterated on removing in the earlier incarnations of Sock within Phoenix. Instead of having Taking a step back, perhaps this would hang together more cleanly with @ericmj's suggestion above, namely moving the Graphically, we'd be looking at something like the second option below:
The benefit of the second approach is that the Sock library doesn't even need to exist at all! Less code & fewer abstractions all around, and a stackup that's consistent with the HTTP side of things. [3] Really, I think it comes down to your concern that '[each] webserver has a different API and unifying them can lead us to an unhealthy common ground', and where that gets expressed in the stack. The unification of disparate server capabilities under a common In any case, this is of course a PR against your library and thus this is obviously your call :). The changes on both this PR and the branch above are complete viz a viz tests / docs / &c so whichever direction you want to go, the work there is solid and ready to go pending merge of elixir-plug/plug#1119 and and update to the dependencies here. Also, thanks once again for such timely and thoughtful reviews! [1] Of note, this wrapping is a clean passthrough of the plain [2] This is the long-term plan. In the near term the Sock steps aren't present, but the fact remains that multiple layers in the stack still need to understand the [3] This does preclude splitting phoenixframework/phoenix#5030 into two separate workups. If we went this way we'd also probably want to revisit the |
I see @mtrudel, thanks for exploring! Maybe it is worth sending a pull request to Cowboy, to additionally support |
Looking at the Cowboy code, I believe we could easily do it! https://github.com/ninenines/cowboy/blob/master/src/cowboy_handler.erl#L41-L44 We just need to define a |
I agree. This looks like an easy workup that we can do outside of |
|
Very good! |
The proposed A couple of things to note:
Otherwise this should be good to go! |
It actually has to be.
To be clear, do you mean that we support all callbacks, except |
lib/plug/cowboy.ex
Outdated
should handle the WebSocket connection, and must take the form `{handler, handler_opts, | ||
connection_opts}`, where values are as follows: | ||
|
||
* `handler` is a module which implements the `:cowboy_websocket` behaviour. Note that this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a link to cowboy_websocket
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/me looks up how to link to Erlang behaviours in ex_doc lol
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It has to be manual:
* `handler` is a module which implements the `:cowboy_websocket` behaviour. Note that this | |
* `handler` is a module which implements the [`:cowboy_websocket`](https://ninenines.eu/docs/en/cowboy/2.6/manual/cowboy_websocket/) behaviour. Note that this |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
lib/plug/cowboy/conn.ex
Outdated
@@ -90,6 +90,10 @@ defmodule Plug.Cowboy.Conn do | |||
:cowboy_req.inform(status, to_headers_map(headers), req) | |||
end | |||
|
|||
@impl true | |||
def upgrade(req, :websocket, args), do: {:ok, Map.put(req, :upgrade, {:websocket, args})} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably validate here that args
is a triplet, the first element is an atom and the third is a map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about that at the dog park yesterday - maybe we should add a matching pattern match in the plug PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is fine to raise here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to explicitly match on a three element tuple
We are almost there! Just three minor comments left! |
Yeah, I'm talking about after your recent fix to that in phoenixframework/phoenix#4933. In any case, it'll be in the right place now :)
Yes, exactly. |
Other than a final change to the plug dependency herein, all done! |
PR updated to depend on 1.14.0. Note that this brings up a number of deprecation errors around push (what's happening with that, btw? I removed push support in bandit recently as a follow-on to it being deprecated in Plug). Testing green locally; assume it will on CI now as well! |
|
The funny thing is that push was probably the most difficult thing to get right in Bandit's HTTP/2 implementation. Had I not been such a completionist and skipped over it, I'd have saved a ton of time. Oh well 🙃 |
Co-authored-by: José Valim <jose.valim@gmail.com>
Co-authored-by: José Valim <jose.valim@gmail.com>
Can you please update CI to use Elixir v1.10 and update the mix.exs to require ~> 1.10 (the same as Plug)? |
Yep, just saw that too. Coming right up |
Updated to the same min OTP/Elixir pair as Plug. Note that:
Let's see how CI likes that. |
Cowboy requires 22 IIRC. |
Updated to just change Elixir to 1.10, leaving OTP unchanged |
💚 💙 💜 💛 ❤️ |
🥳 |
This PR provides support for upgrading HTTP connections to WebSocket connections by use of the
c:Plug.Conn.Adapter.upgrade/3
callback. WebSocket implementations are called the:cowboy_websocket
API. This work is hoisted in large part directly from Phoenix'sPhoenix.Endpoint.Cowboy2Handler
. This work is the Plug.Cowboy analog of mtrudel/bandit#38See phoenixframework/phoenix#5003 for an omnibus overview of the larger body of work that this PR is part of.