Calculator - Multi-Method Service
This example demonstrates a gRPC service with multiple methods and proper error handling.
What You'll Learn
- Defining a service with multiple RPC methods
- Implementing multiple handlers
- Using gRPC status codes for error handling
- Working with numeric proto types
Proto Definition
// calculator.proto
syntax = "proto3";
package calculator;
service Calculator {
rpc Add (CalculatorRequest) returns (CalculatorResponse);
rpc Subtract (CalculatorRequest) returns (CalculatorResponse);
rpc Multiply (CalculatorRequest) returns (CalculatorResponse);
rpc Divide (CalculatorRequest) returns (CalculatorResponse);
}
message CalculatorRequest {
double first_number = 1;
double second_number = 2;
}
message CalculatorResponse {
double result = 1;
}Server Implementation
using gRPCServer
include("generated/calculator/calculator.jl")
using .calculator
# Handlers
function add(ctx::ServerContext, request::CalculatorRequest)::CalculatorResponse
@info "Add" a=request.first_number b=request.second_number
CalculatorResponse(request.first_number + request.second_number)
end
function subtract(ctx::ServerContext, request::CalculatorRequest)::CalculatorResponse
@info "Subtract" a=request.first_number b=request.second_number
CalculatorResponse(request.first_number - request.second_number)
end
function multiply(ctx::ServerContext, request::CalculatorRequest)::CalculatorResponse
@info "Multiply" a=request.first_number b=request.second_number
CalculatorResponse(request.first_number * request.second_number)
end
function divide(ctx::ServerContext, request::CalculatorRequest)::CalculatorResponse
@info "Divide" a=request.first_number b=request.second_number
if request.second_number == 0.0
throw(GRPCError(StatusCode.INVALID_ARGUMENT, "Division by zero"))
end
CalculatorResponse(request.first_number / request.second_number)
end
# Service definition
struct CalculatorService end
function gRPCServer.service_descriptor(::CalculatorService)
ServiceDescriptor(
"calculator.Calculator",
Dict(
"Add" => MethodDescriptor("Add", MethodType.UNARY,
CalculatorRequest, CalculatorResponse, add),
"Subtract" => MethodDescriptor("Subtract", MethodType.UNARY,
CalculatorRequest, CalculatorResponse, subtract),
"Multiply" => MethodDescriptor("Multiply", MethodType.UNARY,
CalculatorRequest, CalculatorResponse, multiply),
"Divide" => MethodDescriptor("Divide", MethodType.UNARY,
CalculatorRequest, CalculatorResponse, divide)
),
nothing
)
end
function main()
server = GRPCServer("127.0.0.1", 50052;
enable_health_check = true,
enable_reflection = true
)
register!(server, CalculatorService())
@info "Calculator gRPC server starting" host="127.0.0.1" port=50052
run(server)
end
main()Key Concepts
Error Handling with Status Codes
Throw GRPCError to return appropriate status codes:
throw(GRPCError(StatusCode.INVALID_ARGUMENT, "Division by zero"))Common status codes:
INVALID_ARGUMENT- Client provided invalid inputNOT_FOUND- Requested resource doesn't existPERMISSION_DENIED- Client lacks permissionINTERNAL- Server-side errorUNAUTHENTICATED- Missing or invalid authentication
Multiple Methods in One Service
Register multiple methods in the ServiceDescriptor:
Dict(
"Method1" => MethodDescriptor(...),
"Method2" => MethodDescriptor(...),
"Method3" => MethodDescriptor(...)
)Each method can have its own handler function with appropriate logic.
Numeric Types
Proto double maps to Julia Float64. The generated types handle serialization automatically.
Testing
Run the Server
cd examples/05_calculator
julia --project=../.. server.jlNote: This example uses port 50052 (different from hello world examples).
Test Add
grpcurl -plaintext -d '{"first_number": 10, "second_number": 5}' \
localhost:50052 calculator.Calculator/AddExpected output:
{
"result": 15
}Test Subtract
grpcurl -plaintext -d '{"first_number": 10, "second_number": 3}' \
localhost:50052 calculator.Calculator/SubtractExpected output:
{
"result": 7
}Test Multiply
grpcurl -plaintext -d '{"first_number": 7, "second_number": 6}' \
localhost:50052 calculator.Calculator/MultiplyExpected output:
{
"result": 42
}Test Divide
grpcurl -plaintext -d '{"first_number": 20, "second_number": 4}' \
localhost:50052 calculator.Calculator/DivideExpected output:
{
"result": 5
}Test Error Handling (Division by Zero)
grpcurl -plaintext -d '{"first_number": 10, "second_number": 0}' \
localhost:50052 calculator.Calculator/DivideExpected output:
ERROR:
Code: InvalidArgument
Message: Division by zeroBest Practices
Input Validation
Always validate inputs before processing:
function handler(ctx::ServerContext, request::RequestType)
if !is_valid(request)
throw(GRPCError(StatusCode.INVALID_ARGUMENT, "Invalid input: ..."))
end
# Process valid request
endConsistent Error Messages
Provide clear, actionable error messages:
# Good
throw(GRPCError(StatusCode.INVALID_ARGUMENT, "Division by zero"))
# Better
throw(GRPCError(StatusCode.INVALID_ARGUMENT,
"Cannot divide $(request.first_number) by zero"))Logging
Log request details for debugging:
@info "Operation" a=request.first_number b=request.second_number request_id=ctx.request_idNext Steps
For production features, see Advanced Topics covering:
- Interceptors for logging, authentication, and rate limiting
- TLS configuration for secure connections
- Compression for large messages
- Health checking for load balancers