Rivora Contract Protocol (RCP)
RCP (Rivora Contract Protocol) is a lightweight application server interface specification for the Rivora Ecosystem.
RCP defines a common contract between applications, frameworks, and servers while remaining independent of any specific implementation.
RCP is designed around HTTP/3 and QUIC and defines how HTTP requests, application lifecycles, and protocol events are represented through scopes, receive events, and send events.
RCP is an independently specified protocol. Its application interface follows an ASGI-inspired scope/receive/send architecture, but RCP defines its own protocol rules and is optimized for HTTP/3.
Architecture
RCP sits between an application framework and a server.
Application
↓
Framework
↓
RCP
↓
Server
↓
HTTP/3 / QUIC
The protocol separates application logic from the underlying server and transport implementation.
A typical HTTP request flow is:
Client
↓
Server
↓
Receive HTTP/3 request
↓
Validate HTTP/3 fields
↓
Extract HTTP/3 pseudo-headers
↓
Create RCP HTTP Scope
↓
Call RCP Application
↓
Application receives Events
↓
Application sends Events
↓
Server validates response events
↓
Server sends HTTP/3 response
↓
Client
The server is responsible for translating between HTTP/3 and the RCP interface.
Design Goals
RCP is designed around the following goals:
- HTTP/3-first architecture
- QUIC-based transport
- Strong typing
- Minimal application interface
- Framework and server separation
- Streaming request and response bodies
- Application lifespan management
- Explicit connection-disconnect signaling
- HTTP/3 header and pseudo-header handling
- Forward compatibility through HTTP extensions
- Future WebTransport support
Installation
pip install rivora-rcp
Core Concepts
RCP applications use three components:
scopereceivesend
An RCP application has the following interface:
async def app(scope, receive, send):
...
The application is asynchronous.
The server provides the scope and the receive and send callables to the application.
Application Interface
The RCP application contract is:
RCPApplication = Callable[
[Scope, RCPReceiveCallable, RCPSendCallable],
Awaitable[None],
]
A complete application is therefore an awaitable callable receiving:
scope
receive
send
scope
The scope contains metadata describing the HTTP request or lifespan context.
receive
receive is an asynchronous callable used by the application to receive events from the server.
event = await receive()
For HTTP connections, the application can receive request body events and disconnect events.
For lifespan, the application receives startup and shutdown events.
send
send is an asynchronous callable used by the application to send events to the server.
await send(event)
The server is responsible for validating and translating these events into the underlying HTTP/3 or lifecycle operation.
RCP Application Lifecycle
RCP defines the lifecycle between the server and application.
An application invocation is asynchronous and remains under the control of the RCP lifecycle until the application completes or the server signals termination through an appropriate event.
A server must not arbitrarily cancel the application awaitable or task as a mechanism for normal HTTP connection termination.
For an HTTP connection, the server signals termination by sending:
{
"type": HTTPConnectionEventType.DISCONNECT,
}
The application should respond to this event and terminate its processing when appropriate.
The server may use reason to provide additional disconnect information:
{
"type": HTTPConnectionEventType.DISCONNECT,
"reason": "Connection closed",
}
The reason field is optional.
Scopes
A scope contains information known when the HTTP request or application context is created.
HTTP Scope
class HTTPScope(TypedDict):
type: Literal[ScopeType.HTTP]
rcp: RCP
http_version: HTTPVersions
method: RequestMethod
scheme: HTTPScheme
authority: str | None
path: str
raw_path: bytes
query_string: bytes
root_path: str
headers: Headers
client: tuple[str, int] | None
server: tuple[str, int | None] | None
state: NotRequired[dict[str, Any]]
extensions: NotRequired[dict[str, dict[object, object]]]
Fields
| Field | Description |
|---|---|
type |
Scope type. |
rcp |
RCP version information. |
http_version |
HTTP protocol version. |
method |
HTTP request method. |
scheme |
Request scheme, such as http or https. |
authority |
Value extracted from the HTTP/3 :authority pseudo-header. |
path |
Decoded request path. |
raw_path |
Original request path bytes. |
query_string |
Raw query string bytes. |
root_path |
Application mounting root path. |
headers |
Regular HTTP request headers. |
client |
Client address and port, when available. |
server |
Server address and port, when available. |
state |
Request/application state. |
extensions |
Optional protocol extensions. |
HTTP/3 Stream Isolation
HTTP/3 multiplexes multiple independent streams over a single QUIC connection.
Each HTTP request stream is independent of the other HTTP request streams.
RCP therefore creates a separate HTTP scope for each HTTP/3 request stream.
QUIC Connection
│
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
HTTP/3 Stream HTTP/3 Stream HTTP/3 Stream
1 2 3
│ │ │
▼ ▼ ▼
RCP Scope 1 RCP Scope 2 RCP Scope 3
│ │ │
▼ ▼ ▼
Application Application Application
Each stream has its own:
- HTTP scope
- request body events
- response events
- stream lifecycle
- disconnect state
Events belonging to one HTTP/3 stream must not be delivered to another stream.
The termination of one HTTP/3 stream must not terminate unrelated HTTP/3 streams on the same QUIC connection.
This allows multiple HTTP requests to be processed concurrently over the same QUIC connection while keeping their RCP state independent.
Lifespan Scope
class LifespanScope(TypedDict):
type: Literal[ScopeType.LIFESPAN]
rcp: RCP
state: NotRequired[dict[str, Any]]
The lifespan scope is used during application startup and shutdown.
HTTP/3 Pseudo-Headers
HTTP/3 pseudo-headers are not ordinary HTTP headers.
When the server receives HTTP/3 pseudo-headers, it must extract and process them according to the HTTP/3 request or response rules before constructing the RCP scope or response.
Pseudo-headers must not be inserted into the RCP headers field.
For example:
:method → scope["method"]
:scheme → scope["scheme"]
:authority → scope["authority"]
:path → scope["path"] / scope["query_string"]
The server must validate pseudo-header names, placement, duplication, and required fields according to the applicable HTTP/3 rules.
RCP provides constants for HTTP/3 pseudo-header processing:
H3_REQUEST_PSEUDO_HEADERS
H3_RESPONSE_PSEUDO_HEADERS
H3_EXTENSION_PSEUDO_HEADERS
:authority
The :authority pseudo-header must be extracted and placed in:
scope["authority"]
It must not be included in:
scope["headers"]
If an application does not support the authority value in the scope, the server must provide a Host header with the same value as :authority.
HTTP/3 Header Rules
Servers implementing RCP must enforce the HTTP/3 restrictions applicable to the connection.
Lowercase Header Names
HTTP/3 header field names must be represented using lowercase names.
RCP applications should therefore receive lowercase header names:
[
(b"content-type", b"application/json"),
(b"user-agent", b"example"),
]
rather than:
[
(b"Content-Type", b"application/json"),
]
Servers and frameworks implementing RCP are responsible for validating and normalizing header names at the HTTP/3 boundary.
Forbidden HTTP/3 Headers
RCP provides the following set of HTTP/3-forbidden connection-specific headers:
H3_FORBIDDEN_HEADERS = {
b"connection",
b"keep-alive",
b"proxy-connection",
b"transfer-encoding",
b"upgrade",
}
These headers must not be sent as HTTP/3 field lines.
The server must reject or otherwise prevent forbidden HTTP/3 headers from being transmitted.
This applies to headers received from clients and to headers produced by applications.
TE Header
If a TE header is present, its value must be:
trailers
Servers implementing RCP must reject or otherwise handle an incoming TE header whose value is not permitted.
Applications must not produce an invalid TE header.
Response Header Validation
Applications send response headers through response events.
For example:
{
"type": HTTPResponseEventType.START,
"status": 200,
"headers": [
(b"content-type", b"text/plain"),
],
}
The server must validate response headers before sending them.
In particular:
- Header names must be lowercase.
- HTTP/3-forbidden headers must not be transmitted.
- Pseudo-headers must not be supplied through the ordinary
headerscollection. :statusis represented by the RCPstatusfield and must not be supplied as an ordinary header.- Invalid HTTP/3 field representations must be rejected or handled before transmission.
Applications should therefore treat headers as a collection of ordinary HTTP field names and values.
HTTP Request Events
HTTP events are exchanged after the HTTP scope has been created.
HTTPRequestEvent
Sent by the server to the application.
{
"type": HTTPConnectionEventType.REQUEST,
"body": b"...",
"more_body": False,
}
Fields
| Field | Description |
|---|---|
type |
Event type. |
body |
Request body chunk. |
more_body |
Whether additional request body chunks are expected. |
Request bodies may be delivered as multiple events:
http.request
↓
http.request
↓
http.request
↓
more_body = False
Applications should continue receiving events while additional body data is expected.
HTTP Response Events
HTTPResponseStartEvent
Sent by the application to the server.
{
"type": HTTPResponseEventType.START,
"status": 200,
}
With headers:
{
"type": HTTPResponseEventType.START,
"status": 200,
"headers": [
(b"content-type", b"text/plain"),
],
}
Fields
| Field | Description |
|---|---|
type |
Event type. |
status |
HTTP response status code. |
headers |
Optional response headers. |
trailers |
Indicates whether response trailers will be sent. |
The status field represents the HTTP response :status pseudo-header.
Applications must not place :status inside headers.
HTTPResponseBodyEvent
{
"type": HTTPResponseEventType.BODY,
"body": b"Hello",
"more_body": False,
}
Fields
| Field | Description |
|---|---|
type |
Event type. |
body |
Response body chunk. |
more_body |
Whether additional response body chunks are expected. |
Applications may stream responses by sending multiple body events.
HTTPResponseTrailersEvent
{
"type": HTTPResponseEventType.TRAILERS,
"headers": [
(b"content-md5", b"..."),
],
"more_trailers": False,
}
Used to send HTTP trailers after the response body.
Trailer headers are ordinary HTTP field names and must follow the applicable restrictions.
Pseudo-headers must not be sent through the trailer headers collection.
HTTPResponseDebugEvent
{
"type": HTTPResponseEventType.DEBUG,
"info": {},
}
Provides optional debugging information to the server.
Servers may ignore this event.
Debug information is not part of the HTTP response transmitted to the client.
HTTP Disconnect
HTTPDisconnectEvent
{
"type": HTTPConnectionEventType.DISCONNECT,
"reason": "Connection closed",
}
reason is optional:
{
"type": HTTPConnectionEventType.DISCONNECT,
}
Receive
When sent by the server to the application, the event indicates that the HTTP stream has been disconnected.
The application should stop processing the associated HTTP operation when appropriate.
The server uses this event to communicate termination rather than arbitrarily cancelling the application task.
Send
An application may send HTTPDisconnectEvent to request immediate connection termination.
await send(
{
"type": HTTPConnectionEventType.DISCONNECT,
"reason": "Application requested termination",
}
)
When handling this event from an application, the server should terminate the associated HTTP stream according to its implementation and protocol requirements.
Lifespan
RCP provides a lifespan protocol for application startup and shutdown.
The lifespan scope is:
class LifespanScope(TypedDict):
type: Literal[ScopeType.LIFESPAN]
rcp: RCP
state: NotRequired[dict[str, Any]]
The lifespan scope does not contain HTTP stream information.
Lifespan State
When the server starts the lifespan protocol, it must provide an empty state dictionary:
{
"type": ScopeType.LIFESPAN,
"rcp": {
"version": RCPVersions.VERSION_1,
},
"state": {},
}
The application may populate this state during startup.
For example:
scope["state"]["database"] = database
When the server creates a new HTTP scope, it must provide a copy of the lifespan state.
Conceptually:
Lifespan state
│
▼
Application initializes state
│
▼
Shared server-side state
│
├── copy → HTTP Scope 1
├── copy → HTTP Scope 2
├── copy → HTTP Scope 3
└── copy → HTTP Scope 4
The HTTP scope receives a copy rather than the original lifespan state dictionary.
Lifespan Startup
The server sends:
{
"type": LifespanEventType.STARTUP,
}
The application must respond with either:
{
"type": LifespanEventType.STARTUP_COMPLETE,
}
or:
{
"type": LifespanEventType.STARTUP_FAILED,
"message": "Reason",
}
Lifespan Shutdown
The server sends:
{
"type": LifespanEventType.SHUTDOWN,
}
The application responds with either:
{
"type": LifespanEventType.SHUTDOWN_COMPLETE,
}
or:
{
"type": LifespanEventType.SHUTDOWN_FAILED,
"message": "Reason",
}
Complete Application Example
from rcp import HTTPResponseEventType
async def app(scope, receive, send):
await send(
{
"type": HTTPResponseEventType.START,
"status": 200,
"headers": [
(b"content-type", b"text/plain"),
],
}
)
await send(
{
"type": HTTPResponseEventType.BODY,
"body": b"Hello from RCP",
"more_body": False,
}
)
Streaming Example
An application can stream a response using multiple body events:
from rcp import HTTPResponseEventType
async def app(scope, receive, send):
await send(
{
"type": HTTPResponseEventType.START,
"status": 200,
"headers": [
(b"content-type", b"text/plain"),
],
}
)
await send(
{
"type": HTTPResponseEventType.BODY,
"body": b"First chunk\n",
"more_body": True,
}
)
await send(
{
"type": HTTPResponseEventType.BODY,
"body": b"Second chunk\n",
"more_body": False,
}
)
Server Responsibilities
An RCP server is responsible for translating between HTTP/3 and the RCP application interface.
An RCP server must:
- Accept HTTP/3 requests over QUIC.
- Validate HTTP/3 request fields.
- Validate pseudo-headers according to the applicable HTTP/3 request form.
- Extract pseudo-headers before creating the RCP scope.
- Keep pseudo-headers out of
scope["headers"]. - Store
:authorityinscope["authority"]. - Provide a
Hostheader with the same authority value when the application does not support theauthorityscope value. - Ensure all ordinary header names are lowercase.
- Reject or prevent forbidden HTTP/3 headers.
- Validate
TEaccording to HTTP/3 requirements. - Create a separate HTTP scope for each HTTP/3 request stream.
- Keep events and state belonging to each HTTP stream independent from other HTTP streams.
- Invoke the RCP application using the asynchronous application contract.
- Deliver request body data through
http.requestevents. - Deliver stream termination through
http.disconnect. - Validate application response events.
- Translate response events into HTTP/3 operations.
- Prevent invalid pseudo-headers from being transmitted as ordinary headers.
- Manage the lifespan lifecycle when lifespan support is enabled.
- Provide lifespan state to HTTP scopes as a copy.
- Avoid arbitrary application-task cancellation as a normal stream termination mechanism.
Application Responsibilities
An RCP application should:
- Treat the scope as protocol-provided metadata.
- Use
await receive()to receive events. - Use
await send(event)to send events. - Process request body chunks when
more_bodyis true. - Handle
http.disconnectappropriately. - Produce valid RCP response events.
- Use lowercase ordinary response header names.
- Never place HTTP/3 pseudo-headers inside ordinary response headers.
- Avoid HTTP/3-forbidden headers.
- Respect the response lifecycle.
- Respond correctly to lifespan startup and shutdown events.
HTTP/3 Pseudo-Header Representation
RCP separates HTTP/3 pseudo-headers from ordinary HTTP fields.
| HTTP/3 field | RCP representation |
|---|---|
:method |
scope["method"] |
:scheme |
scope["scheme"] |
:authority |
scope["authority"] |
:path |
scope["path"] and scope["query_string"] |
:status |
HTTPResponseStartEvent["status"] |
:protocol |
Processed as an HTTP/3 extension/extended CONNECT field where applicable |
Extensions
HTTP scopes may contain:
extensions: NotRequired[
dict[str, dict[object, object]]
]
Extensions allow additional protocol capabilities to be introduced without changing the base scope structure.
Extensions are optional and must not change the meaning of the required RCP fields.
WebTransport is reserved for a future protocol version.
HTTP Methods
RCP provides the following request methods:
GET
POST
PUT
PATCH
DELETE
HEAD
OPTIONS
TRACE
CONNECT
The method is represented in the HTTP scope through:
scope["method"]
HTTP Schemes
RCP provides:
http
https
through the HTTPScheme type.
scope["scheme"]
contains the request scheme.
Protocol Version
The RCP protocol version is represented by:
class RCPVersions(StrEnum):
VERSION_1 = "1.0"
An RCP scope contains:
{
"version": RCPVersions.VERSION_1,
}
RCP protocol versioning is separate from the Python package version.
For example:
RCP protocol version: 1.0
Python package release: 1.0.4
Version 1.0
The current protocol defines:
- HTTP/3
- QUIC
- Typed scopes
- Typed events
- Asynchronous applications
- Streaming request bodies
- Streaming response bodies
- HTTP/3 pseudo-header processing
- HTTP/3 header restrictions
- Lifespan startup and shutdown
- Lifespan state
- Protocol extensions
Reserved for future protocol versions:
- WebTransport
- HTTP/2
- HTTP/1.1
- Additional protocol extensions
Future protocol versions may define additional transports or capabilities without changing the fundamental separation between application, framework, RCP, and server.
License
RCP is licensed under the MIT License.
See the LICENSE file for details.
Release files for rivora-rcp 1.0.4
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| rivora_rcp-1.0.4.tar.gz | 16.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| rivora_rcp-1.0.4-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 29.1 kB
Release files / rivora_rcp-1.0.4.tar.gz
| Download URL | rivora_rcp-1.0.4.tar.gz |
|---|---|
| Size | 16.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1172f0806e3e2eb4e8dafaae4e54f8837bd229c6313c22d5f234e86514e0d158
|
|
BLAKE2b-256 checksum How to use checksums |
8c9b6601439c8cd4b43bd0f45209be71a808fcbf33fae8820fc2592d06b15f5b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|
Release files / rivora_rcp-1.0.4-py3-none-any.whl
| Download URL | rivora_rcp-1.0.4-py3-none-any.whl |
|---|---|
| Size | 12.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ba70255a7c3033015cd14768b192f0a7f35382555d1754cff63a090502a7d691
|
|
BLAKE2b-256 checksum How to use checksums |
af9b1c0ee931348c492cca35518fb0ac7565c46ae9d1e6a3f1907771cda9559d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|