Skip to content

Commit 16834ce

Browse files
committed
add close callback for websockets
WebSocket clients can close their end of the connection, and RFC 6455 requires servers to handle that message appropriately. This change adds a new callback message for basic WebSocket callback modules. The server now also replies to client "close" messages with a "close" reply, as RFC 6455 requires. WebSockets documentation updated as well. Also included a message ordering fix from Jan Bothma.
1 parent 0944464 commit 16834ce

File tree

3 files changed

+62
-16
lines changed

3 files changed

+62
-16
lines changed

doc/yaws.tex

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2726,8 +2726,8 @@ \chapter{WebSocket Protocol Support}
27262726
allow the client to establish a WebSocket connection to the
27272727
server. These connections are handled by the code in
27282728
\verb+www/websockets_example_endpoint.yaws+, which when invoked simply
2729-
establishes \\ \verb+src/basic_echo_callback.erl+ as the WebSocket
2730-
callback module for the connection.
2729+
establishes \\ \verb+examples/src/basic_echo_callback.erl+ as the
2730+
WebSocket callback module for the connection.
27312731

27322732
\section{WebSocket Callback Modules}
27332733

@@ -2749,6 +2749,14 @@ \subsection{Basic Callback Modules}
27492749
\item \verb+{binary, Message}+ --- the callback receives an
27502750
unfragmented binary message.
27512751

2752+
\item \verb+{close, Status, Reason}+ --- the callback receives a
2753+
notification that the client has closed the socket. \verb+Status+ is
2754+
the numerical status code sent by the client or the value 1000 (as
2755+
suggested by RFC 6455 section 7.4.1) if the client sent no status
2756+
code. \verb+Reason+ is a binary containing any text the client sent
2757+
to indicate the reason for closing the socket; this binary may be
2758+
empty.
2759+
27522760
\end{itemize}
27532761

27542762
The \verb+handle_message/1+ callback function supplies one of the
@@ -2765,7 +2773,11 @@ \subsection{Basic Callback Modules}
27652773

27662774
\item \verb+{close, Reason}+ --- close the connection and exit the
27672775
handling process with \verb+Reason+. For a regular non-error close,
2768-
\verb+Reason+ should be the atom \verb+normal+.
2776+
\verb+Reason+ should be the atom \verb+normal+. \verb+Reason+ may
2777+
alternatively be any allowed numerical value specified for
2778+
\verb+close+ frames in section 7.4 of RFC 6455. Callback handlers
2779+
for \verb+close+ messages from the client must always return
2780+
\verb+{close, Reason}+.
27692781

27702782
\end{itemize}
27712783

@@ -2808,9 +2820,9 @@ \subsection{Advanced Callback Modules}
28082820
themselves---supply a \verb+handle_message/2+ callback function.
28092821

28102822
To indicate an advanced callback module, include
2811-
\verb+{callback, {advanced, InitialState}}+ in the \verb+Options+ list
2812-
when you return \verb+{websocket, CallbackModule, Options}+ from your
2813-
\verb+out/1+ function, as described above.
2823+
\verb+{callback, {advanced, InitialState}}+ in the \\ \verb+Options+
2824+
list when you return \verb+{websocket, CallbackModule, Options}+ from
2825+
your \verb+out/1+ function, as described above.
28142826

28152827
The arguments to the \verb+handle_message/2+ callback
28162828
function are as follows:
@@ -2839,13 +2851,17 @@ \subsection{Advanced Callback Modules}
28392851
callback module.
28402852

28412853
\item \verb+{reply, Reply, State}+ --- reply to the received message
2842-
with \verb+Reply+, which is either \verb+{text, Data}+ or
2854+
with \verb+Reply+, which is either \\ \verb+{text, Data}+ or
28432855
\verb+{binary, Data}+. \verb+State+ is the (possibly updated) state
28442856
for the callback module.
28452857

28462858
\item \verb+{close, Reason}+ --- close the connection and exit the
28472859
handling process with \verb+Reason+. For a regular non-error close,
2848-
\verb+Reason+ should be the atom \verb+normal+.
2860+
\verb+Reason+ should be the atom \verb+normal+. \verb+Reason+ may
2861+
alternatively be any allowed numerical value specified for
2862+
\verb+close+ frames in section 7.4 of RFC 6455. Callback handlers
2863+
for \verb+close+ frames from the client must always return
2864+
\verb+{close, Reason}+.
28492865

