Skip to content

Commit c08d760

Browse files
committed
Introduce #close_write and #shutdown.
1 parent 2f5dfa1 commit c08d760

File tree

4 files changed

+78
-3
lines changed

4 files changed

+78
-3
lines changed

lib/protocol/websocket/close_frame.rb

+8
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ def pack(code = nil, reason = nil)
5959
end
6060
end
6161

62+
# Generate a suitable reply.
63+
# @returns [CloseFrame]
64+
def reply(code = Error::NO_ERROR, reason = "")
65+
frame = CloseFrame.new
66+
frame.pack(code, reason)
67+
return frame
68+
end
69+
6270
# Apply this frame to the specified connection.
6371
def apply(connection)
6472
connection.receive_close(self)

lib/protocol/websocket/connection.rb

+22-1
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,34 @@ def closed?
8989
@state == :closed
9090
end
9191

92-
# Immediately transition the connection to the closed state *and* close the underlying connection.
92+
# Immediately transition the connection to the closed state *and* close the underlying connection. Any data not yet read will be lost.
9393
def close(...)
9494
close!(...)
9595

9696
@framer.close
9797
end
9898

99+
# Close the connection gracefully, sending a close frame with the specified error code and reason. If an error occurs while sending the close frame, the connection will be closed immediately. You may continue to read data from the connection after calling this method, but you should not write any more data.
100+
#
101+
# @parameter error [Error | Nil] The error that occurred, if any.
102+
def close_write(error = nil)
103+
if error
104+
send_close(Error::INTERNAL_ERROR, error.message)
105+
else
106+
send_close
107+
end
108+
end
109+
110+
# Close the connection gracefully. This will send a close frame and wait for the remote end to respond with a close frame. Any data received after the close frame is sent will be ignored. If you want to process this data, use {#close_write} instead, and read the data before calling {#close}.
111+
def shutdown
112+
send_close unless @state == :closed
113+
114+
# `read_frame` will return nil after receiving a close frame:
115+
while read_frame
116+
# Drain the connection.
117+
end
118+
end
119+
99120
# Read a frame from the framer, and apply it to the connection.
100121
def read_frame
101122
return nil if closed?

lib/protocol/websocket/error.rb

+16-2
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,24 @@ class Error < HTTP::Error
1919
# Indicates that an endpoint is terminating the connection due to a protocol error.
2020
PROTOCOL_ERROR = 1002
2121

22-
# Indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept.
22+
# Indicates that an endpoint is terminating the connection because it has received a type of data it cannot accept. (e.g., an endpoint that understands only text data MAY send this if it receives a binary message).
2323
INVALID_DATA = 1003
2424

25-
# There are other status codes but most of them are "implementation specific".
25+
26+
# Indicates that an endpoint is terminating the connection because it has received data within a message that was not consistent with the type of the message (e.g., non-UTF-8 data within a text message).
27+
INVALID_PAYLOAD = 1007
28+
29+
# Indicates that an endpoint is terminating the connection because it has received a message that violates its policy. This is a generic status code that can be returned when there is no other more suitable status code (e.g., 1003 or 1009) or if there is a need to hide specific details about the policy.
30+
POLICY_VIOLATION = 1008
31+
32+
# Indicates that an endpoint is terminating the connection because it has received a message that is too big for it to process.
33+
MESSAGE_TOO_LARGE = 1009
34+
35+
# Indicates that an endpoint (client) is terminating the connection because it has expected the server to negotiate one or more extension, but the server didn't return them in the response message of the WebSocket handshake. The list of extensions that are needed should appear in the /reason/ part of the Close frame. Note that this status code is not used by the server, because it can fail the WebSocket handshake instead.
36+
MISSING_EXTENSION = 1010
37+
38+
# Indicates that a server is terminating the connection because it encountered an unexpected condition that prevented it from fulfilling the request.
39+
INTERNAL_ERROR = 1011
2640
end
2741

2842
# Raised by stream or connection handlers, results in GOAWAY frame which signals termination of the current connection. You *cannot* recover from this exception, or any exceptions subclassed from it.

test/protocol/websocket/connection.rb

+32
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,38 @@
3131
end
3232
end
3333

34+
with "#close_write" do
35+
it "can close the write side of the connection" do
36+
connection.close_write
37+
38+
frame = client.read_frame
39+
expect(frame).to be_a(Protocol::WebSocket::CloseFrame)
40+
41+
client.write_frame(frame.reply)
42+
client.close
43+
44+
frame = connection.read_frame
45+
expect(frame).to be_a(Protocol::WebSocket::CloseFrame)
46+
47+
expect(connection).to be(:closed?)
48+
end
49+
end
50+
51+
with "#shutdown" do
52+
it "can shutdown the connection" do
53+
data_frame = Protocol::WebSocket::TextFrame.new(true).tap do |frame|
54+
frame.pack("Hello World!")
55+
end
56+
client.write_frame(data_frame)
57+
58+
close_frame = Protocol::WebSocket::CloseFrame.new.tap(&:pack)
59+
client.write_frame(close_frame)
60+
61+
connection.shutdown
62+
expect(connection).to be(:closed?)
63+
end
64+
end
65+
3466
it "doesn't generate mask by default" do
3567
expect(connection.mask).to be == nil
3668
end

0 commit comments

Comments
 (0)