gRPC-Web Ruby
Background
Host gRPC-Web endpoints for Ruby gRPC services in a Rails or Rack app (over HTTP/1). Client included.
gRPC vs gRPC-Web
gRPC-Web is a variation of the gRPC protocol adapted to function over an HTTP/1 connection. This allows gRPC services to be accessed by web browser clients and using infrastructure that does not support end-to-end HTTP/2 load balancing like AWS ELBs and ALBs. ALBs only support HTTP/2 client -> LB not LB -> service.
Use Cases for gRPC-Web
- Client -> Server: Access typed gRPC + Protobuf APIs from javascript in the browser.
- Service <-> Service: Communicate between services using typed gRPC + Protobuf APIs over existing HTTP/1 infrastructure and load balancing solutions.
More Information
gRPC-Web Introductory Blog Post
gRPC-Web Protocol (with comparison against gRPC)
Compatibility
Supported Content Types
- application/grpc-web
- application/grpc-web+proto
- application/grpc-web+json
- application/grpc-web-text (base64 encoded)
- application/grpc-web-text+proto (base64 encoded)
Integration testing
gRPC-Web Ruby includes integration tests between the following implementations of gRPC-Web:
- Ruby Client with Ruby Server
- Javascript Client with Ruby Server
- Ruby Client with Envoy Proxy Server
Getting Started
Add the gem to your Gemfile:
gem 'grpc-web'
Implement a gRPC Service in Ruby
Build a service using the standard Ruby gRPC library or use any existing ruby gRPC service. For a more complete introduction to gRPC in Ruby checkout the gRPC Ruby Quickstart.
Define the service API using Protobuf
# hello.proto
syntax = "proto3";
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}
service HelloService {
rpc SayHello(HelloRequest) returns (HelloResponse);
}
Generate ruby code
# hello_pb.rb
require 'google/protobuf'
Google::Protobuf::DescriptorPool.generated_pool.build do
add_file("hello.proto", :syntax => :proto3) do
add_message "HelloRequest" do
optional :name, :string, 1
end
add_message "HelloResponse" do
optional :message, :string, 1
end
end
end
HelloRequest = Google::Protobuf::DescriptorPool.generated_pool.lookup("HelloRequest").msgclass
HelloResponse = Google::Protobuf::DescriptorPool.generated_pool.lookup("HelloResponse").msgclass
# hello_services_pb.rb
require 'grpc'
require 'hello_pb'
module HelloService
class Service
include GRPC::GenericService
self.marshal_class_method = :encode
self.unmarshal_class_method = :decode
self.service_name = 'HelloService'
rpc :SayHello, HelloRequest, HelloResponse
end
Stub = Service.rpc_stub_class
end
Implement the service
# example_hello_service.rb
require 'hello_services_pb'
class ExampleHelloService < HelloService::Service
def say_hello(request, _call = nil)
return HelloResponse.new(message: "Hello #{request.name}")
end
end
Run a gRPC-Web Server
You can run a gRPC-Web server using any Rack compliant HTTP server such as WEBrick, Unicorn, or Puma. You can also mount gRPC-Web endpoints alongside a Rails, Sinatra, or other Rack application.
Running with WEBrick
# example_grpc_web_server.rb
require 'grpc_web'
require 'example_hello_service'
require 'rack/handler'
GRPCWeb.handle(ExampleHelloService)
Rack::Handler::WEBrick.run GRPCWeb.rack_app
Running with Basic Auth
Since you can run a gRPC-Web server using any Rack compliant app, you can accordingly support basic authentication for the server as well.
# example_grpc_web_server.rb
require 'grpc_web'
require 'example_hello_service'
require 'rack/handler'
GRPCWeb.rack_app.use Rack::Auth::Basic do |username, password|
[user, password] == ["foobar", "verysecret"]
end
GRPCWeb.handle(ExampleHelloService)
Rack::Handler::WEBrick.run GRPCWeb.rack_app
Now you can point your client to a Basic Auth comformant URL for this rack service. Example: http://foobar:verysecret@localhost:3000/grpc
. Or via authentication headers, which ever is best suited by the client.
Configuring Services
Like the standard gRPC RpcServer class, GRPCWeb.handle
accepts either an instance of a service or a service class. gRPC-Web also supports a block syntax for .handle
that enables lazy loading of service classes.
Instance of a service
GRPCWeb.handle(ExampleHelloService.new('initializer argument'))
Service class
When a service class is provided a new instance will be created for each request using .new
.
GRPCWeb.handle(ExampleHelloService)
Lazy initialization block
The argument to .handle
is the service interface class (generated from proto) when using a lazy init block.
GRPCWeb.handle(HelloService::Service) do
require 'example_hello_service'
ExampleHelloService.new
end
Integration with Rails
gRPC-Web Ruby is designed to integrate easily with an existing Ruby on Rails application.
Mount gRPC-Web in the Rails route file
# config/routes.rb
require 'grpc_web'
Rails.application.routes.draw do
mount GRPCWeb.rack_app => '/grpc'
...
end
Configure services in an initializer
# config/initializers/grpc_web.rb
require 'hello_services_pb'
GRPCWeb.handle(HelloService::Service) do
require_dependency 'example_hello_service'
ExampleHelloService.new
end
Using the Ruby Client
The gRPC-Web Ruby client is tested to be compatible with both the gRPC-Web Ruby server and the Envoy gRPC-Web proxy.
Creating a client
require 'grpc_web/client'
require 'hello_services_pb'
$client = GRPCWeb::Client.new("http://localhost:3000/grpc", HelloService::Service)
Calling a method
$client.say_hello(name: 'James')
# => <HelloResponse: message: "Hello James">
You can also pass custom headers:
$client.say_hello({name: 'James'}, {metadata: {'custom-header' => 'Hello'}})
Using Basic Auth
gRPC-Web Ruby client supports Basic Auth out of the box. You can pass in a Basic Auth comformant URL and gRPC-Web Ruby client will take care of the rest when interacting with the server.
$client = GRPCWeb::Client.new("http://foobar:verysecret@localhost:3000/grpc", HelloService::Service)
Error Handling
The gRPC-Web Ruby client and server libraries support the propagation of exceptions using the standard grpc-status
and grpc-message
trailers.
gRPC-Web uses the standard error classes from the core grpc library. Any instance or subclass of GRPC::BadStatus
thrown in a service implementation will propoagate across the wire and be raised as an exception of the same class on the client side. Any other Error
will be treated as a GRPC::Unknown
.
Configuring an on_error callback
# config/initializers/grpc_web.rb
GRPCWeb.on_error do |ex, service, service_method|
ErrorNotifier.notify(ex, metadata: { service: service.class.service_name, method: service_method})
end
Additional Notes
CORS Middleware (for browser clients)
Web Browser clients will only allow HTTP requests to be made to your gRPC-Web API if CORS headers are correctly configured or the request is sameorigin (the gRPC-Web endpoints are hosted on the same domain that served the javascript code). You will need to use a library like cyu/rack-cors to manage CORS if you want to support browsers.
Contributing
See the developer's guide
Useful links
gRPC-Web Repo (protoc generator, js-client, proxy server) https://github.com/grpc/grpc-web
Improbable Eng gRPC Repo (js-client w/ TypeScript + NodeJS support, golang server) https://github.com/improbable-eng/grpc-web