28502866
\end{itemize}
28512867

examples/src/basic_echo_callback.erl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,10 @@ handle_message({text, Message}) ->
2828
{reply, {text, <<Message/binary>>}};
2929

3030
handle_message({binary, Message}) ->
31-
{reply, {binary, Message}}.
31+
{reply, {binary, Message}};
32+
33+
handle_message({close, _Status, _Reason}) ->
34+
{close, normal}.
3235

3336

3437
say_hi(Pid) ->

src/yaws_websockets.erl

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,7 @@ handle_result_fun(WSState) ->
177177
noreply ->
178178
ok;
179179
{close, Reason} ->
180-
exit(Reason)
181-
180+
do_close(WSState, Reason)
182181
end
183182
end.
184183

@@ -191,10 +190,19 @@ do_callback_fun(WSState, CallbackMod) ->
191190
{noreply, NewCallbackState} ->
192191
NewCallbackState;
193192
{close, Reason} ->
194-
exit(Reason)
193+
%% the following does not return
194+
do_close(WSState, Reason)
195195
end
196196
end.
197197

198+
do_close(WSState, Reason) ->
199+
Status = case Reason of
200+
_ when is_integer(Reason) -> Reason;
201+
_ -> 1000
202+
end,
203+
send(WSState, {close, <<Status:16/big>>}),
204+
exit(Reason).
205+
198206
basic_messages(FrameInfos, {FragType, FragAcc}) ->
199207
{Messages, NewFragType, NewFragAcc}
200208
= lists:foldl(fun handle_message/2, {[], FragType, FragAcc}, FrameInfos),
@@ -221,13 +229,13 @@ handle_message(#ws_frame_info{fin=1,
221229
{Messages, text, FragAcc}) ->
222230
Unfragged = <<FragAcc/binary, Data/binary>>,
223231
NewMessage = {text, Unfragged},
224-
{[NewMessage | Messages], none, <<>>};
232+
{Messages ++ [NewMessage], none, <<>>};
225233

226234
%% unfragmented text message
227235
handle_message(#ws_frame_info{opcode=text, data=Data},
228236
{Messages, none, <<>>}) ->
229237
NewMessage = {text, Data},
230-
{[NewMessage | Messages], none, <<>>};
238+
{Messages ++ [NewMessage], none, <<>>};
231239

232240
%% end of binary fragmented message
233241
handle_message(#ws_frame_info{fin=1,
@@ -236,13 +244,13 @@ handle_message(#ws_frame_info{fin=1,
236244
{Messages, binary, FragAcc}) ->
237245
Unfragged = <<FragAcc/binary, Data/binary>>,
238246
NewMessage = {binary, Unfragged},
239-
{[NewMessage|Messages], none, <<>>};
247+
{Messages ++ [NewMessage], none, <<>>};
240248

241249
handle_message(#ws_frame_info{opcode=binary,
242250
data=Data},
243251
{Messages, none, <<>>}) ->
244252
NewMessage = {binary, Data},
245-
{[NewMessage|Messages], none, <<>>};
253+
{Messages ++ [NewMessage], none, <<>>};
246254

247255
handle_message(#ws_frame_info{opcode=ping,
248256
data=Data,
@@ -257,6 +265,25 @@ handle_message(#ws_frame_info{opcode=pong}, Acc) ->
257265
%% draft-ietf-hybi-thewebsocketprotocol-08#section-4
258266
Acc;
259267

268+
%% According to RFC 6455 section 5.4, control messages like close
269+
%% MAY be injected in the middle of a fragmented message, which is
270+
%% why we pass FragType and FragAcc along below. Whether any clients
271+
%% actually do this in practice, I don't know.
272+
handle_message(#ws_frame_info{opcode=close,
273+
length=Len,
274+
data=Data},
275+
{Messages, FragType, FragAcc}) ->
276+
NewMessage = case Len of
277+
0 ->
278+
%% RFC 6455 section 7.4.1:
279+
%% status code 1000 means "normal"
280+
{close, 1000, <<>>};
281+
_ ->
282+
<<Status:16/big, Msg/binary>> = Data,
283+
{close, Status, Msg}
284+
end,
285+
{Messages ++ [NewMessage], FragType, FragAcc};
286+
260287
handle_message(#ws_frame_info{}, Acc) ->
261288
Acc.
262289

0 commit comments

Comments
 (0)