Version: 1.6
This document proposes a standard interface between Rust network protocol servers (particularly web servers) and Python applications, intended to allow the handling of multiple common protocol styles (including HTTP, HTTP/2, and WebSocket).
This base specification is intended to fix in place the set of APIs by which these servers interact and run application code; each supported protocol (such as HTTP) has a sub-specification that outlines how to handle it in a specific way.
The ASGI specification works well in a Python-only world, allowing the same great flexibility WSGI introduced. However, its design is irrevocably tied to the Python language itself, and the AsyncIO implementation. For instance, ASGI design is built around the idea that the socket transport and the threading paradigm is handled by Python itself; a condition that might lead to inefficient paradigms when looking at implementation coming from different languages. We can summarise this concept into this phrase: ASGI expects the lower protocol to be handled by Python.
As the abstract suggests, RSGI is designed to solve the inefficiencies we described for servers written in the Rust language, where the actual I/O communication and threading components are handled outside the Python interpreter, to allow applications to take advantage of the performance provided by the protocol implementation.
RSGI attempts to preserve a simple application interface like ASGI does, while providing an abstraction that allows data to be sent and received through Rust built protocols. This is why, for example, RSGI keeps the same interfaces on the application layer both for HTTP requests and Websockets, but expects different usage of those interfaces based on the protocols: unlike ASGI, requests won't be handled using messages.
As we said, RSGI is not built around the idea of Python handling the lower protocols, and thus its design is not meant to preserve interoperability with ASGI and WSGI: the I/O fundamentals changed, and supporting the previous one would have been a flawed decision since its begin.
Its primary goal is to provide a way to write HTTP/1, HTTP/2, HTTP/3 and WebSocket code in Python, taking advantage of an efficient lower protocol.
RSGI consists of two different components:
- A protocol server, which terminates sockets and translates them into connections and per-connection objects.
- An application, which lives inside a protocol server, is called once per connection, and handles connections events as they happen, emitting its own events back when necessary.
Like ASGI, the server hosts the application inside it, and dispatches incoming requests to it in a standardized format; like ASGI applications are asynchronous callables, and they communicate with the server by interacting with awaitable objects. RSGI applications must run as async/await compatible coroutines (i.e. asyncio-compatible) (on the main thread; they are free to use threading or other processes if they need synchronous code).
There are two separate parts to an RSGI connection:
- A connection scope, like ASGI, which represents a protocol connection to a user and survives until the connection closes.
- A connection protocol interface the application can interact with, that will responsible of trasmitting data from and to the client.
Applications are consequently called and awaited with a connection scope and a connection protocol to interact with. All this happening in an asynchronous event loop.
Each call of the application callable maps to a single incoming “socket” or connection, and is expected to last the lifetime of that connection plus a little longer if there is cleanup to do.
RSGI applications should be a single async callable:
coroutine application(scope, protocol)
scope: the connection scope information, an object that contains type key specifying the protocol that is incoming and the relevant informationprotocol: an object with awaitable methods to communicate data
The application is called once per "connection". The definition of a connection and its lifespan are dictated by the protocol specification in question. For example, with HTTP it is one request, whereas for a WebSocket it is a single WebSocket connection.
The protocol-specific sub-specifications cover scope and protocol specifications.
RSGI specification also includes the following additional methods for applications.
RSGI applications intended to support additional protocols which bind to an async callable like ASGI can expose the more specific __rsgi__ method interface in place of the default call.
For instance, an application supporting both RSGI and ASGI protocols will look like the following:
class App:
async def __call__(self, scope, receive, send):
# ASGI protocol handling
async def __rsgi__(self, scope, protocol):
# RSGI protocol handlingRSGI compliant servers should prefer the __rsgi__ method over __call__ when present.
The init method provides a way for RSGI applications to perform initialization operations during server startup.
The signature of __rsgi_init__ is defined as follows, with the loop argument being the Python asyncio event loop:
function __rsgi_init__(loop)
Note: the event loop won't be running at the time the init function gets called
Thus, an application exposing the RSGI init interface might look like the following:
class App:
def __rsgi_init__(self, loop):
some_sync_init_task()
loop.run_until_complete(some_async_init_task())
async def __rsgi__(self, scope, protocol):
# RSGI protocol handlingThe del method provides a way for RSGI applications to perform cleanup operations during server shutdown.
The signature of __rsgi_del__ is defined as follows, with the loop argument being the Python asyncio event loop:
function __rsgi_del__(loop)
Note: the event loop won't be running at the time the del function gets called
Thus, an application exposing the RSGI del interface might look like the following:
class App:
def __rsgi_del__(self, loop):
some_sync_cleanup_task()
loop.run_until_complete(some_async_cleanup_task())
async def __rsgi__(self, scope, protocol):
# RSGI protocol handlingThe HTTP format covers HTTP/1.0, HTTP/1.1 and HTTP/2. The HTTP version is available as a string in the scope.
HTTP connections have a single-request connection scope - that is, your application will be called at the start of the request, and will last until the end of that specific request, even if the underlying socket is still open and serving multiple requests.
If you hold a response open for long-polling or similar, the connection scope will persist until the response closes from either the client or server side.
The scope object for HTTP protocol is defined as follows:
class Scope:
proto: Literal['http'] = 'http'
rsgi_version: str
http_version: str
server: str
client: str
scheme: str
method: str
path: str
query_string: str
headers: Mapping[str, str]
authority: Optional[str]And here are descriptions for the upper attributes:
rsgi_version: a string containing the version of the RSGI spechttp_version: a string containing the HTTP version (one of "1", "1.1" or "2")server: a string in the format{address}:{port}, where host is the listening address for this server, and port is the integer listening portclient: a string in the format{address}:{port}, where host is the remote host's address and port is the remote portscheme: URL scheme portion (one of "http" or "https")method: the HTTP method name, uppercasedpath: HTTP request target excluding any query stringquery_string: URL portion after the?headers: a mapping-like object, where key is the header name, and value is the header value; header names are always lower-case; aget_allmethod returns a list of all the header values for the given keyauthority: an optional string containing the relevant pseudo-header (empty on HTTP versions prior to 2)
HTTP protocol object implements two awaitable methods to receive the request body, five different methods to send data, and one awaitable method to wait for client disconnection, in particular:
__call__to receive the entire body inbytesformat__aiter__to receive the body inbyteschunksclient_disconnectto watch for client disconnectionresponse_emptyto send back an empty responseresponse_strto send back a response with astrbodyresponse_bytesto send back a response withbytesbodyresponse_fileto send back a file response (from its path)response_file_rangeto send back a file range response (from its path)response_streamto start a stream response
All the upper-mentioned response methods accepts an integer status parameter, a list of string tuples for the headers parameter, and the relevant typed body parameter (if applicable):
coroutine __call__() -> body
asynciterator __aiter__() -> body chunks
coroutine client_disconnect()
function response_empty(status, headers)
function response_str(status, headers, body)
function response_bytes(status, headers, body)
function response_file(status, headers, file)
function response_file_range(status, headers, file, start, end)
function response_stream(status, headers) -> transport
The response_file_range method accepts the additional start and end parameters, which represent the initial and final positions of the range to read from the file. The range is inclusive of the start and exclusive of the end (as the stdlib range function).
Note: for both
response_fileandresponse_file_rangemethods, the RSGI protocol delegates the responsibility of producing correct headers to the application.
The response_stream method will return a transport object, which implements the async messaging interfaces, specifically:
- a
send_bytesawaitable method to produce outgoing messages frombytescontent - a
send_strawaitable method to produce outgoing messages fromstrcontent
coroutine send_bytes(bytes)
coroutine send_str(str)
The client_disconnect method will return a future that resolve ones the client has disconnected.
Note: as HTTP supports keep-alived connections, the lifecycle of the client connection might not be the same of the single request. This is why the RSGI specification doesn't imply
client_disconnectshould resolve in case a client sends multiple requests within the same connection, and thus the protocol delegates to the application the responsibility to cancel the disconnection watcher once the response is sent.
WebSockets share some HTTP details - they have a path and headers - but also have more state. Again, most of that state is in the scope, which will live as long as the socket does.
WebSocket connections' scope lives as long as the socket itself - if the application dies the socket should be closed, and vice-versa.
The scope object for Websocket protocol is defined as follows:
class Scope:
proto: Literal['ws'] = 'ws'
rsgi_version: str
http_version: str
server: str
client: str
scheme: str
method: str
path: str
query_string: str
headers: Mapping[str, str]
authority: Optional[str]And here are descriptions for the upper attributes:
rsgi_version: a string containing the version of the RSGI spechttp_version: a string containing the HTTP version (one of "1", "1.1" or "2")server: a string in the format{address}:{port}, where host is the listening address for this server, and port is the integer listening portclient: a string in the format{address}:{port}, where host is the remote host's address and port is the remote portscheme: URL scheme portion (one of "http" or "https")method: the HTTP method name, uppercasedpath: HTTP request target excluding any query stringquery_string: URL portion after the?headers: a mapping-like object, where key is the header name, and value is the header value; header names are always lower-case; aget_allmethod returns a list of all the header values for the given keyauthority: an optional string containing the relevant pseudo-header (empty on HTTP versions prior to 2)
Websocket protocol object implements two interface methods for applications:
- the
acceptawaitable method - the
closemethod
coroutine accept() -> transport
function close(status)
The accept awaitable method will return a transport object, which implements the async messaging interfaces, specifically:
- a
receiveawaitable method which returns a single incoming message - a
send_bytesawaitable method to produce outgoing messages frombytescontent - a
send_strawaitable method to produce outgoing messages fromstrcontent
coroutine receive() -> message
coroutine send_bytes(bytes)
coroutine send_str(str)
In RSGI websockets' incoming messages consist of objects with the form:
class WebsocketMessage:
kind: int
data: Optional[Union[bytes, str]]where kind is an integer with the following values:
| value | description |
|---|---|
| 0 | Websocket closed by client |
| 1 | Bytes message |
| 2 | String message |