This draft is also implemented by the Agoo server according to the specifications stated in Rack PR#1272.
This document details a Rack specification extension for WebSocket / EventSource servers.
The purpose of this specification is:
-
To improve application safety by phasing out the use of
hijack
and replacing it with the use of application object callbacks.This should make it easer for applications to accept WebSocket and EventSource (SSE) connections without exposing themselves to risks and errors related to IO / network logic (such as slow client attacks, buffer flooding, etc').
-
To improve separation of concerns between servers and applications, moving the IO / network logic related to WebSocket and EventSource (SSE) back to the server.
Simply put, when using a server that support this extension, the application / framework doesn’t need to have any knowledge about networking, transport protocols, IO streams, polling, etc'.
Servers that publish WebSocket and/or EventSource (SSE) support using the env['rack.upgrade?']
value MUST follow the requirements set in this document.
This document reserves the Rack env
Hash keys of rack.upgrade?
and rack.upgrade
.
A conforming server MUST set env['rack.upgrade?']
to :websocket
for incoming WebSocket connections and :sse
for incoming EventSource (SSE) connections.
If a connection is not "upgradeable", a conforming server SHOULD set env['rack.upgrade?']
to either nil
or false
. Setting the env['rack.upgrade?']
to either false
or nil
should make it easier for applications to test for server support during a normal HTTP request.
If the connection is upgradeable and a client application set a value for env['rack.upgrade']
:
-
the server MUST use that value as a WebSocket / EventSource Callback Object unless the response status code
>= 300
(redirection / error status code). -
The response
body
MUST NOT be sent when switching to a Callback Object.
If a connection is NOT upgradeable and a client application set a value for env['rack.upgrade']
:
-
The server SHOULD ignore the Callback Object and process the response as if it did not exist.
-
A server MAY use the Callback Object to allow a client to "hijack" the data stream as raw data stream. Such behavior MUST be documented.
WebSocket and EventSource connection upgrade and handling is performed using a Callback Object.
The Callback Object could be a any object which implements any (of none) of the following callbacks:
-
on_open(client)
MUST be called once the connection had been established and/or the Callback Object had been linked to theclient
object. -
on_message(client, data)
MUST be called when incoming WebSocket data is received.This callback is ignored for EventSource connections.
data
MUST be a String with an encoding of UTF-8 for text messages andbinary
encoding for non-text messages (as specified by the WebSocket Protocol).The callback object MUST assume that the
data
String will be a recyclable buffer and that it's content will be corrupted the moment theon_message
callback returns.Servers MAY, optionally, implement a recyclable buffer for the
on_message
callback. However, this is optional, is not required and might result in issues in cases where the client code is less than pristine. -
on_drained(client)
MAY be called when theclient.write
buffer becomes empty. Ifclient.pending
ever returns a non-zero value (see later on), theon_drained
callback MUST be called once the write buffer becomes empty. -
on_shutdown(client)
MAY be called during the server's graceful shutdown process, before the connection is closed and in addition to theon_close
function (which is called after the connection is closed. -
on_close(client)
MUST be called after the connection was closed for whatever reason (socket errors, parsing errors, timeouts, client disconnection,client.close
being called, etc') or the Callback Object was replaced by another Callback Object.
The server MUST provide the Callback Object with a client
object, that supports the following methods (this approach promises applications could be server agnostic):
-
env
MUST return the Rackenv
hash related to the originating HTTP request. Some changes to theenv
hash (such as removal of the IO hijacking support) MAY be implemented by the server. -
write(data)
MUST schedule all the data to be sent.data
MUST be a String. Servers MAY silently convert non-String objects to JSON if an application attempts towrite
a non-String value, otherwise servers SHOULD throw an exception.A call to
write
only promises that the data is scheduled to be sent. Implementation details may differ across servers.write
shall returntrue
on success andfalse
if the connection is closed.For WebSocket connections only (irrelevant for EventSource connections):
-
If
data
is UTF-8 encoded, the data will be sent as text. -
If
data
is binary encoded it will be sent as non-text (as specified by the WebSocket Protocol).
A server SHOULD document its concurrency model, allowing developers to know whether
write
will block or not, whether buffered IO is implemented, etc'.For example, evented servers are encouraged to avoid blocking and return immediately, deferring the actual
write
operation for later. However, (process/thread/fiber) per-connection based servers MAY choose to return only after all the data was sent. Documenting these differences will allows applications to choose the model that best fits their needs and environments. -
-
close
closes the connection once all the data scheduled usingwrite
was sent. Ifclose
is called while there is still data to be sent,close
SHOULD return immediately and only take effect once the data was sent.close
shall always returnnil
. -
open?
MUST returnfalse
if the connection was never open, is known to be closed or marked to be closed. Otherwisetrue
MUST be returned. -
pending
MUST return -1 if the connection is closed. Otherwise,pending
SHOULD return the number of pending writes (messages in thewrite
queue*) that need to be processed before the next time theon_drained
callback is called.Servers MAY choose to always return the value
0
ONLY IF they never call theon_drained
callback and the connection is open.Servers that return a positive number MUST call the
on_drained
callback when a call topending
would return the value0
.*Servers that divide large messages into a number of smaller messages (implement message fragmentation) MAY count each fragment separately, as if the fragmentation was performed by the user and
write
was called more than once per message. -
pubsub?
MUST returnfalse
unless the pub/sub extension is supported.Pub/Sub patterns are idiomatic for WebSockets and EventSource connections but their API is out of scope for this extension.
-
class
MUST return the client's Class, allowing it be extended with additional features (such as Pub/Sub, etc').Note: Ruby adds this method automatically to every class, no need to do a thing.
The server MAY support the following (optional) methods for the client
object:
-
handler
if implemented, MUST return the callback object linked to theclient
object. -
handler=
if implemented, MUST set a new Callback Object forclient
.This allows applications to switch from one callback object to another (i.e., in case of credential upgrades).
Once a new Callback Object was set, the server MUST call the old handler's
on_close
callback and afterwards call the new handler'son_open
callback.It is RECOMMENDED (but not required) that this also updates the value for
env['rack.upgrade']
. -
timeout
/timeout=
allows applications to get / set connection timeouts dynamically and separately for each connection. Servers SHOULD provide a global setting for the default connection timeout. It is RECOMMENDED (but not required) that a global / default timeout setting be available from the command line (CLI). -
protocol
if implemented, MUST return the same value that was originally set byenv['rack.upgrade?']
.
WebSocket ping
/ pong
, timeouts and network considerations SHOULD be implemented by the server. It is RECOMMENDED (but not required) that the server send ping
s to prevent connection timeouts and to detect network failure. Clients SHOULD also consider sending ping
s to detect network errors (dropped connections).
Server settings MAY be provided to allow for customization and adaptation for different network environments or WebSocket extensions. It is RECOMMENDED that any settings be available as command line arguments and not incorporated into the application's logic.
-
Server:
When a regular HTTP request arrives (non-upgradeable), the server will set the
env['rack.upgrade?']
flag tofalse
, indicating that: 1. this specific request is NOT upgradable; and 2. the server supports this specification for either WebSocket and/or EventSource connections.When a WebSocket upgrade request arrives, the server will set the
env['rack.upgrade?']
flag to:websocket
, indicating that: 1. this specific request is upgradable; and 2. the server supports this specification for WebSocket connections.When an EventSource request arrives, the server will set the
env['rack.upgrade?']
flag to:sse
, indicating that: 1. this specific request is an EventSource request; and 2. the server supports this specification for EventSource connections. -
Client:
If a client decides to upgrade a request, they will place an appropriate Callback Object in the
env['rack.upgrade']
Hash key. -
Server:
-
If the application's response status indicates an error or a redirection (status code
>= 300
), the server shall ignore the Callback Object and/or remove it from theenv
Hash, ignoring the rest of the steps that follow. -
The server will review the
env
Hash before sending the response. If theenv['rack.upgrade']
was set, the server will perform the upgrade. -
The server will send the correct response status and headers, as well as any headers present in the response object. The server will also perform any required housekeeping, such as closing the response body, if it exists.
The response status provided by the response object shall be ignored and the correct response status shall be set by the server.
-
Once the upgrade had completed, the server will call the
on_open
callback.No other callbacks shall be called until the
on_open
callback had returned.WebSocket messages shall be handled by the
on_message
callback in the same order in which they arrive and theon_message
SHOULD NOT be executed concurrently for the same connection.The
on_close
callback MUST NOT be called while any other callback is running (on_open
,on_message
,on_drained
, etc').The
on_drained
callback MAY be called concurrently with theon_message
callback, allowing data to be sent even while incoming data is being processed. Multi-threading considerations apply.
-
The following is an example WebSocket echo server implemented using this specification:
module WSConnection
def on_open(client)
puts "WebSocket connection established (#{client.object_id})."
end
def on_message(client, data)
client.write data # echo the data back
puts "on_drained MUST be implemented if #{ pending } != 0."
end
def on_drained(client)
puts "If this line prints out, on_drained is supported by the server."
end
def on_shutdown(client)
client.write "The server is going away. Goodbye."
end
def on_close(client)
puts "WebSocket connection closed (#{client.object_id})."
end
extend self
end
module App
def self.call(env)
if(env['rack.upgrade?'.freeze] == :websocket)
env['rack.upgrade'.freeze] = WSConnection
return [0, {}, []]
end
return [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Hello World!"]]
end
end
run App
The following example uses Push notifications for both WebSocket and SSE connections. The Pub/Sub API is subject to a separate Pub/Sub API extension and isn't part of this specification (it is, however, supported by iodine):
module Chat
def on_open(client)
client.class.prepend MyPubSubModule unless client.pubsub?
client.subscribe "chat"
client.publish "chat", "#{env[:nickname]} joined the chat."
end
def on_message(client, data)
client.publish "chat", "#{env[:nickname]}: #{data}"
end
def on_close(client)
client.publish "chat", "#{env[:nickname]}: left the chat."
end
extend self
end
module App
def self.call(env)
if(env['rack.upgrade?'.freeze])
nickname = env['PATH_INFO'][1..-1]
nickname = "Someone" if nickname == "".freeze
env[:nickname] = nickname
return [0, {}, []]
end
return [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Hello World!"]]
end
end
run App
Note that SSE connections will only be able to receive messages (the on_message
callback is never called).