userver: gRPC server middleware implementation
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
gRPC server middleware implementation

Your opinion will help to improve our service

Leave a feedback >

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::SimpleMiddlewareFactoryComponent short-cut for simple cases without 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 =
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<server::handlers::ServerMonitor>()
.Append<functional_tests::MyMiddlewareComponent>()
.Append<functional_tests::MySecondMiddlewareComponent>()
.Append<functional_tests::GreeterServiceComponent>();
return utils::DaemonMain(argc, argv, component_list);
}

MiddlewareFactoryComponent

We use a simple short-cut ugrpc::server::SimpleMiddlewareFactoryComponent in the example above. To declare static config options of your middleware see gRPC middlewares configuration.

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...