11defmodule Mint.WebSocket do
22 @ moduledoc """
3- WebSocket
3+ (Unofficial) WebSocket support for the Mint functional HTTP client
4+
5+ Like Mint, `Mint.WebSocket` provides a functional, process-less interface
6+ for operating a WebSocket connection. `Mint.WebSocket` is an extension
7+ to Mint: the sending and receiving of messages is done with Mint functions.
8+ Prospective Mint.WebSocket users should first familiarize themselves with
9+ `Mint.HTTP`.
10+
11+ Mint.WebSocket is not fully spec-conformant on its own. Runtime behaviors
12+ such as responding to pings with pongs must be implemented by the user of
13+ Mint.WebSocket.
14+
15+ ## Usage
16+
17+ A connection formed with `Mint.HTTP.connect/4` can be upgraded to a WebSocket
18+ connection with `upgrade/4`.
19+
20+ ```elixir
21+ {:ok, conn} = Mint.HTTP.connect(:http, "localhost", 9_000)
22+ {:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/", [])
23+ ```
24+
25+ `upgrade/4` sends an upgrade request to the remote server. The WebSocket
26+ connection is then built by awaiting the HTTP response from the server.
27+
28+ ```elixir
29+ http_reply_message = receive(do: (message -> message))
30+ {:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, resp_headers}, {:done, ^ref}]} =
31+ Mint.HTTP.stream(conn, http_reply_message)
32+
33+ {:ok, conn, websocket} =
34+ Mint.WebSocket.new(conn, ref, status, resp_headers)
35+ ```
36+
37+ Now that the WebSocket connection has been formed, we use the `websocket`
38+ data structure to encode and decode frames, and the
39+ `Mint.HTTP.stream_request_body/3` and `Mint.HTTP.stream/2` functions from
40+ Mint to perform sending and receiving of encoded frames.
41+
42+ For example, we'll send a "hello world" text frame across our connection.
43+
44+ ```elixir
45+ {:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
46+ {:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
47+ ```
48+
49+ And let's say that the server is echoing our messages; let's receive our
50+ echoed "hello world" text frame.
51+
52+ ```elixir
53+ echo_message = receive(do: (message -> message))
54+ {:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, echo_message)
55+ {:ok, websocket, [{:text, "hello world"}]} = Mint.WebSocket.decode(websocket, data)
56+ ```
57+
58+ ## HTTP/2 Support
59+
60+ Mint.WebSocket supports WebSockets over HTTP/2 as defined in rfc8441.
61+ rfc8441 is an extension to the HTTP/2 specification. At the time of
62+ writing, very few HTTP/2 server libraries support or enable HTTP/2
63+ WebSockets by default.
64+
65+ `upgrade/4` works on both HTTP/1 and HTTP/2 connections. In order to select
66+ HTTP/2, the `:http2` protocol should be explicitly selected in
67+ `Mint.HTTP.connect/4`.
68+
69+ ```elixir
70+ {:ok, %Mint.HTTP2{} = conn} =
71+ Mint.HTTP.connect(:http, "websocket.example", 80, protocols: [:http2])
72+ {:ok, conn, ref} = Mint.WebSocket.upgrade(conn, "/", [])
73+ ```
74+
75+ If the server does not support the extended CONNECT method needed to bootstrap
76+ WebSocket connections over HTTP/2, `upgrade/4` will return an error tuple
77+ with the `:extended_connect_disabled` error reason.
78+
79+ ```elixir
80+ {:error, conn, %Mint.WebSocketError{reason: :extended_connect_disabled}}
81+ ```
82+
83+ Why use HTTP/2 for WebSocket connections in the first place? HTTP/2
84+ can multiplex many requests over the same connection, which can
85+ reduce the latency incurred by forming new connections for each request.
86+ A WebSocket connection only occupies one stream of a HTTP/2 connection, so
87+ even if an HTTP/2 connection has an open WebSocket communication, it can be
88+ used to transport more requests.
89+
90+ ## WebSocket Secure
91+
92+ Encryption of connections is handled by Mint functions. To start a WSS
93+ connection, select `:https` as the scheme in `Mint.HTTP.connect/4`:
94+
95+ ```elixir
96+ {:ok, conn} = Mint.HTTP.connect(:https, "websocket.example", 443)
97+ ```
98+
99+ And use `upgrade/4` to upgrade the connection to WebSocket. See the
100+ Mint documentation on SSL for more information.
101+
102+ ## Extensions
103+
104+ The WebSocket protocol allows for _extensions_. Extensions act as a
105+ middleware for encoding and decoding frames. For example "permessage-deflate"
106+ compresses and decompresses the body of data frames, which minifies the amount
107+ of bytes which must be sent over the network.
108+
109+ See `Mint.WebSocket.Extension` for more information about extensions and
110+ `Mint.WebSocket.PerMessageDeflate` for information about the
111+ "permessage-deflate" extension.
4112 """
5113
6- require __MODULE__ . Frame , as: Frame
7- alias __MODULE__ . { Utils , Extension }
114+ alias __MODULE__ . { Utils , Extension , Frame }
8115 alias Mint.WebSocketError
9116
10- @ type t :: % __MODULE__ { }
117+ @ typedoc """
118+ An immutable data structure representing a WebSocket connection
119+ """
120+ @ opaque t :: % __MODULE__ {
121+ extensions: [ Extension . t ( ) ] ,
122+ fragment: tuple ( ) ,
123+ private: map ( ) ,
124+ buffer: binary ( )
125+ }
11126 defstruct extensions: [ ] ,
12- fragments: [ ] ,
127+ fragment: nil ,
13128 private: % { } ,
14129 buffer: << >>
15130
16131 @ type error :: Mint.Types . error ( ) | WebSocketError . t ( )
17132
133+ @ typedoc """
134+ Shorthand notations for control frames
135+
136+ * `:ping` - shorthand for `{:ping, ""}`
137+ * `:pong` - shorthand for `{:pong, ""}`
138+ * `:close` - shorthand for `{:close, 1_000, ""}`
139+
140+ These may be passed to `encode/2`.
141+
142+ <!--
143+ Note that the shorthand notations may be passed to `encode/2`
144+ but frames returned from `decode/2` will never be in
145+ shorthand format.
146+ -->
147+ """
148+ @ type shorthand_frame :: :ping | :pong | :close
149+
150+ @ typedoc """
151+ A WebSocket frame
152+
153+ * `{:binary, binary}` - a frame containing binary data. Binary frames
154+ can be used to send arbitrary binary data such as a PDF.
155+ * `{:text, text}` - a frame containing string data. Text frames must be
156+ valid utf8. Elixir has wonderful support for utf8: `String.valid?/1`
157+ can detect valid and invalid utf8.
158+ * `{:ping, binary}` - a control frame which the server should respond to
159+ with a pong. The binary data must be echoed in the pong response.
160+ * `{:pong, binary}` - a control frame which forms a reply to a ping frame.
161+ Pings and pongs may be used to check the a connection is alive or to
162+ estimate latency.
163+ * `{:close, code, reason}` - a control frame used to request that a connection
164+ be closed or to acknowledgee a close frame send by the server.
165+
166+ These may be passed to `encode/2` or returned from `decode/2`.
167+
168+ ## Close frames
169+
170+ In order to close a WebSocket connection gracefully, either the client or
171+ server sends a close frame. Then the other endpoint responds with a
172+ close with code `1_000` and then closes the TCP connection. This can be
173+ accomplished in Mint.WebSocket like so:
174+
175+ ```elixir
176+ {:ok, websocket, data} = Mint.WebSocket.encode(websocket, :close)
177+ {:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
178+
179+ close_response = receive(do: (message -> message))
180+ {:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, close_response)
181+ {:ok, websocket, [{:close, 1_000, ""}]} = Mint.WebSocket.decode(websocket, data)
182+
183+ Mint.HTTP.close(conn)
184+ ```
185+
186+ [rfc6455
187+ section 7.4.1](https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.1)
188+ documents codes which may be used in the `code` element.
189+ """
18190 @ type frame ::
19- { :text , binary ( ) }
191+ { :text , String . t ( ) }
20192 | { :binary , binary ( ) }
21- | :ping
22193 | { :ping , binary ( ) }
23- | :pong
24194 | { :pong , binary ( ) }
25- | :close
26195 | { :close , code :: non_neg_integer ( ) , reason :: binary ( ) }
27196
28197 @ doc """
@@ -35,13 +204,49 @@ defmodule Mint.WebSocket do
35204 WebSocket-specific headers. For HTTP/2 connections, this function performs
36205 an extended CONNECT request which opens a stream to be used for the WebSocket
37206 connection.
207+
208+ ## Options
209+
210+ * `:extensions` - a list of extensions to negotiate. See the extensions
211+ section below.
212+
213+ ## Extensions
214+
215+ Extensions should be declared by passing the `:extensions` option in the
216+ `opts` keyword list. Note that in the WebSocket protocol, extensions are
217+ negotiated: the client proposes a list of extensions and the server may
218+ accept any (or none) of them. See `Mint.WebSocket.Extension` for more
219+ information about extension negotiation.
220+
221+ Extensions may be passed as a list of `Mint.WebSocket.Extension` structs
222+ or with the following shorthand notations:
223+
224+ * `module` - shorthand for `{module, []}`
225+ * `{module, params}` - shorthand for `{module, params, []}`
226+ * `{module, params, opts}` - a shorthand which is expanded to a
227+ `Mint.WebSocket.Extension` struct
228+
229+ ## Examples
230+
231+ ```elixir
232+ {:ok, conn} = Mint.HTTP.connect(:http, "localhost", 9_000)
233+ {:ok, conn, ref} =
234+ Mint.WebSocket.upgrade(conn, "/", [], extensions: [Mint.WebSocket.PerMessageDeflate])
235+ # or provide params:
236+ {:ok, conn, ref} =
237+ Mint.WebSocket.upgrade(
238+ conn,
239+ "/",
240+ [],
241+ extensions: [{Mint.WebSocket.PerMessageDeflate, [:client_max_window_bits]]}]
242+ )
243+ ```
38244 """
39245 @ spec upgrade (
40246 conn :: Mint.HTTP . t ( ) ,
41247 path :: String . t ( ) ,
42248 headers :: Mint.Types . headers ( ) ,
43- # maybe t:Keyword.t/0, will hold extensions in the future
44- opts :: list ( )
249+ opts :: Keyword . t ( )
45250 ) :: { :ok , Mint.HTTP . t ( ) , Mint.Types . request_ref ( ) } | { :error , Mint.HTTP . t ( ) , error ( ) }
46251 def upgrade ( conn , path , headers , opts \\ [ ] )
47252
@@ -83,6 +288,24 @@ defmodule Mint.WebSocket do
83288 { :error , conn , % WebSocketError { reason: :extended_connect_disabled } }
84289 end
85290
291+ @ doc """
292+ Creates a new WebSocket data structure given the server's reply to the
293+ upgrade request
294+
295+ This function will setup any extensions accepted by the server using
296+ the `c:Mint.WebSocket.Extension.init/2` callback.
297+
298+ ## Examples
299+
300+ ```elixir
301+ http_reply = receive(do: (message -> message))
302+ {:ok, conn, [{:status, ^ref, status}, {:headers, ^ref, headers}, {:done, ^ref}]} =
303+ Mint.HTTP.stream(conn, http_reply)
304+
305+ {:ok, conn, websocket} =
306+ Mint.WebSocket.new(conn, ref, status, resp_headers)
307+ ```
308+ """
86309 @ spec new ( Mint.HTTP . t ( ) , reference ( ) , pos_integer ( ) , Mint.Types . headers ( ) ) ::
87310 { :ok , Mint.HTTP . t ( ) , t ( ) , [ Mint.Types . response ( ) ] } | { :error , Mint.HTTP . t ( ) , error ( ) }
88311 def new ( % Mint.HTTP1 { } = conn , _request_ref , status , _response_headers )
@@ -114,10 +337,42 @@ defmodule Mint.WebSocket do
114337 { :error , conn , % WebSocketError { reason: :connection_not_upgraded } }
115338 end
116339
117- @ spec encode ( t ( ) , frame ( ) ) :: { :ok , t ( ) , binary ( ) } | { :error , t ( ) , any ( ) }
118- defdelegate encode ( websocket , data ) , to: Frame
340+ @ doc """
341+ Encodes a frame into a binary
342+
343+ The resulting binary may be sent with `Mint.HTTP.stream_request_body/3`.
344+
345+ This function will invoke the `c:Mint.WebSocket.Extension.encode/2` callback
346+ for any accepted extensions.
347+
348+ ## Examples
119349
120- @ spec decode ( t ( ) , data :: binary ( ) ) :: { :ok , t ( ) , [ frame ( ) ] } | { :error , t ( ) , any ( ) }
350+ ```elixir
351+ {:ok, websocket, data} = Mint.WebSocket.encode(websocket, {:text, "hello world"})
352+ {:ok, conn} = Mint.HTTP.stream_request_body(conn, ref, data)
353+ ```
354+ """
355+ @ spec encode ( t ( ) , shorthand_frame ( ) | frame ( ) ) :: { :ok , t ( ) , binary ( ) } | { :error , t ( ) , any ( ) }
356+ defdelegate encode ( websocket , frame ) , to: Frame
357+
358+ @ doc """
359+ Decodes a binary into a list of frames
360+
361+ The binary may received from the connection with `Mint.HTTP.stream/2`.
362+
363+ This function will invoke the `c:Mint.WebSocket.Extension.decode/2` callback
364+ for any accepted extensions.
365+
366+ ## Examples
367+
368+ ```elixir
369+ message = receive(do: (message -> message))
370+ {:ok, conn, [{:data, ^ref, data}]} = Mint.HTTP.stream(conn, message)
371+ {:ok, websocket, frames} = Mint.WebSocket.decode(websocket, data)
372+ ```
373+ """
374+ @ spec decode ( t ( ) , data :: binary ( ) ) ::
375+ { :ok , t ( ) , [ frame ( ) | { :error , term ( ) } ] } | { :error , t ( ) , any ( ) }
121376 defdelegate decode ( websocket , data ) , to: Frame
122377
123378 # we re-open the request in the conn for HTTP1 connections because a :done
0 commit comments