Skip to content

Commit

Permalink
refactor API to respect Mint's opaque structures (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
the-mikedavis authored Feb 17, 2022
1 parent 8eac1b9 commit 802316a
Show file tree
Hide file tree
Showing 13 changed files with 487 additions and 255 deletions.
41 changes: 41 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,47 @@ The format is based on [Keep a
Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## UNRELEASED

This release is a breaking change from the 0.1.0 series. This update removes
all instances where Mint.WebSocket would access opaque `t:Mint.HTTP.t/0` fields
or call private functions within `Mint.HTTP1`, so now Mint.WebSocket should be
more compatible with future changes to Mint.

#### Upgrade guide

First, add the `scheme` argument to calls to `Mint.WebSocket.upgrade/5`.
For connections formed with `Mint.HTTP.connect(:http, ..)`, use the `:ws`
scheme. For `Mint.HTTP.connect(:https, ..)`, use `:wss`.


```diff
- Mint.WebSocket.upgrade(conn, path, headers)
+ Mint.WebSocket.upgrade(scheme, conn, path, headers)
```

Then replace calls to `Mint.HTTP.stream/2` and/or `Mint.HTTP.recv/3` and
`Mint.HTTP.stream_request_body/3` with the new `Mint.WebSocket` wrappers.
This is safe to do even when these functions are being used to send and
receive data in normal HTTP requests: the functionality only changes when
the connection is an established HTTP/1 WebSocket.

### Added

- Added `Mint.WebSocket.stream/2` which wraps `Mint.HTTP.stream/2`
- Added `Mint.WebSocket.recv/3` which wraps `Mint.HTTP.recv/3`
- Added `Mint.WebSocket.stream_request_body/3` which wraps `Mint.HTTP.stream_request_body/3`

### Changed

- Changed function signature of `Mint.Websocket.upgrade/5` to accept the
WebSocket's scheme (`:ws` or `:wss`) as the first argument
- Added an optional `opts` argument to `Mint.WebSocket.new/5` to control
active vs. passive mode on the socket
- Restricted compatible Mint versions to `~> 1.4`
- `Mint.WebSocket` now uses `Mint.HTTP.get_protocol/1` which was
introduced in `1.4.0`.

## 0.1.4 - 2021-07-06

### Fixed
Expand Down
79 changes: 33 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,45 @@

(Unofficial) HTTP/1 and HTTP/2 WebSocket support for Mint 🌱

## Usage

`Mint.WebSocket` works together with `Mint.HTTP` API. For example,
this snippet shows sending and receiving a text frame of "hello world" to a
WebSocket server which echos our frames:

```elixir
# bootstrap
{:ok, conn} = Mint.HTTP.connect(:http, "echo", 9000)

{:ok, conn, ref} = Mint.WebSocket.upgrade(:ws, conn, "/", [])

http_get_message = receive(do: (message -> message))
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
Mint.WebSocket.stream(conn, http_get_message)

{:ok, conn, websocket} = Mint.WebSocket.new(conn, ref, status, resp_headers)

# send the hello world frame
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, ref, data)

# receive the hello world reply frame
hello_world_echo_message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, hello_world_echo_message)
{:ok, websocket, [{:text, "hello world"}]} = Mint.WebSocket.decode(websocket, data)
```

## What is Mint?

Mint is a _functional_ HTTP/1 and HTTP/2 client library written in Elixir.

Why does it matter that it's functional? Isn't Elixir functional?

Existing WebSocket implementations like
[`:gun`](https://github.com/ninenines/gun),
[`:websocket_client`](https://github.com/jeremyong/websocket_client),
or [`WebSockex`](https://github.com/Azolo/websockex) work by spawning and
[`:gun`](https://github.com/ninenines/gun) /
[`:websocket_client`](https://github.com/jeremyong/websocket_client) /
[`Socket`](https://github.com/meh/elixir-socket) /
[`WebSockex`](https://github.com/Azolo/websockex) work by spawning and
passing messages among processes. This is a very convenient interface in
Elixir and Erlang, but it does not allow the author much control over
the WebSocket connection.
Expand Down Expand Up @@ -58,48 +87,6 @@ If `Mint.WebSocket.upgrade/4` returns
Then the server does not support HTTP/2 WebSockets or does not have them
enabled.

Support for HTTP/2 extended CONNECT was added to Mint in version `1.4.0`.
If you need HTTP/2 support, make sure you require that version as a minimum.

```elixir
# mix.exs
def deps do
[
{:mint_web_socket, "~> 0.1"},
{:mint, "~> 1.4"},
# ..
]
end
```

## Usage

`Mint.WebSocket` piggybacks much of the existing `Mint.HTTP` API. For example,
this snippet shows sending and receiving a text frame of "hello world" to a
WebSocket server which echos our frames:

```elixir
# bootstrap
{:ok, conn} = Mint.HTTP.connect(:http, "echo", 9000)

{:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/", [])

http_get_message = receive(do: (message -> message))
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
Mint.HTTP.stream(conn, http_get_message)

{:ok, conn, websocket} = Mint.WebSocket.new(conn, ref, status, resp_headers)

# send the hello world frame
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)

# receive the hello world reply frame
hello_world_echo_message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, hello_world_echo_message)
{:ok, websocket, [{:text, "hello world"}]} = Mint.WebSocket.decode(websocket, data)
```

## Development workflow

Interested in developing `Mint.WebSocket`? The `docker-compose.yml` sets up
Expand All @@ -108,7 +95,7 @@ fuzzing server.

```
(host)$ docker-compose up -d
(host)$ docker-compose exec app /bin/bash
(host)$ docker-compose exec app bash
(app)$ mix deps.get
(app)$ mix test
(app)$ iex -S mix
Expand Down
2 changes: 1 addition & 1 deletion coveralls.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"terminal_options": {
"file_column_width": 60
},
"skip_files": ["^deps", "^test/compare/"]
"skip_files": ["^deps", "^test/compare/", "^test/fixtures/websocket_"]
}
12 changes: 6 additions & 6 deletions examples/echo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,31 @@ require Logger
Logger.debug("Connected to https://echo.websocket.org:443")

Logger.debug("Upgrading to WebSocket protocol on /")
{:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/", [])
{:ok, conn, ref} = Mint.WebSocket.upgrade(:wss, conn, "/", [])

message = receive(do: (message -> message))
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
Mint.HTTP.stream(conn, message)
Mint.WebSocket.stream(conn, message)
{:ok, conn, websocket} = Mint.WebSocket.new(conn, ref, status, resp_headers)
Logger.debug("WebSocket established")

frame = {:text, "Rock it with Mint.WebSocket"}
Logger.debug("Sending frame #{inspect(frame)}")
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, frame)
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, ref, data)

message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, message)
{:ok, websocket, frames} = Mint.WebSocket.decode(websocket, data)
Logger.debug("Received frames #{inspect(frames)}")

frame = :close
Logger.debug("Sending frame #{inspect(frame)}")
{:ok, websocket, data} = Mint.WebSocket.encode(websocket, frame)
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, ref, data)

message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, message)
{:ok, websocket, frames} = Mint.WebSocket.decode(websocket, data)
Logger.debug("Received frames #{inspect(frames)}")

Expand Down
16 changes: 8 additions & 8 deletions examples/phoenixchat_herokuapp.exs
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
# N.B. this is a phoenix v1.3 server that sends pings periodically
# this is a phoenix v1.3 server that sends pings periodically
# see https://phoenixchat.herokuapp.com for the in-browser version
{:ok, conn} = Mint.HTTP.connect(:https, "phoenixchat.herokuapp.com", 443)

{:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/ws", [])
{:ok, conn, ref} = Mint.WebSocket.upgrade(:wss, conn, "/ws", [])

http_get_message = receive(do: (message -> message))
{:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
Mint.HTTP.stream(conn, http_get_message)
Mint.WebSocket.stream(conn, http_get_message)
{:ok, conn, websocket} = Mint.WebSocket.new(conn, ref, status, resp_headers)

{:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, ~s[{"topic":"rooms:lobby","event":"phx_join","payload":{},"ref":1}]})
{:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
{:ok, conn} = Mint.WebSocket.stream_request_body(conn, ref, data)

message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, message)
{:ok, websocket, messages} = Mint.WebSocket.decode(websocket, data)
IO.inspect(messages)

message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, message)
{:ok, websocket, messages} = Mint.WebSocket.decode(websocket, data)
IO.inspect(messages)

message = receive(do: (message -> message))
{:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
{:ok, conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, message)
{:ok, websocket, messages} = Mint.WebSocket.decode(websocket, data)
IO.inspect(messages)

message = receive(do: (message -> message))
{:ok, _conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
{:ok, _conn, [{:data, ^ref, data}]} = Mint.WebSocket.stream(conn, message)
{:ok, _websocket, messages} = Mint.WebSocket.decode(websocket, data)
IO.inspect(messages)
Loading

0 comments on commit 802316a

Please sign in to comment.