userver: gRPC server middlewares
Loading...
Searching...
No Matches
gRPC server middlewares

See also
gRPC middleware tutorial.

The gRPC server can be extended by middlewares. Middleware hooks are called at the various corresponding stages of handling of each incoming RPC. Different middlewares handle the call in the defined order. A middleware may decide to reject the call or call the next middleware in the stack. Middlewares may implement almost any enhancement to the gRPC server including authorization and authentication, ratelimiting, logging, tracing, audit, etc.

Default middlewares

There is an ugrpc::server::MiddlewarePipelineComponent component for configuring the middlewares pipeline. There are default middlewares:

If you add these middlewares to the components::ComponentList, these middlewares will be enabled by default. To register core gRPC server components and a set of builtin middlewares use ugrpc::server::DefaultComponentList or ugrpc::server::MinimalComponentList. As will be shown below, custom middlewares require additional actions to work: registering in grpc-server-middleware-pipeline and writing a required static config entry.

ugrpc::server::MiddlewarePipelineComponent is a component for a global configuration of server middlewares. You can enable/disable middlewares with enabled option.

If you don't want to disable userver middlewares, just take that config:

components_manager:
components:
grpc-server-middlewares-pipeline:
grpc-server:
# some server options...
some-service:
# some service options...

Enable/disable middlewares

You can enable or disable any middleware:

components_manager:
components:
grpc-server:
grpc-server-middlewares-pipeline:
middlewares:
grpc-server-headers-propagator:
enabled: false # globally disable for all services
some-service:
middlewares:
# force enable in that service. Or it can be disabled for special service
grpc-server-headers-propagator:
enabled: true

For more information about enabled:

See also
gRPC middlewares configuration.

Two main classes

There are two main interfaces for implementing a middleware:

  1. ugrpc::server::MiddlewareBase. Class that implements the main logic of a middleware
  2. ugrpc::server::MiddlewareFactoryComponentBase. The factory for the middleware to declare static options.

MiddlewareBase

OnCallStart and OnCallFinish

Methods ugrpc::server::MiddlewareBase::OnCallStart and ugrpc::server::MiddlewareBase::OnCallFinish are called once per grpc Call (RPC).

OnCallStart is called after the client metadata is received. OnCallFinish is called before the last message is sent or before error status is sent to a client.

OnCallStart hooks are called in the order of middlewares. OnCallFinish hooks are called in the reverse order of middlewares

Per-Call (RPC) hooks implementation example

class Middleware final : public ugrpc::server::MiddlewareBase {
public:
// Name of a middleware-factory that creates this middleware.
static constexpr std::string_view kName = "grpc-server-auth";
// 'Auth' is a group for auth authentication. See middlewares groups for more information.
static inline const auto kDependency =
middlewares::MiddlewareDependencyBuilder().InGroup<middlewares::groups::Auth>();
Middleware();
void OnCallStart(ugrpc::server::MiddlewareCallContext& context) const override;
};
// This component creates Middleware. Name of the component is 'Middleware::kName'.
// In this case we use a short-cut for defining a middleware-factory, but you can create your own factory by
// inheritance from 'ugrpc::server::MiddlewareFactoryComponentBase'
Middleware::Middleware() = default;
void Middleware::OnCallStart(ugrpc::server::MiddlewareCallContext& context) const {
const auto& metadata = context.GetServerContext().client_metadata();
auto it = metadata.find(kKey);
if (it == metadata.cend() || it->second != kCredentials) {
LOG_ERROR() << "Invalid credentials";
return context.SetError(::grpc::Status{::grpc::StatusCode::PERMISSION_DENIED, "Invalid credentials"});
}
}

Register the component.

int main(int argc, char* argv[]) {
const auto component_list = components::MinimalServerComponentList()
.Append<components::TestsuiteSupport>()
.Append<samples::grpc::auth::GreeterClient>()
.Append<samples::grpc::auth::GreeterServiceComponent>()
.Append<samples::grpc::auth::GreeterHttpHandler>()
.Append<samples::grpc::auth::server::AuthComponent>()
.Append<samples::grpc::auth::server::MetaFilterComponent>()
.Append<samples::grpc::auth::client::AuthComponent>()
.Append<samples::grpc::auth::client::ChaosComponent>();
return utils::DaemonMain(argc, argv, component_list);
}

The static YAML config.

grpc-server-auth:
grpc-server-middlewares-pipeline:
middlewares:
grpc-server-auth: # register the middleware in the pipeline
enabled: true

PostRecvMessage and PreSendMessage

You can add some behavior on each request/response. Especially, it can be important for grpc-stream. See about streams in gRPC.

PostRecvMessage hooks are called in the direct middlewares order. PreSendMessage hooks are called in the reversed order.

For more information about the middlewares order:

See also
gRPC middlewares order.

Per-message hooks implementation example

