Skip to main content
Beta
This functionality is in active development and may undergo changes. While stable, you might encounter occasional issues.

Syft RPC Reference Guide

Syft RPC is a distributed request-response system for asynchronous communication between applications. It provides a clean, future-based App for making remote procedure calls with built-in support for request expiration, caching, and structured data validation. The system facilitates communication between distributed components by serializing requests and responses to files, which serves as the transport mechanism.

Core Components

Message Models

SyftRequest

Represents a client request to a remote endpoint.

class SyftRequest:
id: UUID # Unique identifier for the request
sender: str # Email of the sender
url: SyftBoxURL # Destination URL
method: SyftMethod # HTTP method (GET, POST, etc.)
body: bytes # Request payload
headers: dict # Optional HTTP headers
expires: datetime # When the request expires

The SyftRequest class encapsulates all information needed to make a request. The id uniquely identifies this request, while sender stores the email address of the requester. The url specifies the destination in a format similar to HTTP URLs but using the syft:// protocol. The body contains the serialized request data, and expires indicates when the request should be considered stale.

SyftResponse

Represents the server's response to a request.

class SyftResponse:
id: UUID # Same ID as the request
sender: str # Email of the responder
status_code: int # Status code (200, 400, etc.)
body: bytes # Response payload
headers: dict # Response headers
expires: datetime # When the response expires

The SyftResponse class contains the server's reply to a request. It uses the same id as the original request to maintain the connection between request and response. The status_code follows HTTP conventions (200 for success, 400+ for errors). The body contains the serialized response data that will be deserialized by the client.

Future Models

SyftFuture

Represents a pending request and its eventual response.

class SyftFuture:
id: UUID # Request/response ID
path: Path # Where request/response files are stored
expires: datetime # When the future expires

The SyftFuture class is the core of the asynchronous design. When a client sends a request, it immediately receives a SyftFuture object. This object doesn't contain the response yet, but provides methods to check for and retrieve the response when it becomes available. The path property points to the directory where both request and response files are stored.

Client App

Sending Requests

from syft_rpc import rpc

future = rpc.send(
url="syft://user@domain.com/app_data/app_name/rpc/endpoint",
body=your_request_object, # Automatically serialized
expiry="15m", # Time until request expires (15 minutes)
cache=True, # Enable request caching
)

The rpc.send() function initiates an asynchronous request. The url parameter specifies the destination using Syft's URL format, which includes the user, domain, application name, and endpoint. The body parameter is your request object, which will be automatically serialized to JSON. The expiry parameter sets a time limit for the request's validity, helping manage resources by automatically cleaning up stale requests. The cache parameter enables request deduplication, so identical requests can reuse previous responses.

Handling Responses

try:
# Wait for response with timeout (in seconds)
response = future.wait(timeout=300)

# Check for errors
response.raise_for_status()

# Parse response into your model
result = response.model(YourResponseModel)

# Process the result
print(f"Got response: {result}")
except Exception as e:
print(f"Error: {e}")

The future.wait() method blocks until a response is available or the timeout is reached. It periodically checks for the existence of a response file. The timeout parameter prevents indefinite waiting, raising a SyftTimeoutError if no response arrives within the specified time.

The response.raise_for_status() method checks if the status code indicates an error (400+) and raises a SyftError if so. The response.model() method parses the response body into a specific Pydantic model, providing type safety and validation.

Server App

Creating a Request Handler

from syft_event import SyftEvents
from pydantic import BaseModel

# Initialize event handler for your app
box = SyftEvents("your_app_name")

# Define request/response models
class YourRequest(BaseModel):
field1: str
field2: int

class YourResponse(BaseModel):
result: str

# Register endpoint handler
@box.on_request("/your_endpoint")
def handle_request(request: YourRequest, ctx) -> YourResponse:
# Process the request
# ...
return YourResponse(result="Success!")

# Start the server
if __name__ == "__main__":
box.run_forever()

The server side uses the SyftEvents class to handle incoming requests. The constructor takes an application name that corresponds to the one in the URL. The @box.on_request() decorator registers a function to handle requests to a specific endpoint.

Request handlers receive the deserialized request object and a context object with additional information. The return value is automatically serialized and sent back as the response. Pydantic models are used for both request and response to ensure data validation. The box.run_forever() method starts the server, which continuously monitors for new request files, processes them, and writes response files.

Error Handling

Status Codes:

  • SYFT_200_OK - Request succeeded
  • SYFT_400_BAD_REQUEST - Invalid request
  • SYFT_403_FORBIDDEN - Unauthorized access
  • SYFT_404_NOT_FOUND - Endpoint not found
  • SYFT_419_EXPIRED - Request expired
  • SYFT_500_SERVER_ERROR - Server error

Status codes follow HTTP conventions. The is_success property on SyftStatus provides a convenient way to check if a status code indicates success (200-299) or error (400+).

Complete Example: Ping-Pong

This example demonstrates the complete flow of a simple RPC interaction:

  • Client sends a "ping" message with timestamp
  • Server processes the request
  • Server responds with a "pong" message and new timestamp
  • Client processes the response

Client (ping_request.py)

from datetime import datetime, timezone
from pydantic import BaseModel
from syft_rpc import rpc

# Define request/response models
class PingRequest:
msg: str
ts: datetime = datetime.now(timezone.utc)

class PongResponse(BaseModel):
msg: str
ts: datetime

# Send request
future = rpc.send(
url=f"syft://{server_email}/app_data/pingpong/rpc/ping",
body=PingRequest(msg="hello!"),
expiry="5m",
cache=True,
)

# Wait for and handle response
try:
response = future.wait(timeout=300)
response.raise_for_status()
pong = response.model(PongResponse)
print(f"Got response: {pong.msg} at {pong.ts}")
except Exception as e:
print(f"Error: {e}")

In this client example:

  • The PingRequest class defines the structure of our request, containing a message and a timestamp.
  • The PongResponse class defines the expected structure of the server's response.
  • The rpc.send() call creates and sends the request, returning a future to track the operation.
  • The future.wait() call blocks until a response is received or the timeout (300 seconds) is reached.
  • The response.raise_for_status() call checks for error status codes.
  • The response.model() call parses the raw response body into a typed PongResponse object.

Server (pong_server.py)

from datetime import datetime, timezone
from pydantic import BaseModel, Field
from syft_event import SyftEvents

# Initialize event handler
box = SyftEvents("pingpong")

# Define request/response models
class PingRequest(BaseModel):
msg: str
ts: datetime

class PongResponse(BaseModel):
msg: str
ts: datetime

# Register endpoint handler
@box.on_request("/ping")
def pong(ping: PingRequest, ctx) -> PongResponse:
return PongResponse(
msg=f"Pong from {box.client.email}",
ts=datetime.now(timezone.utc),
)

# Start the server
if __name__ == "__main__":
box.run_forever()

In this server example:

  • The SyftEvents class initializes an event handler for the "pingpong" application.
  • The PingRequest and PongResponse classes define the data structures, similar to the client.
  • The @box.on_request("/ping") decorator registers the pong function to handle requests to the "/ping" endpoint.
  • The handler function receives a typed PingRequest object, processes it (in this case, simply creating a response), and returns a PongResponse.
  • The box.run_forever() call starts the server, which listens for incoming requests indefinitely.

This reference guide covers the core functionality of Syft RPC as demonstrated in the ping-pong example, providing a solid foundation for implementing communication in your applications.