Getting Started with gRPC¶
Quick Start: In 30 seconds, you'll have a working gRPC service handler.
# user_handler.py
from spikard import GrpcRequest, GrpcResponse
import user_pb2 # Generated from your .proto file
class UserServiceHandler:
async def handle_request(self, request: GrpcRequest) -> GrpcResponse:
if request.method_name == "GetUser":
# Deserialize
req = user_pb2.GetUserRequest()
req.ParseFromString(request.payload)
# Process
user = user_pb2.User(id=req.user_id, name="Alice", email="alice@example.com")
# Serialize and return
return GrpcResponse(payload=user.SerializeToString())
// user_handler.ts
import { GrpcRequest, GrpcResponse } from '@spikard/node';
import { userservice } from './user_service';
export class UserServiceHandler {
async handleRequest(request: GrpcRequest): Promise<GrpcResponse> {
if (request.methodName === 'GetUser') {
// Deserialize
const req = userservice.v1.GetUserRequest.decode(request.payload);
// Process
const user = userservice.v1.User.create({
id: req.userId,
name: 'Alice',
email: 'alice@example.com'
});
// Serialize and return
const payload = userservice.v1.User.encode(user).finish();
return new GrpcResponse({ payload });
}
}
}
# user_handler.rb
require 'spikard'
require_relative 'user_service_pb'
class UserServiceHandler
def handle_request(request)
if request.method_name == 'GetUser'
# Deserialize
req = Userservice::V1::GetUserRequest.decode(request.payload)
# Process
user = Userservice::V1::User.new(
id: req.user_id,
name: 'Alice',
email: 'alice@example.com'
)
# Serialize and return
Spikard::Grpc::Response.new(payload: user.to_proto)
end
end
end
<?php
declare(strict_types=1);
// UserServiceHandler.php
use Spikard\Grpc\Request;
use Spikard\Grpc\Response;
use Userservice\V1\GetUserRequest;
use Userservice\V1\User;
class UserServiceHandler
{
public function handleRequest(Request $request): Response
{
if ($request->methodName === 'GetUser') {
// Deserialize
$req = new GetUserRequest();
$req->mergeFromString($request->payload);
// Process
$user = new User();
$user->setId($req->getUserId());
$user->setName('Alice');
$user->setEmail('alice@example.com');
// Serialize and return
return new Response(payload: $user->serializeToString());
}
}
}
use spikard_http::grpc::{GrpcHandler, GrpcRequestData, GrpcResponseData};
use prost::Message;
use tonic::Status;
mod userservice {
include!("userservice.rs"); // Generated by prost
}
pub struct UserServiceHandler;
impl GrpcHandler for UserServiceHandler {
async fn call(&self, request: GrpcRequestData) -> Result<GrpcResponseData, Status> {
if request.method_name == "GetUser" {
// Deserialize
let req = userservice::GetUserRequest::decode(request.payload)
.map_err(|e| Status::invalid_argument(e.to_string()))?;
// Process
let user = userservice::User {
id: req.user_id,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
..Default::default()
};
// Serialize and return
let mut buf = Vec::new();
user.encode(&mut buf)
.map_err(|e| Status::internal(e.to_string()))?;
Ok(GrpcResponseData::new(buf.into()))
} else {
Err(Status::unimplemented("Method not found"))
}
}
}
That's it! Now let's build a complete gRPC service from scratch.
What is gRPC in Spikard?¶
Spikard's gRPC support lets you write type-safe service handlers in Python, TypeScript, Ruby, PHP, or Rust that integrate with a high-performance Rust runtime. You write handlers in your language of choice, Spikard handles the protocol details.
Architecture¶
+-------------------------------------------------------------+
| Your Handler (Python, TypeScript, Ruby, PHP, Rust) |
| - Implements: handle_request(GrpcRequest) -> GrpcResponse |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| FFI Layer (spikard-py, spikard-node, spikard-rb, etc.) |
| - Binary protobuf payloads, metadata conversion |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| Rust Runtime (spikard-http + Tonic) |
| - HTTP/2, gRPC protocol, routing, status codes |
+-------------------------------------------------------------+
Key insight: Your handler receives raw protobuf bytes and returns raw protobuf bytes. Spikard handles HTTP/2, routing, and status codes.
Prerequisites¶
Required Tools¶
-
protoc (Protocol Buffers compiler):
-
Spikard CLI:
-
Language-specific protobuf runtime:
Step-by-Step Tutorial¶
Step 1: Write a .proto File¶
Create user_service.proto:
syntax = "proto3";
package userservice;
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
}
message GetUserRequest {
int32 id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
string created_at = 4;
}
Proto3 Key Concepts:
- messages: Data structures (like structs/classes)
- fields: Each has a type, name, and unique number
- optional: Field may or may not be present
- repeated: Array/list of values
- service: Defines RPC methods (input -> output)
Step 2: Generate Code¶
Code Generation Commands¶
Generate protobuf code for each language:
Python:
TypeScript:
# Install protobufjs CLI
npm install -g protobufjs-cli
# Generate TypeScript definitions
pbjs -t static-module -w commonjs -o user_service.js user_service.proto
pbts -o user_service.d.ts user_service.js
Ruby:
PHP:
Rust (add to build.rs):
Step 3: Implement a Handler¶
Python gRPC Handler¶
Complete Python handler implementation for UserService with GetUser and CreateUser methods.
from spikard.grpc import GrpcHandler, GrpcRequest, GrpcResponse
import userservice_pb2 # Generated from proto
from datetime import datetime
class UserServiceHandler(GrpcHandler):
"""UserService gRPC handler implementation."""
def __init__(self, user_repository):
"""Initialize handler with dependencies."""
self.user_repository = user_repository
async def handle_request(self, request: GrpcRequest) -> GrpcResponse:
"""
Handle incoming gRPC requests.
Routes to appropriate method based on request.method_name.
"""
if request.method_name == "GetUser":
return await self._get_user(request)
elif request.method_name == "CreateUser":
return await self._create_user(request)
else:
raise NotImplementedError(f"Unknown method: {request.method_name}")
async def _get_user(self, request: GrpcRequest) -> GrpcResponse:
"""Handle GetUser RPC."""
# 1. Deserialize request
req = userservice_pb2.GetUserRequest()
req.ParseFromString(request.payload)
# 2. Validate input
if req.id <= 0:
raise ValueError("User ID must be positive")
# 3. Business logic
user = await self.user_repository.find_by_id(req.id)
if not user:
raise ValueError(f"User {req.id} not found")
# 4. Build response
response_user = userservice_pb2.User()
response_user.id = user.id
response_user.name = user.name
response_user.email = user.email
response_user.created_at = user.created_at.isoformat()
# 5. Serialize and return
return GrpcResponse(
payload=response_user.SerializeToString(),
metadata={"x-user-found": "true"}
)
async def _create_user(self, request: GrpcRequest) -> GrpcResponse:
"""Handle CreateUser RPC."""
# 1. Deserialize request
req = userservice_pb2.CreateUserRequest()
req.ParseFromString(request.payload)
# 2. Validate input
if not req.name or not req.email:
raise ValueError("Name and email are required")
# 3. Check authorization from metadata
auth_token = request.get_metadata("authorization")
if not auth_token:
raise PermissionError("Authentication required")
# 4. Business logic
user = await self.user_repository.create(
name=req.name,
email=req.email
)
# 5. Build response
response_user = userservice_pb2.User()
response_user.id = user.id
response_user.name = user.name
response_user.email = user.email
response_user.created_at = datetime.utcnow().isoformat()
# 6. Serialize with metadata
return GrpcResponse(
payload=response_user.SerializeToString(),
metadata={
"x-user-id": str(user.id),
"x-created": "true"
}
)
Key Patterns¶
- Async/await: All handlers are async for non-blocking I/O
ParseFromString(): Deserializes binary protobuf to Python objectSerializeToString(): Serializes Python object to binary protobuf- Exception mapping:
ValueError->INVALID_ARGUMENT,PermissionError->PERMISSION_DENIED - Metadata access:
request.get_metadata(key)returnsstr | None
Registration¶
from spikard import create_app
from spikard.grpc import GrpcService
# Create app
app = create_app()
# Create service registry
grpc_service = GrpcService()
# Register handler
handler = UserServiceHandler(user_repository=UserRepository())
grpc_service.register_handler("userservice.UserService", handler)
# App is now ready to serve gRPC requests
TypeScript gRPC Handler¶
Complete TypeScript handler implementation for UserService with GetUser and CreateUser methods.
import {
GrpcHandler,
GrpcRequest,
GrpcResponse,
GrpcError,
GrpcStatusCode,
createServiceHandler,
createUnaryHandler,
} from 'spikard';
import * as userservice from './userservice_pb'; // Generated protobufjs types
class UserServiceHandler implements GrpcHandler {
constructor(private userRepository: UserRepository) {}
async handleRequest(request: GrpcRequest): Promise<GrpcResponse> {
/**
* Handle incoming gRPC requests.
* Routes to appropriate method based on request.methodName.
*/
switch (request.methodName) {
case 'GetUser':
return this.getUser(request);
case 'CreateUser':
return this.createUser(request);
default:
throw new GrpcError(
GrpcStatusCode.UNIMPLEMENTED,
`Method ${request.methodName} not implemented`
);
}
}
private async getUser(request: GrpcRequest): Promise<GrpcResponse> {
// 1. Deserialize request
const req = userservice.GetUserRequest.decode(request.payload);
// 2. Validate input
if (req.id <= 0) {
throw new GrpcError(
GrpcStatusCode.INVALID_ARGUMENT,
'User ID must be positive'
);
}
// 3. Business logic
const user = await this.userRepository.findById(req.id);
if (!user) {
throw new GrpcError(
GrpcStatusCode.NOT_FOUND,
`User ${req.id} not found`
);
}
// 4. Build response
const responseUser = userservice.User.create({
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt.toISOString(),
});
// 5. Serialize and return
const encoded = userservice.User.encode(responseUser).finish();
return {
payload: Buffer.from(encoded),
metadata: { 'x-user-found': 'true' },
};
}
private async createUser(request: GrpcRequest): Promise<GrpcResponse> {
// 1. Deserialize request
const req = userservice.CreateUserRequest.decode(request.payload);
// 2. Validate input
if (!req.name || !req.email) {
throw new GrpcError(
GrpcStatusCode.INVALID_ARGUMENT,
'Name and email are required'
);
}
// 3. Check authorization from metadata
const authToken = request.metadata['authorization'];
if (!authToken) {
throw new GrpcError(
GrpcStatusCode.UNAUTHENTICATED,
'Authentication required'
);
}
// 4. Business logic
const user = await this.userRepository.create({
name: req.name,
email: req.email,
});
// 5. Build response
const responseUser = userservice.User.create({
id: user.id,
name: user.name,
email: user.email,
createdAt: new Date().toISOString(),
});
// 6. Serialize with metadata
const encoded = userservice.User.encode(responseUser).finish();
return {
payload: Buffer.from(encoded),
metadata: {
'x-user-id': user.id.toString(),
'x-created': 'true',
},
};
}
}
Key Patterns¶
- Protobufjs: Uses
.decode()and.encode().finish()for serialization - Buffer: gRPC payloads are Node.js
Bufferobjects - GrpcError: Throw with explicit status codes for proper error responses
- Helper functions:
createUnaryHandlerandcreateServiceHandlerreduce boilerplate - Type safety: Full TypeScript type inference for protobuf messages
Registration¶
Ruby gRPC Handler¶
Complete Ruby handler implementation for UserService with GetUser and CreateUser methods.
require 'spikard/grpc'
require 'userservice_pb' # Generated from proto
class UserServiceHandler < Spikard::Grpc::Handler
def initialize(user_repository)
@user_repository = user_repository
end
def handle_request(request)
# Route based on method name
case request.method_name
when 'GetUser'
get_user(request)
when 'CreateUser'
create_user(request)
else
raise "Unknown method: #{request.method_name}"
end
end
private
def get_user(request)
# 1. Deserialize request
req = Userservice::GetUserRequest.decode(request.payload)
# 2. Validate input
raise ArgumentError, 'User ID must be positive' if req.id <= 0
# 3. Business logic
user = @user_repository.find_by_id(req.id)
raise ArgumentError, "User #{req.id} not found" unless user
# 4. Build response
response_user = Userservice::User.new(
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at.iso8601
)
# 5. Serialize and return
response = Spikard::Grpc::Response.new(
payload: Userservice::User.encode(response_user)
)
response.metadata = { 'x-user-found' => 'true' }
response
end
def create_user(request)
# 1. Deserialize request
req = Userservice::CreateUserRequest.decode(request.payload)
# 2. Validate input
if req.name.empty? || req.email.empty?
raise ArgumentError, 'Name and email are required'
end
# 3. Check authorization from metadata
auth_token = request.get_metadata('authorization')
unless auth_token
raise SecurityError, 'Authentication required'
end
# 4. Business logic
user = @user_repository.create(
name: req.name,
email: req.email
)
# 5. Build response
response_user = Userservice::User.new(
id: user.id,
name: user.name,
email: user.email,
created_at: Time.now.utc.iso8601
)
# 6. Serialize with metadata
response = Spikard::Grpc::Response.new(
payload: Userservice::User.encode(response_user)
)
response.metadata = {
'x-user-id' => user.id.to_s,
'x-created' => 'true'
}
response
end
end
Key Patterns¶
- Synchronous: Ruby handlers are synchronous (Rust runtime handles async)
.decode()/.encode(): Ruby protobuf methods for serialization- Metadata access:
request.get_metadata(key)returnsString | nil - Response construction: Create response, then set metadata separately
- Error responses: Use
Response.error()for error cases - Exception mapping: Rescue exceptions and convert to gRPC status codes
Error Handling¶
class UserServiceHandler < Spikard::Grpc::Handler
def handle_request(request)
case request.method_name
when 'GetUser'
get_user(request)
else
# Return error response
Spikard::Grpc::Response.error(
"Method not implemented: #{request.method_name}"
)
end
rescue ArgumentError => e
# Invalid argument error
Spikard::Grpc::Response.error(e.message, 'INVALID_ARGUMENT')
rescue SecurityError => e
# Authentication error
Spikard::Grpc::Response.error(e.message, 'UNAUTHENTICATED')
rescue StandardError => e
# Internal error
Spikard::Grpc::Response.error("Internal error: #{e.message}")
end
end
Registration¶
PHP gRPC Handler¶
Complete PHP handler implementation for UserService with GetUser and CreateUser methods.
<?php
declare(strict_types=1);
use Spikard\Grpc\HandlerInterface;
use Spikard\Grpc\Request;
use Spikard\Grpc\Response;
use Userservice\GetUserRequest;
use Userservice\CreateUserRequest;
use Userservice\User;
class UserServiceHandler implements HandlerInterface
{
public function __construct(
private UserRepository $userRepository,
) {}
public function handleRequest(Request $request): Response
{
// Route based on method name
return match ($request->methodName) {
'GetUser' => $this->getUser($request),
'CreateUser' => $this->createUser($request),
default => Response::error("Unknown method: {$request->methodName}"),
};
}
private function getUser(Request $request): Response
{
try {
// 1. Deserialize request
$req = new GetUserRequest();
$req->mergeFromString($request->payload);
// 2. Validate input
if ($req->getId() <= 0) {
return Response::error('User ID must be positive');
}
// 3. Business logic
$user = $this->userRepository->findById($req->getId());
if (!$user) {
return Response::error("User {$req->getId()} not found");
}
// 4. Build response
$responseUser = new User();
$responseUser->setId($user->getId());
$responseUser->setName($user->getName());
$responseUser->setEmail($user->getEmail());
$responseUser->setCreatedAt($user->getCreatedAt()->format('c'));
// 5. Serialize and return
return new Response(
payload: $responseUser->serializeToString(),
metadata: ['x-user-found' => 'true']
);
} catch (\Exception $e) {
return Response::error("Error: {$e->getMessage()}");
}
}
private function createUser(Request $request): Response
{
try {
// 1. Deserialize request
$req = new CreateUserRequest();
$req->mergeFromString($request->payload);
// 2. Validate input
if (empty($req->getName()) || empty($req->getEmail())) {
return Response::error('Name and email are required');
}
// 3. Check authorization from metadata
$authToken = $request->getMetadata('authorization');
if (!$authToken) {
return Response::error(
'Authentication required',
'UNAUTHENTICATED'
);
}
// 4. Business logic
$user = $this->userRepository->create(
name: $req->getName(),
email: $req->getEmail()
);
// 5. Build response
$responseUser = new User();
$responseUser->setId($user->getId());
$responseUser->setName($user->getName());
$responseUser->setEmail($user->getEmail());
$responseUser->setCreatedAt((new \DateTime())->format('c'));
// 6. Serialize with metadata
return new Response(
payload: $responseUser->serializeToString(),
metadata: [
'x-user-id' => (string)$user->getId(),
'x-created' => 'true',
]
);
} catch (\Exception $e) {
return Response::error("Error: {$e->getMessage()}");
}
}
}
Key Patterns¶
- Synchronous: PHP handlers are synchronous
mergeFromString(): Deserializes binary protobuf (use merge, not parse)serializeToString(): Serializes protobuf to binary- Getters/Setters: PHP protobuf uses getter/setter methods
- Error responses: Return
Response::error()instead of throwing - Named arguments: PHP 8.0+ named arguments for clarity
- Type hints: Leverage PHP type system for safety
Registration¶
<?php
declare(strict_types=1);
use Spikard\Grpc;
// Create service registry
$service = Grpc::createService();
// Register handler
$handler = new UserServiceHandler(
userRepository: new UserRepository()
);
$service->registerHandler('userservice.UserService', $handler);
// Service ready to handle requests
Rust gRPC Handler¶
Complete Rust handler implementation for UserService with GetUser and CreateUser methods.
use bytes::Bytes;
use spikard_http::grpc::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
use tonic::Status;
use std::sync::Arc;
// Generated protobuf types
mod userservice {
include!("userservice.rs"); // Generated by prost
}
pub struct UserServiceHandler {
user_repository: Arc<dyn UserRepository + Send + Sync>,
}
impl UserServiceHandler {
pub fn new(user_repository: Arc<dyn UserRepository + Send + Sync>) -> Self {
Self { user_repository }
}
async fn get_user(&self, request: GrpcRequestData) -> GrpcHandlerResult {
// 1. Deserialize request
use prost::Message;
let req = userservice::GetUserRequest::decode(request.payload)
.map_err(|e| Status::invalid_argument(format!("Invalid request: {}", e)))?;
// 2. Validate input
if req.id <= 0 {
return Err(Status::invalid_argument("User ID must be positive"));
}
// 3. Business logic
let user = self.user_repository.find_by_id(req.id).await
.map_err(|e| Status::internal(format!("Database error: {}", e)))?
.ok_or_else(|| Status::not_found(format!("User {} not found", req.id)))?;
// 4. Build response
let response_user = userservice::User {
id: user.id,
name: user.name.clone(),
email: user.email.clone(),
created_at: user.created_at.to_rfc3339(),
};
// 5. Serialize
let mut buf = Vec::new();
response_user.encode(&mut buf)
.map_err(|e| Status::internal(format!("Encoding error: {}", e)))?;
// 6. Add metadata
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("x-user-found", "true".parse().map_err(|_| Status::internal("failed to parse metadata value"))?);
Ok(GrpcResponseData {
payload: Bytes::from(buf),
metadata,
})
}
async fn create_user(&self, request: GrpcRequestData) -> GrpcHandlerResult {
// 1. Deserialize request
use prost::Message;
let req = userservice::CreateUserRequest::decode(request.payload)
.map_err(|e| Status::invalid_argument(format!("Invalid request: {}", e)))?;
// 2. Validate input
if req.name.is_empty() || req.email.is_empty() {
return Err(Status::invalid_argument("Name and email are required"));
}
// 3. Check authorization from metadata
let auth_token = request.metadata
.get("authorization")
.and_then(|v| v.to_str().ok());
if auth_token.is_none() {
return Err(Status::unauthenticated("Authentication required"));
}
// 4. Business logic
let user = self.user_repository.create(&req.name, &req.email).await
.map_err(|e| Status::internal(format!("Create failed: {}", e)))?;
// 5. Build response
let response_user = userservice::User {
id: user.id,
name: user.name.clone(),
email: user.email.clone(),
created_at: chrono::Utc::now().to_rfc3339(),
};
// 6. Serialize
let mut buf = Vec::new();
response_user.encode(&mut buf)
.map_err(|e| Status::internal(format!("Encoding error: {}", e)))?;
// 7. Add metadata
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("x-user-id", user.id.to_string().parse().map_err(|_| Status::internal("failed to parse metadata value"))?);
metadata.insert("x-created", "true".parse().map_err(|_| Status::internal("failed to parse metadata value"))?);
Ok(GrpcResponseData {
payload: Bytes::from(buf),
metadata,
})
}
}
impl GrpcHandler for UserServiceHandler {
fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
Box::pin(async move {
match request.method_name.as_str() {
"GetUser" => self.get_user(request).await,
"CreateUser" => self.create_user(request).await,
_ => Err(Status::unimplemented(format!("Unknown method: {}", request.method_name))),
}
})
}
fn service_name(&self) -> &'static str {
"userservice.UserService"
}
}
Key Patterns¶
- Async/await: Handlers return
Pin<Box<dyn Future>> - prost: Uses
.decode()and.encode()for protobuf - Error handling: Return
tonic::Statusdirectly - Zero-copy: Uses
Bytesfor efficient payload handling - Type safety: Full compile-time type checking
- Arc: Shared ownership for thread-safe repository access
Registration¶
use spikard_http::grpc::{GrpcRegistry, RpcMode};
use std::sync::Arc;
// Create handler
let user_repository = Arc::new(UserRepositoryImpl::new());
let handler = Arc::new(UserServiceHandler::new(user_repository));
// Register with gRPC runtime
let mut registry = GrpcRegistry::new();
registry.register("userservice.UserService", handler, RpcMode::Unary);
// Runtime ready to serve
Step 4: Register the Handler¶
from spikard import create_app
from spikard.grpc import GrpcService
# Create app
app = create_app()
# Create service registry
grpc_service = GrpcService()
# Register handler
handler = UserServiceHandler(user_repository=UserRepository())
grpc_service.register_handler("userservice.UserService", handler)
# App is now ready to serve gRPC requests
<?php declare(strict_types=1);
use Spikard\Grpc;
// Create service registry
$service = Grpc::createService();
// Register handler
$handler = new UserServiceHandler(
userRepository: new UserRepository()
);
$service->registerHandler('userservice.UserService', $handler);
// Service ready to handle requests
use spikard_http::grpc::{GrpcRegistry, RpcMode};
use std::sync::Arc;
// Create handler
let user_repository = Arc::new(UserRepositoryImpl::new());
let handler = Arc::new(UserServiceHandler::new(user_repository));
// Register with gRPC runtime
let mut registry = GrpcRegistry::new();
registry.register("userservice.UserService", handler, RpcMode::Unary);
// Runtime ready to serve
Step 5: Test the Handler¶
Python gRPC Handler Tests¶
Comprehensive test examples for gRPC handlers using pytest.
# test_user_handler.py
import pytest
from user_handler import UserServiceHandler
from spikard import GrpcRequest
import user_service_pb2 as pb
@pytest.mark.asyncio
async def test_get_user_success():
"""Test getting an existing user."""
handler = UserServiceHandler()
# Create request
req = pb.GetUserRequest(user_id=1)
grpc_request = GrpcRequest(
service_name="userservice.v1.UserService",
method_name="GetUser",
payload=req.SerializeToString(),
)
# Call handler
response = await handler.handle_request(grpc_request)
# Deserialize response
user_response = pb.UserResponse()
user_response.ParseFromString(response.payload)
# Assertions
assert user_response.success is True
assert user_response.user.id == 1
assert user_response.user.name == "Alice"
assert user_response.user.email == "alice@example.com"
@pytest.mark.asyncio
async def test_get_user_not_found():
"""Test getting a non-existent user."""
handler = UserServiceHandler()
# Create request for non-existent user
req = pb.GetUserRequest(user_id=999)
grpc_request = GrpcRequest(
service_name="userservice.v1.UserService",
method_name="GetUser",
payload=req.SerializeToString(),
)
# Call handler
response = await handler.handle_request(grpc_request)
# Deserialize response
user_response = pb.UserResponse()
user_response.ParseFromString(response.payload)
# Assertions
assert user_response.success is False
assert "not found" in user_response.error_message
@pytest.mark.asyncio
async def test_create_user_success():
"""Test creating a new user."""
handler = UserServiceHandler()
# Create request
req = pb.CreateUserRequest(
name="Charlie",
email="charlie@example.com",
phone="555-1234",
tags=["developer", "remote"],
)
grpc_request = GrpcRequest(
service_name="userservice.v1.UserService",
method_name="CreateUser",
payload=req.SerializeToString(),
)
# Call handler
response = await handler.handle_request(grpc_request)
# Deserialize response
user_response = pb.UserResponse()
user_response.ParseFromString(response.payload)
# Assertions
assert user_response.success is True
assert user_response.user.id == 3 # Next available ID
assert user_response.user.name == "Charlie"
assert user_response.user.email == "charlie@example.com"
assert user_response.user.phone == "555-1234"
assert list(user_response.user.tags) == ["developer", "remote"]
@pytest.mark.asyncio
async def test_create_user_validation_error():
"""Test creating a user with missing required fields."""
handler = UserServiceHandler()
# Create request with missing email
req = pb.CreateUserRequest(name="Invalid")
grpc_request = GrpcRequest(
service_name="userservice.v1.UserService",
method_name="CreateUser",
payload=req.SerializeToString(),
)
# Call handler
response = await handler.handle_request(grpc_request)
# Deserialize response
user_response = pb.UserResponse()
user_response.ParseFromString(response.payload)
# Assertions
assert user_response.success is False
assert "required" in user_response.error_message
@pytest.mark.asyncio
async def test_unknown_method():
"""Test calling an unknown method."""
handler = UserServiceHandler()
grpc_request = GrpcRequest(
service_name="userservice.v1.UserService",
method_name="DeleteUser", # Not implemented
payload=b"",
)
# Should raise NotImplementedError
with pytest.raises(NotImplementedError, match="Unknown method"):
await handler.handle_request(grpc_request)
# Run tests
if __name__ == "__main__":
pytest.main([__file__, "-v"])
Test Patterns¶
Using Fixtures¶
import pytest
from user_handler import UserServiceHandler
@pytest.fixture
def handler():
"""Create handler with test data."""
return UserServiceHandler()
@pytest.fixture
def sample_user():
"""Create sample user for tests."""
return pb.User(
id=1,
name="Test User",
email="test@example.com",
)
@pytest.mark.asyncio
async def test_with_fixtures(handler, sample_user):
# Test using fixtures
pass
Testing Error Cases¶
@pytest.mark.asyncio
async def test_handles_malformed_payload():
"""Test handler with malformed protobuf."""
handler = UserServiceHandler()
# Create request with invalid protobuf
grpc_request = GrpcRequest(
service_name="userservice.v1.UserService",
method_name="GetUser",
payload=b"invalid protobuf data",
)
# Should handle deserialization error gracefully
with pytest.raises(Exception): # Or specific protobuf exception
await handler.handle_request(grpc_request)
Running Tests¶
TypeScript gRPC Handler Tests¶
Comprehensive test examples for gRPC handlers using Vitest.
// user_handler.test.ts
import { describe, it, expect } from 'vitest';
import { GrpcRequest } from '@spikard/node';
import { UserServiceHandler } from './user_handler';
import { userservice } from './user_service';
describe('UserServiceHandler', () => {
it('should get an existing user', async () => {
const handler = new UserServiceHandler();
// Create request
const req = userservice.v1.GetUserRequest.create({ userId: 1 });
const payload = userservice.v1.GetUserRequest.encode(req).finish();
const grpcRequest = new GrpcRequest({
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload,
});
// Call handler
const response = await handler.handleRequest(grpcRequest);
// Deserialize response
const userResponse = userservice.v1.UserResponse.decode(response.payload);
// Assertions
expect(userResponse.success).toBe(true);
expect(userResponse.user?.id).toBe(1);
expect(userResponse.user?.name).toBe('Alice');
});
it('should return error for non-existent user', async () => {
const handler = new UserServiceHandler();
const req = userservice.v1.GetUserRequest.create({ userId: 999 });
const payload = userservice.v1.GetUserRequest.encode(req).finish();
const grpcRequest = new GrpcRequest({
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload,
});
const response = await handler.handleRequest(grpcRequest);
const userResponse = userservice.v1.UserResponse.decode(response.payload);
expect(userResponse.success).toBe(false);
expect(userResponse.errorMessage).toContain('not found');
});
it('should create a new user', async () => {
const handler = new UserServiceHandler();
const req = userservice.v1.CreateUserRequest.create({
name: 'Charlie',
email: 'charlie@example.com',
tags: ['developer'],
});
const payload = userservice.v1.CreateUserRequest.encode(req).finish();
const grpcRequest = new GrpcRequest({
serviceName: 'userservice.v1.UserService',
methodName: 'CreateUser',
payload,
});
const response = await handler.handleRequest(grpcRequest);
const userResponse = userservice.v1.UserResponse.decode(response.payload);
expect(userResponse.success).toBe(true);
expect(userResponse.user?.name).toBe('Charlie');
expect(userResponse.user?.id).toBe(3);
});
it('should validate required fields on create', async () => {
const handler = new UserServiceHandler();
const req = userservice.v1.CreateUserRequest.create({
name: 'Test User',
email: '', // Missing email
});
const payload = userservice.v1.CreateUserRequest.encode(req).finish();
const grpcRequest = new GrpcRequest({
serviceName: 'userservice.v1.UserService',
methodName: 'CreateUser',
payload,
});
const response = await handler.handleRequest(grpcRequest);
const userResponse = userservice.v1.UserResponse.decode(response.payload);
expect(userResponse.success).toBe(false);
expect(userResponse.errorMessage).toContain('required');
});
it('should throw for unknown methods', async () => {
const handler = new UserServiceHandler();
const grpcRequest = new GrpcRequest({
serviceName: 'userservice.v1.UserService',
methodName: 'DeleteUser', // Not implemented
payload: new Uint8Array(),
});
await expect(handler.handleRequest(grpcRequest)).rejects.toThrow(
'Unknown method'
);
});
});
Test Patterns¶
Using Test Helpers¶
// test-helpers.ts
import { GrpcRequest } from '@spikard/node';
import { userservice } from './user_service';
export function createGetUserRequest(userId: number): GrpcRequest {
const req = userservice.v1.GetUserRequest.create({ userId });
const payload = userservice.v1.GetUserRequest.encode(req).finish();
return new GrpcRequest({
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload,
});
}
export function createCreateUserRequest(
name: string,
email: string,
tags?: string[]
): GrpcRequest {
const req = userservice.v1.CreateUserRequest.create({
name,
email,
tags: tags || [],
});
const payload = userservice.v1.CreateUserRequest.encode(req).finish();
return new GrpcRequest({
serviceName: 'userservice.v1.UserService',
methodName: 'CreateUser',
payload,
});
}
Testing with Metadata¶
it('should handle authorization header', async () => {
const handler = new UserServiceHandler();
const req = userservice.v1.CreateUserRequest.create({
name: 'Test',
email: 'test@example.com',
});
const payload = userservice.v1.CreateUserRequest.encode(req).finish();
const grpcRequest = new GrpcRequest({
serviceName: 'userservice.v1.UserService',
methodName: 'CreateUser',
payload,
metadata: {
authorization: 'Bearer valid-token',
},
});
const response = await handler.handleRequest(grpcRequest);
const userResponse = userservice.v1.UserResponse.decode(response.payload);
expect(userResponse.success).toBe(true);
});
Running Tests¶
Ruby gRPC Handler Tests¶
Comprehensive test examples for gRPC handlers using RSpec.
# spec/user_service_handler_spec.rb
require 'spec_helper'
require 'spikard/grpc'
require 'userservice_pb'
require_relative '../lib/user_service_handler'
RSpec.describe UserServiceHandler do
let(:user_repository) { instance_double('UserRepository') }
let(:handler) { described_class.new(user_repository) }
describe '#handle_request' do
context 'GetUser' do
it 'returns an existing user successfully' do
# Setup mock data
mock_user = OpenStruct.new(
id: 1,
name: 'Alice',
email: 'alice@example.com',
created_at: Time.now.utc
)
allow(user_repository).to receive(:find_by_id).with(1).and_return(mock_user)
# Create request
req = Userservice::GetUserRequest.new(id: 1)
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'GetUser',
payload: Userservice::GetUserRequest.encode(req)
)
# Call handler
response = handler.handle_request(grpc_request)
# Deserialize response
user_response = Userservice::User.decode(response.payload)
# Assertions
expect(user_response.id).to eq(1)
expect(user_response.name).to eq('Alice')
expect(user_response.email).to eq('alice@example.com')
expect(response.metadata['x-user-found']).to eq('true')
end
it 'returns error for non-existent user' do
allow(user_repository).to receive(:find_by_id).with(999).and_return(nil)
# Create request for non-existent user
req = Userservice::GetUserRequest.new(id: 999)
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'GetUser',
payload: Userservice::GetUserRequest.encode(req)
)
# Call handler - should raise error
expect { handler.handle_request(grpc_request) }.to raise_error(
ArgumentError, /not found/
)
end
it 'validates user ID is positive' do
req = Userservice::GetUserRequest.new(id: 0)
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'GetUser',
payload: Userservice::GetUserRequest.encode(req)
)
expect { handler.handle_request(grpc_request) }.to raise_error(
ArgumentError, /must be positive/
)
end
end
context 'CreateUser' do
it 'creates a new user successfully' do
mock_user = OpenStruct.new(
id: 3,
name: 'Charlie',
email: 'charlie@example.com'
)
allow(user_repository).to receive(:create).and_return(mock_user)
# Create request with authorization metadata
req = Userservice::CreateUserRequest.new(
name: 'Charlie',
email: 'charlie@example.com'
)
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'CreateUser',
payload: Userservice::CreateUserRequest.encode(req),
metadata: { 'authorization' => 'Bearer valid-token' }
)
# Call handler
response = handler.handle_request(grpc_request)
# Deserialize response
user_response = Userservice::User.decode(response.payload)
# Assertions
expect(user_response.id).to eq(3)
expect(user_response.name).to eq('Charlie')
expect(user_response.email).to eq('charlie@example.com')
expect(response.metadata['x-user-id']).to eq('3')
expect(response.metadata['x-created']).to eq('true')
end
it 'returns error when name is missing' do
req = Userservice::CreateUserRequest.new(
name: '',
email: 'test@example.com'
)
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'CreateUser',
payload: Userservice::CreateUserRequest.encode(req),
metadata: { 'authorization' => 'Bearer token' }
)
expect { handler.handle_request(grpc_request) }.to raise_error(
ArgumentError, /required/
)
end
it 'returns error when authorization is missing' do
req = Userservice::CreateUserRequest.new(
name: 'Test',
email: 'test@example.com'
)
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'CreateUser',
payload: Userservice::CreateUserRequest.encode(req)
)
expect { handler.handle_request(grpc_request) }.to raise_error(
SecurityError, /Authentication required/
)
end
end
context 'unknown method' do
it 'raises error for unknown method' do
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'DeleteUser',
payload: ''
)
expect { handler.handle_request(grpc_request) }.to raise_error(
RuntimeError, /Unknown method/
)
end
end
end
end
Test Patterns¶
Using Shared Examples¶
RSpec.shared_examples 'authenticated endpoint' do |method_name|
it 'requires authentication' do
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: method_name,
payload: request_payload
)
expect { handler.handle_request(grpc_request) }.to raise_error(
SecurityError, /Authentication required/
)
end
end
RSpec.describe UserServiceHandler do
describe 'CreateUser' do
let(:request_payload) do
req = Userservice::CreateUserRequest.new(name: 'Test', email: 'test@example.com')
Userservice::CreateUserRequest.encode(req)
end
include_examples 'authenticated endpoint', 'CreateUser'
end
end
Testing Error Responses¶
RSpec.describe UserServiceHandler do
describe 'error handling' do
it 'handles malformed protobuf gracefully' do
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'GetUser',
payload: 'invalid protobuf data'
)
expect { handler.handle_request(grpc_request) }.to raise_error(
Google::Protobuf::ParseError
)
end
end
end
Using let! for Setup¶
RSpec.describe UserServiceHandler do
let!(:alice) do
OpenStruct.new(id: 1, name: 'Alice', email: 'alice@example.com', created_at: Time.now)
end
let!(:bob) do
OpenStruct.new(id: 2, name: 'Bob', email: 'bob@example.com', created_at: Time.now)
end
before do
allow(user_repository).to receive(:find_by_id).with(1).and_return(alice)
allow(user_repository).to receive(:find_by_id).with(2).and_return(bob)
end
it 'retrieves different users' do
# Test with alice
req = Userservice::GetUserRequest.new(id: 1)
grpc_request = Spikard::Grpc::Request.new(
service_name: 'userservice.v1.UserService',
method_name: 'GetUser',
payload: Userservice::GetUserRequest.encode(req)
)
response = handler.handle_request(grpc_request)
user = Userservice::User.decode(response.payload)
expect(user.name).to eq('Alice')
end
end
Running Tests¶
PHP gRPC Handler Tests¶
Comprehensive test examples for gRPC handlers using PHPUnit.
<?php
// tests/UserServiceHandlerTest.php
declare(strict_types=1);
namespace Tests;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Spikard\Grpc\Request;
use Spikard\Grpc\Response;
use Userservice\GetUserRequest;
use Userservice\CreateUserRequest;
use Userservice\User;
class UserServiceHandlerTest extends TestCase
{
private UserServiceHandler $handler;
private MockObject $userRepository;
protected function setUp(): void
{
$this->userRepository = $this->createMock(UserRepository::class);
$this->handler = new UserServiceHandler($this->userRepository);
}
public function testGetUserSuccess(): void
{
// Setup mock data
$mockUser = new \stdClass();
$mockUser->id = 1;
$mockUser->name = 'Alice';
$mockUser->email = 'alice@example.com';
$mockUser->createdAt = new \DateTime();
$this->userRepository
->expects($this->once())
->method('findById')
->with(1)
->willReturn($mockUser);
// Create request
$req = new GetUserRequest();
$req->setId(1);
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload: $req->serializeToString()
);
// Call handler
$response = $this->handler->handleRequest($grpcRequest);
// Deserialize response
$userResponse = new User();
$userResponse->mergeFromString($response->payload);
// Assertions
$this->assertEquals(1, $userResponse->getId());
$this->assertEquals('Alice', $userResponse->getName());
$this->assertEquals('alice@example.com', $userResponse->getEmail());
$this->assertEquals('true', $response->getMetadata('x-user-found'));
}
public function testGetUserNotFound(): void
{
$this->userRepository
->expects($this->once())
->method('findById')
->with(999)
->willReturn(null);
// Create request for non-existent user
$req = new GetUserRequest();
$req->setId(999);
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload: $req->serializeToString()
);
// Call handler
$response = $this->handler->handleRequest($grpcRequest);
// Should return error response
$this->assertTrue($response->isError());
$this->assertStringContainsString('not found', $response->errorMessage);
}
public function testGetUserInvalidId(): void
{
$req = new GetUserRequest();
$req->setId(0);
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload: $req->serializeToString()
);
$response = $this->handler->handleRequest($grpcRequest);
$this->assertTrue($response->isError());
$this->assertStringContainsString('must be positive', $response->errorMessage);
}
public function testCreateUserSuccess(): void
{
$mockUser = new \stdClass();
$mockUser->id = 3;
$mockUser->name = 'Charlie';
$mockUser->email = 'charlie@example.com';
$this->userRepository
->expects($this->once())
->method('create')
->willReturn($mockUser);
// Create request
$req = new CreateUserRequest();
$req->setName('Charlie');
$req->setEmail('charlie@example.com');
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'CreateUser',
payload: $req->serializeToString(),
metadata: ['authorization' => 'Bearer valid-token']
);
// Call handler
$response = $this->handler->handleRequest($grpcRequest);
// Deserialize response
$userResponse = new User();
$userResponse->mergeFromString($response->payload);
// Assertions
$this->assertEquals(3, $userResponse->getId());
$this->assertEquals('Charlie', $userResponse->getName());
$this->assertEquals('charlie@example.com', $userResponse->getEmail());
$this->assertEquals('3', $response->getMetadata('x-user-id'));
$this->assertEquals('true', $response->getMetadata('x-created'));
}
public function testCreateUserValidationError(): void
{
// Create request with missing email
$req = new CreateUserRequest();
$req->setName('Test User');
$req->setEmail('');
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'CreateUser',
payload: $req->serializeToString(),
metadata: ['authorization' => 'Bearer token']
);
$response = $this->handler->handleRequest($grpcRequest);
$this->assertTrue($response->isError());
$this->assertStringContainsString('required', $response->errorMessage);
}
public function testCreateUserRequiresAuthentication(): void
{
$req = new CreateUserRequest();
$req->setName('Test');
$req->setEmail('test@example.com');
// Request without authorization header
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'CreateUser',
payload: $req->serializeToString()
);
$response = $this->handler->handleRequest($grpcRequest);
$this->assertTrue($response->isError());
$this->assertStringContainsString('Authentication required', $response->errorMessage);
$this->assertEquals('16', $response->getMetadata('grpc-status'));
}
public function testUnknownMethod(): void
{
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'DeleteUser',
payload: ''
);
$response = $this->handler->handleRequest($grpcRequest);
$this->assertTrue($response->isError());
$this->assertStringContainsString('Unknown method', $response->errorMessage);
}
}
Test Patterns¶
Using Data Providers¶
<?php
class UserServiceHandlerTest extends TestCase
{
/**
* @dataProvider invalidUserIdProvider
*/
public function testGetUserWithInvalidIds(int $invalidId, string $expectedError): void
{
$req = new GetUserRequest();
$req->setId($invalidId);
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload: $req->serializeToString()
);
$response = $this->handler->handleRequest($grpcRequest);
$this->assertTrue($response->isError());
$this->assertStringContainsString($expectedError, $response->errorMessage);
}
public static function invalidUserIdProvider(): array
{
return [
'zero id' => [0, 'must be positive'],
'negative id' => [-1, 'must be positive'],
];
}
}
Testing Error Handling¶
<?php
class UserServiceHandlerTest extends TestCase
{
public function testHandlesMalformedPayload(): void
{
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload: 'invalid protobuf data'
);
$response = $this->handler->handleRequest($grpcRequest);
// Should return error, not throw exception
$this->assertTrue($response->isError());
$this->assertStringContainsString('Error', $response->errorMessage);
}
public function testHandlesRepositoryException(): void
{
$this->userRepository
->method('findById')
->willThrowException(new \RuntimeException('Database connection failed'));
$req = new GetUserRequest();
$req->setId(1);
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload: $req->serializeToString()
);
$response = $this->handler->handleRequest($grpcRequest);
$this->assertTrue($response->isError());
$this->assertStringContainsString('Database connection failed', $response->errorMessage);
}
}
Testing with Metadata¶
<?php
class UserServiceHandlerTest extends TestCase
{
public function testHandlerReadsCustomMetadata(): void
{
$mockUser = new \stdClass();
$mockUser->id = 1;
$mockUser->name = 'Alice';
$mockUser->email = 'alice@example.com';
$mockUser->createdAt = new \DateTime();
$this->userRepository->method('findById')->willReturn($mockUser);
$req = new GetUserRequest();
$req->setId(1);
$grpcRequest = new Request(
serviceName: 'userservice.v1.UserService',
methodName: 'GetUser',
payload: $req->serializeToString(),
metadata: [
'x-request-id' => 'abc-123',
'x-trace-id' => 'trace-456',
]
);
$response = $this->handler->handleRequest($grpcRequest);
$this->assertFalse($response->isError());
}
}
Running Tests¶
# Run all tests
./vendor/bin/phpunit
# Run with verbose output
./vendor/bin/phpunit --verbose
# Run specific test file
./vendor/bin/phpunit tests/UserServiceHandlerTest.php
# Run specific test method
./vendor/bin/phpunit --filter testGetUserSuccess
# Run with coverage
./vendor/bin/phpunit --coverage-html coverage/
Rust gRPC Handler Tests¶
Comprehensive test examples for gRPC handlers using Tokio test.
// tests/user_handler_test.rs
use bytes::Bytes;
use spikard_http::grpc::{GrpcHandler, GrpcRequestData, GrpcResponseData};
use std::sync::Arc;
use tonic::metadata::MetadataMap;
mod userservice {
include!("../src/userservice.rs");
}
use crate::user_handler::UserServiceHandler;
// Mock repository for testing
struct MockUserRepository {
users: std::sync::RwLock<Vec<userservice::User>>,
}
impl MockUserRepository {
fn new() -> Self {
let users = vec![
userservice::User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
created_at: "2024-01-01T00:00:00Z".to_string(),
},
userservice::User {
id: 2,
name: "Bob".to_string(),
email: "bob@example.com".to_string(),
created_at: "2024-01-02T00:00:00Z".to_string(),
},
];
Self {
users: std::sync::RwLock::new(users),
}
}
}
#[async_trait::async_trait]
impl UserRepository for MockUserRepository {
async fn find_by_id(&self, id: i32) -> Result<Option<userservice::User>, String> {
let users = self.users.read().expect("lock poisoned");
Ok(users.iter().find(|u| u.id == id).cloned())
}
async fn create(&self, name: &str, email: &str) -> Result<userservice::User, String> {
let mut users = self.users.write().expect("lock poisoned");
let new_id = users.len() as i32 + 1;
let user = userservice::User {
id: new_id,
name: name.to_string(),
email: email.to_string(),
created_at: chrono::Utc::now().to_rfc3339(),
};
users.push(user.clone());
Ok(user)
}
}
fn create_handler() -> UserServiceHandler {
let repo = Arc::new(MockUserRepository::new());
UserServiceHandler::new(repo)
}
#[tokio::test]
async fn test_get_user_success() {
use prost::Message;
let handler = create_handler();
// Create request
let req = userservice::GetUserRequest { id: 1 };
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "GetUser".to_string(),
payload: Bytes::from(buf),
metadata: MetadataMap::new(),
};
// Call handler
let response = handler.call(grpc_request).await.expect("handler call failed");
// Deserialize response
let user_response = userservice::User::decode(response.payload).expect("failed to decode response payload");
// Assertions
assert_eq!(user_response.id, 1);
assert_eq!(user_response.name, "Alice");
assert_eq!(user_response.email, "alice@example.com");
assert_eq!(
response.metadata.get("x-user-found").expect("x-user-found header missing").to_str().expect("invalid metadata value"),
"true"
);
}
#[tokio::test]
async fn test_get_user_not_found() {
use prost::Message;
let handler = create_handler();
// Create request for non-existent user
let req = userservice::GetUserRequest { id: 999 };
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "GetUser".to_string(),
payload: Bytes::from(buf),
metadata: MetadataMap::new(),
};
// Call handler - should return error
let result = handler.call(grpc_request).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), tonic::Code::NotFound);
assert!(status.message().contains("not found"));
}
#[tokio::test]
async fn test_get_user_invalid_id() {
use prost::Message;
let handler = create_handler();
// Create request with invalid ID
let req = userservice::GetUserRequest { id: 0 };
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "GetUser".to_string(),
payload: Bytes::from(buf),
metadata: MetadataMap::new(),
};
let result = handler.call(grpc_request).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert!(status.message().contains("must be positive"));
}
#[tokio::test]
async fn test_create_user_success() {
use prost::Message;
let handler = create_handler();
// Create request
let req = userservice::CreateUserRequest {
name: "Charlie".to_string(),
email: "charlie@example.com".to_string(),
};
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
// Add authorization metadata
let mut metadata = MetadataMap::new();
metadata.insert("authorization", "Bearer valid-token".parse().expect("failed to parse authorization header"));
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "CreateUser".to_string(),
payload: Bytes::from(buf),
metadata,
};
// Call handler
let response = handler.call(grpc_request).await.expect("handler call failed");
// Deserialize response
let user_response = userservice::User::decode(response.payload).expect("failed to decode response payload");
// Assertions
assert_eq!(user_response.id, 3); // Next available ID
assert_eq!(user_response.name, "Charlie");
assert_eq!(user_response.email, "charlie@example.com");
assert_eq!(
response.metadata.get("x-user-id").expect("x-user-id header missing").to_str().expect("invalid metadata value"),
"3"
);
assert_eq!(
response.metadata.get("x-created").expect("x-created header missing").to_str().expect("invalid metadata value"),
"true"
);
}
#[tokio::test]
async fn test_create_user_validation_error() {
use prost::Message;
let handler = create_handler();
// Create request with missing email
let req = userservice::CreateUserRequest {
name: "Test User".to_string(),
email: "".to_string(),
};
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
let mut metadata = MetadataMap::new();
metadata.insert("authorization", "Bearer token".parse().expect("failed to parse authorization header"));
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "CreateUser".to_string(),
payload: Bytes::from(buf),
metadata,
};
let result = handler.call(grpc_request).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert!(status.message().contains("required"));
}
#[tokio::test]
async fn test_create_user_requires_authentication() {
use prost::Message;
let handler = create_handler();
let req = userservice::CreateUserRequest {
name: "Test".to_string(),
email: "test@example.com".to_string(),
};
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
// Request without authorization header
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "CreateUser".to_string(),
payload: Bytes::from(buf),
metadata: MetadataMap::new(),
};
let result = handler.call(grpc_request).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), tonic::Code::Unauthenticated);
assert!(status.message().contains("Authentication required"));
}
#[tokio::test]
async fn test_unknown_method() {
let handler = create_handler();
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "DeleteUser".to_string(),
payload: Bytes::new(),
metadata: MetadataMap::new(),
};
let result = handler.call(grpc_request).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), tonic::Code::Unimplemented);
assert!(status.message().contains("Unknown method"));
}
Test Patterns¶
Using Test Fixtures¶
use once_cell::sync::Lazy;
static TEST_HANDLER: Lazy<UserServiceHandler> = Lazy::new(|| {
let repo = Arc::new(MockUserRepository::new());
UserServiceHandler::new(repo)
});
#[tokio::test]
async fn test_with_shared_handler() {
use prost::Message;
let req = userservice::GetUserRequest { id: 1 };
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "GetUser".to_string(),
payload: Bytes::from(buf),
metadata: MetadataMap::new(),
};
let response = TEST_HANDLER.call(grpc_request).await.expect("handler call failed");
let user = userservice::User::decode(response.payload).expect("failed to decode response payload");
assert_eq!(user.name, "Alice");
}
Testing Error Cases¶
#[tokio::test]
async fn test_handles_malformed_payload() {
let handler = create_handler();
let grpc_request = GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "GetUser".to_string(),
payload: Bytes::from("invalid protobuf data"),
metadata: MetadataMap::new(),
};
let result = handler.call(grpc_request).await;
assert!(result.is_err());
let status = result.unwrap_err();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
}
Helper Functions¶
fn create_get_user_request(user_id: i32) -> GrpcRequestData {
use prost::Message;
let req = userservice::GetUserRequest { id: user_id };
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "GetUser".to_string(),
payload: Bytes::from(buf),
metadata: MetadataMap::new(),
}
}
fn create_create_user_request(name: &str, email: &str, auth_token: Option<&str>) -> GrpcRequestData {
use prost::Message;
let req = userservice::CreateUserRequest {
name: name.to_string(),
email: email.to_string(),
};
let mut buf = Vec::new();
req.encode(&mut buf).expect("failed to encode protobuf request");
let mut metadata = MetadataMap::new();
if let Some(token) = auth_token {
metadata.insert("authorization", token.parse().expect("failed to parse authorization header"));
}
GrpcRequestData {
service_name: "userservice.v1.UserService".to_string(),
method_name: "CreateUser".to_string(),
payload: Bytes::from(buf),
metadata,
}
}
#[tokio::test]
async fn test_with_helpers() {
let handler = create_handler();
let request = create_get_user_request(1);
let response = handler.call(request).await.expect("handler call failed");
let user = userservice::User::decode(response.payload).expect("failed to decode response payload");
assert_eq!(user.id, 1);
}
Running Tests¶
Complete Handler Examples¶
Full-featured handler implementations showing routing, validation, business logic, and metadata:
from spikard.grpc import GrpcHandler, GrpcRequest, GrpcResponse
import userservice_pb2 # Generated from proto
from datetime import datetime
class UserServiceHandler(GrpcHandler):
"""UserService gRPC handler implementation."""
def __init__(self, user_repository):
"""Initialize handler with dependencies."""
self.user_repository = user_repository
async def handle_request(self, request: GrpcRequest) -> GrpcResponse:
"""
Handle incoming gRPC requests.
Routes to appropriate method based on request.method_name.
"""
if request.method_name == "GetUser":
return await self._get_user(request)
elif request.method_name == "CreateUser":
return await self._create_user(request)
else:
raise NotImplementedError(f"Unknown method: {request.method_name}")
async def _get_user(self, request: GrpcRequest) -> GrpcResponse:
"""Handle GetUser RPC."""
# 1. Deserialize request
req = userservice_pb2.GetUserRequest()
req.ParseFromString(request.payload)
# 2. Validate input
if req.id <= 0:
raise ValueError("User ID must be positive")
# 3. Business logic
user = await self.user_repository.find_by_id(req.id)
if not user:
raise ValueError(f"User {req.id} not found")
# 4. Build response
response_user = userservice_pb2.User()
response_user.id = user.id
response_user.name = user.name
response_user.email = user.email
response_user.created_at = user.created_at.isoformat()
# 5. Serialize and return
return GrpcResponse(
payload=response_user.SerializeToString(),
metadata={"x-user-found": "true"}
)
async def _create_user(self, request: GrpcRequest) -> GrpcResponse:
"""Handle CreateUser RPC."""
# 1. Deserialize request
req = userservice_pb2.CreateUserRequest()
req.ParseFromString(request.payload)
# 2. Validate input
if not req.name or not req.email:
raise ValueError("Name and email are required")
# 3. Check authorization from metadata
auth_token = request.get_metadata("authorization")
if not auth_token:
raise PermissionError("Authentication required")
# 4. Business logic
user = await self.user_repository.create(
name=req.name,
email=req.email
)
# 5. Build response
response_user = userservice_pb2.User()
response_user.id = user.id
response_user.name = user.name
response_user.email = user.email
response_user.created_at = datetime.utcnow().isoformat()
# 6. Serialize with metadata
return GrpcResponse(
payload=response_user.SerializeToString(),
metadata={
"x-user-id": str(user.id),
"x-created": "true"
}
)
Class-Based Handler¶
import {
GrpcHandler,
GrpcRequest,
GrpcResponse,
GrpcError,
GrpcStatusCode,
createServiceHandler,
createUnaryHandler,
} from 'spikard';
import * as userservice from './userservice_pb'; // Generated protobufjs types
class UserServiceHandler implements GrpcHandler {
constructor(private userRepository: UserRepository) {}
async handleRequest(request: GrpcRequest): Promise<GrpcResponse> {
/**
* Handle incoming gRPC requests.
* Routes to appropriate method based on request.methodName.
*/
switch (request.methodName) {
case 'GetUser':
return this.getUser(request);
case 'CreateUser':
return this.createUser(request);
default:
throw new GrpcError(
GrpcStatusCode.UNIMPLEMENTED,
`Method ${request.methodName} not implemented`
);
}
}
private async getUser(request: GrpcRequest): Promise<GrpcResponse> {
// 1. Deserialize request
const req = userservice.GetUserRequest.decode(request.payload);
// 2. Validate input
if (req.id <= 0) {
throw new GrpcError(
GrpcStatusCode.INVALID_ARGUMENT,
'User ID must be positive'
);
}
// 3. Business logic
const user = await this.userRepository.findById(req.id);
if (!user) {
throw new GrpcError(
GrpcStatusCode.NOT_FOUND,
`User ${req.id} not found`
);
}
// 4. Build response
const responseUser = userservice.User.create({
id: user.id,
name: user.name,
email: user.email,
createdAt: user.createdAt.toISOString(),
});
// 5. Serialize and return
const encoded = userservice.User.encode(responseUser).finish();
return {
payload: Buffer.from(encoded),
metadata: { 'x-user-found': 'true' },
};
}
private async createUser(request: GrpcRequest): Promise<GrpcResponse> {
// 1. Deserialize request
const req = userservice.CreateUserRequest.decode(request.payload);
// 2. Validate input
if (!req.name || !req.email) {
throw new GrpcError(
GrpcStatusCode.INVALID_ARGUMENT,
'Name and email are required'
);
}
// 3. Check authorization from metadata
const authToken = request.metadata['authorization'];
if (!authToken) {
throw new GrpcError(
GrpcStatusCode.UNAUTHENTICATED,
'Authentication required'
);
}
// 4. Business logic
const user = await this.userRepository.create({
name: req.name,
email: req.email,
});
// 5. Build response
const responseUser = userservice.User.create({
id: user.id,
name: user.name,
email: user.email,
createdAt: new Date().toISOString(),
});
// 6. Serialize with metadata
const encoded = userservice.User.encode(responseUser).finish();
return {
payload: Buffer.from(encoded),
metadata: {
'x-user-id': user.id.toString(),
'x-created': 'true',
},
};
}
}
Helper Function Pattern (Alternative)¶
For simpler handlers, use the factory helper pattern:
// Create handler using helper function
const userServiceHandler = createServiceHandler({
GetUser: createUnaryHandler<GetUserRequest, User>(
'GetUser',
async (req, metadata) => {
// Validate
if (req.id <= 0) {
throw new GrpcError(GrpcStatusCode.INVALID_ARGUMENT, 'Invalid ID');
}
// Business logic
const user = await userRepository.findById(req.id);
if (!user) {
throw new GrpcError(GrpcStatusCode.NOT_FOUND, 'User not found');
}
// Return with metadata
return {
response: user,
metadata: { 'x-user-found': 'true' },
};
},
userservice.GetUserRequest,
userservice.User
),
CreateUser: createUnaryHandler<CreateUserRequest, User>(
'CreateUser',
async (req, metadata) => {
// Validate
if (!req.name || !req.email) {
throw new GrpcError(GrpcStatusCode.INVALID_ARGUMENT, 'Missing fields');
}
// Check auth
if (!metadata['authorization']) {
throw new GrpcError(GrpcStatusCode.UNAUTHENTICATED, 'Auth required');
}
// Business logic
const user = await userRepository.create(req);
return {
response: user,
metadata: { 'x-created': 'true' },
};
},
userservice.CreateUserRequest,
userservice.User
),
});
When to use each: - Class-based: Complex services with shared state, dependency injection, multiple methods - Helper functions: Simple services, functional style, less boilerplate
require 'spikard/grpc'
require 'userservice_pb' # Generated from proto
class UserServiceHandler < Spikard::Grpc::Handler
def initialize(user_repository)
@user_repository = user_repository
end
def handle_request(request)
# Route based on method name
case request.method_name
when 'GetUser'
get_user(request)
when 'CreateUser'
create_user(request)
else
raise "Unknown method: #{request.method_name}"
end
end
private
def get_user(request)
# 1. Deserialize request
req = Userservice::GetUserRequest.decode(request.payload)
# 2. Validate input
raise ArgumentError, 'User ID must be positive' if req.id <= 0
# 3. Business logic
user = @user_repository.find_by_id(req.id)
raise ArgumentError, "User #{req.id} not found" unless user
# 4. Build response
response_user = Userservice::User.new(
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at.iso8601
)
# 5. Serialize and return
response = Spikard::Grpc::Response.new(
payload: Userservice::User.encode(response_user)
)
response.metadata = { 'x-user-found' => 'true' }
response
end
def create_user(request)
# 1. Deserialize request
req = Userservice::CreateUserRequest.decode(request.payload)
# 2. Validate input
if req.name.empty? || req.email.empty?
raise ArgumentError, 'Name and email are required'
end
# 3. Check authorization from metadata
auth_token = request.get_metadata('authorization')
unless auth_token
raise SecurityError, 'Authentication required'
end
# 4. Business logic
user = @user_repository.create(
name: req.name,
email: req.email
)
# 5. Build response
response_user = Userservice::User.new(
id: user.id,
name: user.name,
email: user.email,
created_at: Time.now.utc.iso8601
)
# 6. Serialize with metadata
response = Spikard::Grpc::Response.new(
payload: Userservice::User.encode(response_user)
)
response.metadata = {
'x-user-id' => user.id.to_s,
'x-created' => 'true'
}
response
end
end
<?php declare(strict_types=1);
use Spikard\Grpc\HandlerInterface;
use Spikard\Grpc\Request;
use Spikard\Grpc\Response;
use Userservice\GetUserRequest;
use Userservice\CreateUserRequest;
use Userservice\User;
class UserServiceHandler implements HandlerInterface
{
public function __construct(
private UserRepository $userRepository,
) {}
public function handleRequest(Request $request): Response
{
// Route based on method name
return match ($request->methodName) {
'GetUser' => $this->getUser($request),
'CreateUser' => $this->createUser($request),
default => Response::error("Unknown method: {$request->methodName}"),
};
}
private function getUser(Request $request): Response
{
try {
// 1. Deserialize request
$req = new GetUserRequest();
$req->mergeFromString($request->payload);
// 2. Validate input
if ($req->getId() <= 0) {
return Response::error('User ID must be positive');
}
// 3. Business logic
$user = $this->userRepository->findById($req->getId());
if (!$user) {
return Response::error("User {$req->getId()} not found");
}
// 4. Build response
$responseUser = new User();
$responseUser->setId($user->getId());
$responseUser->setName($user->getName());
$responseUser->setEmail($user->getEmail());
$responseUser->setCreatedAt($user->getCreatedAt()->format('c'));
// 5. Serialize and return
return new Response(
payload: $responseUser->serializeToString(),
metadata: ['x-user-found' => 'true']
);
} catch (\Exception $e) {
return Response::error("Error: {$e->getMessage()}");
}
}
private function createUser(Request $request): Response
{
try {
// 1. Deserialize request
$req = new CreateUserRequest();
$req->mergeFromString($request->payload);
// 2. Validate input
if (empty($req->getName()) || empty($req->getEmail())) {
return Response::error('Name and email are required');
}
// 3. Check authorization from metadata
$authToken = $request->getMetadata('authorization');
if (!$authToken) {
return Response::error(
'Authentication required',
'UNAUTHENTICATED'
);
}
// 4. Business logic
$user = $this->userRepository->create(
name: $req->getName(),
email: $req->getEmail()
);
// 5. Build response
$responseUser = new User();
$responseUser->setId($user->getId());
$responseUser->setName($user->getName());
$responseUser->setEmail($user->getEmail());
$responseUser->setCreatedAt((new \DateTime())->format('c'));
// 6. Serialize with metadata
return new Response(
payload: $responseUser->serializeToString(),
metadata: [
'x-user-id' => (string)$user->getId(),
'x-created' => 'true',
]
);
} catch (\Exception $e) {
return Response::error("Error: {$e->getMessage()}");
}
}
}
use bytes::Bytes;
use spikard_http::grpc::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
use tonic::Status;
use std::sync::Arc;
// Generated protobuf types
mod userservice {
include!("userservice.rs"); // Generated by prost
}
pub struct UserServiceHandler {
user_repository: Arc<dyn UserRepository + Send + Sync>,
}
impl UserServiceHandler {
pub fn new(user_repository: Arc<dyn UserRepository + Send + Sync>) -> Self {
Self { user_repository }
}
async fn get_user(&self, request: GrpcRequestData) -> GrpcHandlerResult {
// 1. Deserialize request
use prost::Message;
let req = userservice::GetUserRequest::decode(request.payload)
.map_err(|e| Status::invalid_argument(format!("Invalid request: {}", e)))?;
// 2. Validate input
if req.id <= 0 {
return Err(Status::invalid_argument("User ID must be positive"));
}
// 3. Business logic
let user = self.user_repository.find_by_id(req.id).await
.map_err(|e| Status::internal(format!("Database error: {}", e)))?
.ok_or_else(|| Status::not_found(format!("User {} not found", req.id)))?;
// 4. Build response
let response_user = userservice::User {
id: user.id,
name: user.name.clone(),
email: user.email.clone(),
created_at: user.created_at.to_rfc3339(),
};
// 5. Serialize
let mut buf = Vec::new();
response_user.encode(&mut buf)
.map_err(|e| Status::internal(format!("Encoding error: {}", e)))?;
// 6. Add metadata
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("x-user-found", "true".parse()
.map_err(|e| Status::internal(format!("Invalid metadata value: {}", e)))?
);
Ok(GrpcResponseData {
payload: Bytes::from(buf),
metadata,
})
}
async fn create_user(&self, request: GrpcRequestData) -> GrpcHandlerResult {
// 1. Deserialize request
use prost::Message;
let req = userservice::CreateUserRequest::decode(request.payload)
.map_err(|e| Status::invalid_argument(format!("Invalid request: {}", e)))?;
// 2. Validate input
if req.name.is_empty() || req.email.is_empty() {
return Err(Status::invalid_argument("Name and email are required"));
}
// 3. Check authorization from metadata
let auth_token = request.metadata
.get("authorization")
.and_then(|v| v.to_str().ok());
if auth_token.is_none() {
return Err(Status::unauthenticated("Authentication required"));
}
// 4. Business logic
let user = self.user_repository.create(&req.name, &req.email).await
.map_err(|e| Status::internal(format!("Create failed: {}", e)))?;
// 5. Build response
let response_user = userservice::User {
id: user.id,
name: user.name.clone(),
email: user.email.clone(),
created_at: chrono::Utc::now().to_rfc3339(),
};
// 6. Serialize
let mut buf = Vec::new();
response_user.encode(&mut buf)
.map_err(|e| Status::internal(format!("Encoding error: {}", e)))?;
// 7. Add metadata
let mut metadata = tonic::metadata::MetadataMap::new();
metadata.insert("x-user-id", user.id.to_string().parse()
.map_err(|e| Status::internal(format!("Invalid metadata value: {}", e)))?
);
metadata.insert("x-created", "true".parse()
.map_err(|e| Status::internal(format!("Invalid metadata value: {}", e)))?
);
Ok(GrpcResponseData {
payload: Bytes::from(buf),
metadata,
})
}
}
impl GrpcHandler for UserServiceHandler {
fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
Box::pin(async move {
match request.method_name.as_str() {
"GetUser" => self.get_user(request).await,
"CreateUser" => self.create_user(request).await,
_ => Err(Status::unimplemented(format!("Unknown method: {}", request.method_name))),
}
})
}
fn service_name(&self) -> &'static str {
"userservice.UserService"
}
}
Common Patterns¶
Key Patterns by Language¶
- Async/await: All handlers are async for non-blocking I/O
ParseFromString(): Deserializes binary protobuf to Python objectSerializeToString(): Serializes Python object to binary protobuf- Exception mapping:
ValueError->INVALID_ARGUMENT,PermissionError->PERMISSION_DENIED - Metadata access:
request.get_metadata(key)returnsstr | None
- Protobufjs: Uses
.decode()and.encode().finish()for serialization - Buffer: gRPC payloads are Node.js
Bufferobjects - GrpcError: Throw with explicit status codes for proper error responses
- Helper functions:
createUnaryHandlerandcreateServiceHandlerreduce boilerplate - Type safety: Full TypeScript type inference for protobuf messages
- Synchronous: Ruby handlers are synchronous (Rust runtime handles async)
.decode()/.encode(): Ruby protobuf methods for serialization- Metadata access:
request.get_metadata(key)returnsString | nil - Response construction: Create response, then set metadata separately
- Error responses: Use
Response.error()for error cases - Exception mapping: Rescue exceptions and convert to gRPC status codes
- Synchronous: PHP handlers are synchronous
mergeFromString(): Deserializes binary protobuf (use merge, not parse)serializeToString(): Serializes protobuf to binary- Getters/Setters: PHP protobuf uses getter/setter methods
- Error responses: Return
Response::error()instead of throwing - Named arguments: PHP 8.0+ named arguments for clarity
- Type hints: Leverage PHP type system for safety
- Async/await: Handlers return
Pin<Box<dyn Future>> - prost: Uses
.decode()and.encode()for protobuf - Error handling: Return
tonic::Statusdirectly - Zero-copy: Uses
Bytesfor efficient payload handling - Type safety: Full compile-time type checking
- Arc: Shared ownership for thread-safe repository access
Error Handling¶
try:
# Handler logic
pass
except ValueError as e:
# Maps to INVALID_ARGUMENT
raise
except PermissionError as e:
# Maps to PERMISSION_DENIED
raise
except NotImplementedError as e:
# Maps to UNIMPLEMENTED
raise
except Exception as e:
# Maps to INTERNAL
raise
Mapping is automatic via FFI layer (pyerr_to_grpc_status).
import { GrpcError, GrpcStatusCode } from 'spikard';
// Explicit status codes
throw new GrpcError(GrpcStatusCode.INVALID_ARGUMENT, 'Invalid ID');
throw new GrpcError(GrpcStatusCode.NOT_FOUND, 'User not found');
throw new GrpcError(GrpcStatusCode.UNAUTHENTICATED, 'Auth required');
throw new GrpcError(GrpcStatusCode.PERMISSION_DENIED, 'Access denied');
throw new GrpcError(GrpcStatusCode.INTERNAL, 'Internal error');
Explicit GrpcError for all status codes.
class UserServiceHandler < Spikard::Grpc::Handler
def handle_request(request)
case request.method_name
when 'GetUser'
get_user(request)
else
# Return error response
Spikard::Grpc::Response.error(
"Method not implemented: #{request.method_name}"
)
end
rescue ArgumentError => e
# Invalid argument error
Spikard::Grpc::Response.error(e.message, 'INVALID_ARGUMENT')
rescue SecurityError => e
# Authentication error
Spikard::Grpc::Response.error(e.message, 'UNAUTHENTICATED')
rescue StandardError => e
# Internal error
Spikard::Grpc::Response.error("Internal error: #{e.message}")
end
end
<?php declare(strict_types=1);
// Return error response
return Response::error('Error message');
// With status code in metadata
return Response::error(
'Error message',
'INVALID_ARGUMENT'
);
// Try-catch pattern
try {
// Handler logic
} catch (\InvalidArgumentException $e) {
return Response::error($e->getMessage());
} catch (\Exception $e) {
return Response::error("Internal error: {$e->getMessage()}");
}
Return error responses instead of throwing.
// Direct tonic::Status
return Err(Status::invalid_argument("Invalid ID"));
return Err(Status::not_found("User not found"));
return Err(Status::unauthenticated("Auth required"));
return Err(Status::permission_denied("Access denied"));
return Err(Status::internal("Internal error"));
// With .map_err()
user_repository.find_by_id(id)
.await
.map_err(|e| Status::internal(format!("DB error: {}", e)))?;
Type-safe Result<T, Status> pattern.
Status Codes Reference¶
gRPC Status Codes Reference¶
Complete reference table for all 17 standard gRPC status codes.
| Code | Numeric | When to Use | HTTP Equivalent |
|---|---|---|---|
| OK | 0 | Request completed successfully | 200 OK |
| CANCELLED | 1 | Operation was cancelled (typically by caller) | 499 Client Closed Request |
| UNKNOWN | 2 | Unknown error or unmapped status from another system | 500 Internal Server Error |
| INVALID_ARGUMENT | 3 | Client specified an invalid argument (validation errors) | 400 Bad Request |
| DEADLINE_EXCEEDED | 4 | Operation deadline was exceeded before completion | 504 Gateway Timeout |
| NOT_FOUND | 5 | Requested entity (e.g., file, user) was not found | 404 Not Found |
| ALREADY_EXISTS | 6 | Entity that client attempted to create already exists | 409 Conflict |
| PERMISSION_DENIED | 7 | Caller lacks permission for the operation | 403 Forbidden |
| RESOURCE_EXHAUSTED | 8 | Resource has been exhausted (quota, rate limit) | 429 Too Many Requests |
| FAILED_PRECONDITION | 9 | Operation rejected because system not in required state | 400 Bad Request |
| ABORTED | 10 | Operation aborted due to concurrency issues | 409 Conflict |
| OUT_OF_RANGE | 11 | Operation attempted past valid range | 400 Bad Request |
| UNIMPLEMENTED | 12 | Operation is not implemented or not supported | 501 Not Implemented |
| INTERNAL | 13 | Internal server error | 500 Internal Server Error |
| UNAVAILABLE | 14 | Service is currently unavailable (temporary condition) | 503 Service Unavailable |
| DATA_LOSS | 15 | Unrecoverable data loss or corruption | 500 Internal Server Error |
| UNAUTHENTICATED | 16 | Request missing or invalid authentication credentials | 401 Unauthorized |
Usage Guidelines¶
-
Choose the most specific code: Use the most descriptive status code that accurately represents the error condition.
-
Provide helpful messages: Include clear, actionable error messages that help clients understand and resolve the issue.
-
Never expose sensitive information: Don't include stack traces, database errors, or internal system details in error messages.
-
Use INTERNAL for unexpected errors: When encountering unexpected server errors, return INTERNAL and log the details server-side.
-
Distinguish UNAUTHENTICATED vs PERMISSION_DENIED: Use UNAUTHENTICATED for missing/invalid credentials, PERMISSION_DENIED for authenticated users lacking permissions.
-
Consider retry behavior: Clients may automatically retry certain codes (UNAVAILABLE, DEADLINE_EXCEEDED) but not others (INVALID_ARGUMENT, PERMISSION_DENIED).
Next Steps¶
- Streaming RPCs: Server, client, and bidirectional streaming
- Authentication: Implement auth using metadata headers
- Observability: Add request tracing and logging
Learn More¶
- Protobuf/gRPC Guide - Comprehensive reference
- Proto3 Language Guide
- gRPC Core Concepts
Summary¶
You've learned:
- What Spikard gRPC is: Handler-focused gRPC with a shared Rust runtime
- How to write .proto files: Define messages and services
- Code generation: Use protoc to generate language-specific types
- Handler implementation: Deserialize -> Process -> Serialize pattern
- Testing: Write comprehensive tests for your handlers
Key Takeaway: Spikard gRPC lets you focus on business logic. The runtime handles HTTP/2, gRPC protocol, routing, and status codes.