class MyMiddleware final : public ugrpc::server::MiddlewareBase {
public:
static constexpr std::string_view kName = "my-middleware-server";
static inline const auto kDependency = middlewares::MiddlewareDependencyBuilder();
MyMiddleware() = default;
void PostRecvMessage(ugrpc::server::MiddlewareCallContext& context, google::protobuf::Message& request)
const override;
void PreSendMessage(ugrpc::server::MiddlewareCallContext& context, google::protobuf::Message& response)
const override;
};
// There isn't a special logic to construct that middleware (doesn't have static config options) => use short-cut
void MyMiddleware::PostRecvMessage(ugrpc::server::MiddlewareCallContext& context, google::protobuf::Message& request)
const {
const google::protobuf::Descriptor* descriptor = request.GetDescriptor();
const google::protobuf::FieldDescriptor* name_field = descriptor->FindFieldByName("name");
UINVARIANT(name_field->type() == google::protobuf::FieldDescriptor::TYPE_STRING, "field must be a string");
if (name_field) {
const google::protobuf::Reflection* reflection = request.GetReflection();
auto name = reflection->GetString(request, name_field);
name += " One";
reflection->SetString(&request, name_field, name);
} else {
return context.SetError(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Field 'name' not found"));
}
}
void MyMiddleware::PreSendMessage(ugrpc::server::MiddlewareCallContext& context, google::protobuf::Message& response)
const {
const google::protobuf::Descriptor* descriptor = response.GetDescriptor();
const google::protobuf::FieldDescriptor* name_field = descriptor->FindFieldByName("greeting");
UINVARIANT(name_field->type() == google::protobuf::FieldDescriptor::TYPE_STRING, "field must be a string");
if (name_field) {
const google::protobuf::Reflection* reflection = response.GetReflection();
auto greeting = reflection->GetString(response, name_field);
greeting += " EndOne";
reflection->SetString(&response, name_field, greeting);
} else {
return context.SetError(grpc::Status(grpc::StatusCode::INVALID_ARGUMENT, "Field 'greeting' not found"));
}
}

The static YAML config.

my-middleware-server:
grpc-server-middlewares-pipeline:
middlewares:
# We must register middlewares in the pipeline:
my-middleware-server:
enabled: true

Register the middleware component in the component system.

int main(int argc, const char* const argv[]) {
const auto component_list = components::MinimalServerComponentList()
.AppendComponentList(ugrpc::server::DefaultComponentList())
.Append<components::TestsuiteSupport>()
.Append<functional_tests::MyMiddlewareComponent>()
.Append<functional_tests::MySecondMiddlewareComponent>()
.Append<functional_tests::GreeterServiceComponent>();
return utils::DaemonMain(argc, argv, component_list);
}

Exceptions and errors in middlewares

To fully understand what happens when middlewares hooks fail, you should understand the middlewares order:

See also
grpc_server_middlewares_order.

If you want to interrupt a Call (RPC) in middlewares, you should use ugrpc::server::MiddlewareCallContext::SetError (see examples above on this page).

If you throw an exception in middlewares hooks, that exception will be translated to grpc::Status (by default grpc::StatusCode::UNKNOWN) and next hooks won't be called. server::handlers::CustomHandlerException is translated to a relevant grpc::Status.

All errors will be logged just like an exception or error status from the user handler:

Note
But exceptions are not the best practice in middlewares hooks ⇒ prefer SetError.

Errors and OnCallFinish

ugrpc::server::MiddlewareBase::OnCallFinish will be called despite of any errors.

The actual status is passed to OnCallFinish hooks. Each OnCallFinish hook gets the status from a previous OnCallFinish call and can change that by SetError (or exception). An error status from a handler will be passed to a first OnCallFinish and that hook can change that status, next hooks will get the new status. If all OnCallFinish hooks don't change the status, that status will be the final status for a client.

The call path example with errors in the pipeline

There are 3 middlewares A, B, C. Cases:

  1. A::OnCallStart and B::OnCallStart succeed, but C::OnCallStart fails (by SetError or exception) ⇒ B::OnCallFinish and A::OnCallFinish will be called (remember that OnCallFinish order is reversed).
  2. If all OnCallStart succeed and C::OnCallFinish fails, B::OnCallStart and A::OnCallStart will be called and these hooks get an error status from C::OnCallFinish.
  3. If a handler returns an error, all OnCallFinish will be called.
  4. If there are errors in PostRecvMessage/PreSendMessage ⇒ RPC is failed ⇒ all OnCallFinish hooks will be called.

Using static config options in middlewares

There are two ways to implement a middleware component. You can see above ugrpc::server::SimpleMiddlewareFactoryComponent. This component is needed for simple cases without static config options of a middleware.

Note
In that case, kName and kDependency (middlewares::MiddlewareDependencyBuilder) must be in a middleware class (as shown above).

If you want to use static config options for your middleware, use ugrpc::server::MiddlewareFactoryComponentBase.

See also
gRPC middlewares configuration.

To override static config options of a middleware per a server see grpc_middlewares_config_override.

Middleware order

Before starting to read specifics of server middlewares ordering:

See also
gRPC middlewares order.

There are simple cases above: we just set Auth group for one middleware and use a default constructor of MiddlewareDependencyBuilder in other middleware. Here we say that all server middlewares are located in these groups.

PreCore group is called firstly, then Logging and so forth...