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

See also
gRPC middleware tutorial.

The gRPC client can be extended by middlewares. Middleware is called on each outgoing RPC request and incoming response. 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 client including authorization and authentication, ratelimiting, logging, tracing, audit, etc.

Default middlewares

There is a ugrpc::client::MiddlewarePipelineComponent component for configuring the middlewares's 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 client components and a set of builtin middlewares use ugrpc::client::DefaultComponentList or ugrpc::client::MinimalComponentList. As will be shown below, custom middlewares require additional actions to work: registering in grpc-client-middleware-pipeline and writing a required static config entry.

ugrpc::client::MiddlewarePipelineComponent is a global configuration of client middlewares. If you don't want to disable userver middlewares, just take that config:

components_manager:
components:
grpc-client-middlewares-pipeline:
grpc-client-common:
# some options...
some-client-factory:
# some client options...

Enable/disable middlewares

You can enable or disable any middleware:

components_manager:
components:
grpc-client-common:
grpc-client-middlewares-pipeline:
middlewares:
grpc-client-baggage:
enabled: false # globally disable for all clients
some-client-factory:
middlewares:
# force enable in that client. Or it can be disabled for special clients
grpc-client-baggage:
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::client::MiddlewareBase. Class that implements the main logic of a middleware.
  2. ugrpc::client::MiddlewareFactoryComponentBase The factory for the middleware to declare static options.

MiddlewareBase

ugrpc::client::MiddlewareBase

PreStartCall and PostFinish

Methods ugrpc::client::MiddlewareBase::PreStartCall and ugrpc::client::MiddlewareBase::PostFinish are called once per grpc Call (RPC).

PreStartCall is called before the first message is sent. PostFinish is called after the last message is received or after an error status is received from the downstream service.

PreStartCall hooks are called in the direct middlewares order. PostFinish hooks are called in the reversed order.

Streaming RPCs can have multiple requests and responses, but PreStartCall and PostFinish are called once per RPC in any case.

For more information about the middlewares order:

See also
gRPC middlewares order.

Per-call (RPC) hooks implementation example

class AuthMiddleware final : public ugrpc::client::MiddlewareBase {
public:
// Name of a middleware-factory that creates this middleware.
static constexpr std::string_view kName = "grpc-auth-client";
// 'Auth' is a group for authentication. See middlewares groups for more information.
static inline const auto kDependency =
middlewares::MiddlewareDependencyBuilder().InGroup<middlewares::groups::Auth>();
AuthMiddleware();
~AuthMiddleware() override;
void PreStartCall(ugrpc::client::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::client::MiddlewareFactoryComponentBase'
void ApplyCredentials(::grpc::ClientContext& context) { context.AddMetadata(kKey, kCredentials); }
AuthMiddleware::AuthMiddleware() = default;
AuthMiddleware::~AuthMiddleware() = default;
void AuthMiddleware::PreStartCall(ugrpc::client::MiddlewareCallContext& context) const {
ApplyCredentials(context.GetContext());
}

Register the Middleware component in the component system.

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-auth-client:
grpc-client-middlewares-pipeline:
middlewares:
grpc-auth-client:
enabled: true # register the middleware in the pipeline

PreSendMessage and PostRecvMessage

PreSendMessage hooks are called in the order of middlewares. PostRecvMessage hooks are called in the reverse order of middlewares.

For more information about the middlewares order:

See also
gRPC middlewares order.

These hooks are called on each message.

PreSendMessage:

  • unary: is called exactly once
  • stream: is called 0, 1 or more

PostRecvMessage:

  • unary: is called 0 or 1 (0 if service doesn't return a message)
  • stream: is called 0, 1 or more

Per-message hooks implementation example

class Middleware final : public MiddlewareBase {
public:
explicit Middleware(const Settings& settings);
void PreStartCall(MiddlewareCallContext& context) const override;
void PreSendMessage(MiddlewareCallContext& context, const google::protobuf::Message& message) const override;
void PostRecvMessage(MiddlewareCallContext& context, const google::protobuf::Message& message) const override;
void PostFinish(MiddlewareCallContext& context, const grpc::Status& status) const override;
private:
Settings settings_;
};
void Middleware::PreSendMessage(MiddlewareCallContext& context, const google::protobuf::Message& message) const {
SpanLogger logger{context.GetSpan()};
{ugrpc::impl::kTypeTag, "request"}, //
{"body", GetMessageForLogging(message, settings_)}, //
};
if (context.IsClientStreaming()) {
logger.Log(logging::Level::kInfo, "gRPC request stream message", std::move(extra));
} else {
logger.Log(logging::Level::kInfo, "gRPC request", std::move(extra));
}
}
void Middleware::PostRecvMessage(MiddlewareCallContext& context, const google::protobuf::Message& message) const {
SpanLogger logger{context.GetSpan()};
{ugrpc::impl::kTypeTag, "response"}, //
{"body", GetMessageForLogging(message, settings_)}, //
};
if (context.IsServerStreaming()) {
logger.Log(logging::Level::kInfo, "gRPC response stream message", std::move(extra));
} else {
logger.Log(logging::Level::kInfo, "gRPC response", std::move(extra));
}
}

The static YAML config and component registration are identical as in example above. So, let's not focus on this.

Exceptions and errors in middlewares

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

See also
grpc_client_middlewares_order.

All exceptions are rethrown to the user code from client's RPC creating methods, Read / Write (for streaming), and from methods that return the RPC status.

Using static config options in middlewares

There are two ways to implement a middleware component. You can see above ugrpc::client::SimpleMiddlewareFactoryComponent. This component is need 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::client::MiddlewareFactoryComponentBase.

See also
gRPC middlewares configuration.

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

Middlewares order

Before starting to read specifics of client middlewares ordering:

See also
gRPC middlewares order.

There are simple cases above: we just set Auth group for one middleware.

Here we say that all client middlewares are located in these groups.

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