Core Concepts

HTTP + gRPC Support

New to acton-service?

Start with the homepage to understand what acton-service is, then explore Core Concepts for foundational explanations. See the Glossary for technical term definitions.


HTTP + gRPC Support

Acton Service supports running HTTP REST APIs and gRPC services simultaneously, either on a single port with automatic protocol detection or on separate ports for isolated traffic management. This dual-protocol capability enables you to serve different client types from the same service deployment.


Why Run Both Protocols?

Combining HTTP and gRPC in a single service provides several architectural benefits:

Internal Service Communication

  • Use gRPC for efficient, type-safe service-to-service communication
  • Leverage HTTP/2 streaming and reduced serialization overhead
  • Automatic schema validation with Protocol Buffers

External Client Access

  • Expose HTTP REST APIs for browser clients and third-party integrations
  • Support clients without gRPC tooling or Protocol Buffer support
  • Maintain backwards compatibility with existing REST consumers

Operational Simplicity

  • Single deployment unit serving both protocols
  • Optional single-port mode with automatic protocol detection
  • Unified observability, logging, and metrics across both protocols
  • Simplified service mesh integration

Single-Port Mode (Default)

By default, Acton Service runs both HTTP and gRPC on the same port using automatic protocol detection. The framework inspects incoming connections to determine whether they're HTTP/1.1, HTTP/2 REST, or HTTP/2 gRPC requests.

How Protocol Detection Works

  • gRPC requests: Identified by the content-type: application/grpc header
  • HTTP/2 requests: Standard HTTP/2 traffic routed to REST handlers
  • HTTP/1.1 requests: Automatically upgraded or handled as REST

This detection happens transparently with no performance penalty, allowing clients to connect using their preferred protocol without coordination.

Benefits of Single-Port Mode

  • Simplified networking: One port to configure in firewalls and load balancers
  • Reduced resource usage: Single listener, single connection pool
  • Easier service discovery: Clients only need to know one endpoint
  • Cloud-friendly: Fewer port allocations in containerized environments

Separate-Port Mode

For scenarios requiring protocol isolation, you can configure separate ports for HTTP and gRPC traffic:

[grpc]
enabled = true
use_separate_port = true
port = 9090  # gRPC traffic only
[service]
port = 8080  # HTTP traffic only

When to Use Separate Ports

Network Policy Requirements

  • Different firewall rules for internal vs external traffic
  • Separate ingress controllers for HTTP and gRPC
  • Protocol-specific rate limiting or throttling

Performance Tuning

  • Dedicated connection pools for each protocol
  • Independent timeout configurations
  • Separate load balancing strategies

Monitoring and Observability

  • Protocol-specific metrics collection
  • Isolated traffic analysis
  • Granular network-level debugging

Configuration Options

Configure gRPC behavior in your config.toml:

OptionTypeDefaultDescription
grpc.enabledbooleantrueEnable or disable gRPC support
grpc.use_separate_portbooleanfalseUse dedicated port for gRPC traffic
grpc.portinteger9090Port for gRPC when use_separate_port = true
service.portinteger8080Primary HTTP port (also serves gRPC in single-port mode)

Single-Port Configuration (Default)

[grpc]
enabled = true
use_separate_port = false  # Automatic protocol detection on service.port

Separate-Port Configuration

[service]
name = "my-service"
port = 8080  # HTTP REST traffic

[grpc]
enabled = true
use_separate_port = true
port = 9090  # gRPC traffic only

Complete Example

Here's a complete service implementing both HTTP REST and gRPC:

use acton_service::prelude::*;
use acton_service::grpc::server::GrpcServicesBuilder;
use axum::{routing::get, Json};
use tonic::{Request, Response, Status};

// HTTP handler
async fn list_users() -> Json<Vec<String>> {
    Json(vec!["alice".to_string(), "bob".to_string()])
}

// Define HTTP routes with versioning
let http_routes = VersionedApiBuilder::new()
    .add_version(ApiVersion::V1, |router| {
        router.route("/users", get(list_users))
    })
    .build_routes();

// Define gRPC service
#[derive(Default)]
struct MyGrpcService;

#[tonic::async_trait]
impl my_service::MyService for MyGrpcService {
    async fn my_method(&self, req: Request<MyRequest>)
        -> Result<Response<MyResponse>, Status> {
        let user_id = req.into_inner().user_id;

        Ok(Response::new(MyResponse {
            message: format!("Hello from gRPC, user {}", user_id),
        }))
    }
}

// Collect gRPC services into tonic routes
let grpc_routes = GrpcServicesBuilder::new()
    .add_service(my_service::MyServiceServer::new(MyGrpcService))
    .build(None);

// Serve both protocols on the same port (automatic protocol detection)
ServiceBuilder::new()
    .with_routes(http_routes)
    .with_grpc_services(grpc_routes)
    .build()
    .serve()
    .await?;

GrpcServicesBuilder collects every gRPC service you register and returns a tonic::service::Routes value, which .with_grpc_services() accepts. Call .add_service() once per service — a single .with_grpc_services() call carries them all.

The build() argument is an optional AppState. Pass None when you don't enable the health service; pass Some(state) when you do, so health checks can inspect your configured dependencies.

Client Examples

HTTP REST client (any HTTP library, curl, browser):

curl http://localhost:8080/v1/users

gRPC client (using Tonic or any gRPC client):

let mut client = my_service::MyServiceClient::connect("http://localhost:8080").await?;
let response = client.my_method(MyRequest { user_id: 123 }).await?;

Both clients connect to the same port, with the framework automatically routing traffic to the appropriate handler.


Advanced Features

Reflection Support

Enable gRPC reflection for dynamic client discovery. GrpcServicesBuilder builds the reflection service for you — call .with_reflection() and register the file descriptor set generated by tonic-build:

use acton_service::grpc::server::GrpcServicesBuilder;

let grpc_routes = GrpcServicesBuilder::new()
    .with_reflection()
    .add_file_descriptor_set(my_service::FILE_DESCRIPTOR_SET)
    .add_service(my_service::MyServiceServer::new(MyGrpcService))
    .build(None);

ServiceBuilder::new()
    .with_grpc_services(grpc_routes)
    .build()
    .serve()
    .await?;

Health Checks

Both protocols share the same health check endpoint:

  • HTTP: GET /health
  • gRPC: Standard grpc.health.v1.Health service

Next Steps

  • See Examples for complete dual-protocol service implementations
  • Learn about Configuration options for fine-tuning both protocols
  • Explore gRPC Guide for protocol buffers, build setup, and implementation details
  • Review Observability for protocol-specific metrics and tracing
Previous
API Versioning