- 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:
- ugrpc::server::MiddlewareBase. Class that implements the main logic of a middleware
- 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:
static constexpr std::string_view kName = "grpc-server-auth";
static inline const auto kDependency =
middlewares::MiddlewareDependencyBuilder().InGroup<middlewares::groups::Auth>();
Middleware();
void OnCallStart(ugrpc::server::MiddlewareCallContext& context) const override;
};
Middleware::Middleware() = default;
void Middleware::OnCallStart(ugrpc::server::MiddlewareCallContext& context) const {
auto it = metadata.find(kKey);
if (it == metadata.cend() || it->second != kCredentials) {
return context.
SetError(::grpc::Status{::grpc::StatusCode::PERMISSION_DENIED,
"Invalid credentials"});
}
}
Register the component.
int main(int argc, char* argv[]) {
.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>();
}
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;
};
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[]) {
.Append<components::TestsuiteSupport>()
.Append<functional_tests::MyMiddlewareComponent>()
.Append<functional_tests::MySecondMiddlewareComponent>()
.Append<functional_tests::GreeterServiceComponent>();
}
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:
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).
- 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
.
- If a handler returns an error, all
OnCallFinish
will be called.
- 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...