Skip to content

Commit c730f47

Browse files
authored
Transition into closed state only occurs on READING a close frame. (#14)
Invoking `close` should transition into the closed state. Receiving a close frame also needs to make the same transition, but shouldn't close the framer. Protocol errors should immediately close the connection (sending the close frame if possible). Rework the code to make this possible.
1 parent 8c5e550 commit c730f47

File tree

2 files changed

+25
-20
lines changed

2 files changed

+25
-20
lines changed

lib/protocol/websocket/connection.rb

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,17 @@ def open!
6161
return self
6262
end
6363

64+
# If not already closed, transition the connection to the closed state and send a close frame.
6465
def close!(...)
65-
@state = :closed
66+
unless @state == :closed
67+
@state = :closed
68+
69+
begin
70+
send_close(...)
71+
rescue
72+
# Ignore errors.
73+
end
74+
end
6675

6776
return self
6877
end
@@ -71,16 +80,9 @@ def closed?
7180
@state == :closed
7281
end
7382

83+
# Immediately transition the connection to the closed state and close the underlying connection.
7484
def close(...)
75-
unless @state == :closed
76-
close!
77-
78-
begin
79-
send_close(...)
80-
rescue
81-
# Ignore.
82-
end
83-
end
85+
close!(...)
8486

8587
@framer.close
8688
end
@@ -101,11 +103,9 @@ def read_frame
101103
return frame
102104
rescue ProtocolError => error
103105
close(error.code, error.message)
104-
105106
raise
106-
rescue
107-
close(Error::PROTOCOL_ERROR, $!.message)
108-
107+
rescue => error
108+
close(Error::PROTOCOL_ERROR, error.message)
109109
raise
110110
end
111111

@@ -142,11 +142,8 @@ def receive_continuation(frame)
142142
def receive_close(frame)
143143
code, reason = frame.unpack
144144

145-
# If we're already closed, then we don't need to send a close frame. Otherwise, according to the RFC, we should echo the close frame. However, it's possible it will fail to send if the connection is already closed.
146-
unless @state == :closed
147-
close!
148-
send_close(code, reason)
149-
end
145+
# On receiving a close frame, we must enter the closed state:
146+
close!(code, reason)
150147

151148
if code and code != Error::NO_ERROR
152149
raise ClosedError.new reason, code
@@ -202,6 +199,7 @@ def send_binary(buffer, **options)
202199
write_frame(@writer.pack_binary_frame(buffer, **options))
203200
end
204201

202+
# Send a control frame with data containing a specified control sequence to begin the closing handshake. Does not close the connection, until the remote end responds with a close frame.
205203
def send_close(code = Error::NO_ERROR, reason = "")
206204
frame = CloseFrame.new(mask: @mask)
207205
frame.pack(code, reason)
@@ -246,7 +244,6 @@ def read(**options)
246244
end
247245
rescue ProtocolError => error
248246
close(error.code, error.message)
249-
250247
raise
251248
end
252249
end

test/protocol/websocket/connection.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,14 @@
182182
end
183183

184184
with '#receive_close' do
185+
it "does not close the underlying connection" do
186+
close_frame = Protocol::WebSocket::CloseFrame.new
187+
close_frame.pack
188+
189+
expect(client).not.to receive(:close)
190+
close_frame.apply(connection)
191+
end
192+
185193
it "raises an exception when the close frame has an error code" do
186194
close_frame = Protocol::WebSocket::CloseFrame.new
187195
close_frame.pack(1001, "Fake error message")

0 commit comments

Comments
 (0)