Qt GRPC Client Guide | Qt GRPC | Qt 6.11.1
Description: The Qt GRPC client guide.

- Qt 6.11
- Qt GRPC
- Qt GRPC Examples
- Qt GRPC Client Guide
- Qt 6.11.1 Reference Documentation

# Qt GRPC Client Guide

The Qt GRPC client guide.

#### Service Methods

In gRPC™, service methods can be defined in a protobuf schema to specify the communication between clients and servers. The protobuf compiler, `protoc`, can then generate the required server and client interfaces based on these definitions. gRPC supports four types of service methods:

- Unary Calls — The client sends a single request and receives a single response.

       rpc UnaryCall (Request) returns (Response);

  The corresponding client handler is QGrpcCallReply.

- Server Streaming — The client sends a single request and receives multiple responses.

       rpc ServerStreaming (Request) returns (stream Response);

  The corresponding client handler is QGrpcServerStream.

- Client Streaming — The client sends multiple requests and receives a single response.

       rpc ClientStreaming (stream Request) returns (Response);

  The corresponding client handler is QGrpcClientStream.

- Bidirectional Streaming — The client and server exchange multiple messages.

       rpc BidirectionalStreaming (stream Request) returns (stream Response);

  The corresponding client handler is QGrpcBidiStream.

gRPC communication always starts with the client, which initiates the remote procedure call (RPC) by sending the first message to the server. The server then concludes any type of communication by returning a StatusCode.

All client RPC handlers are derived from the QGrpcOperation class, which provides shared functionality. Due to the asynchronous nature of RPCs, they are naturally managed through Qt's Signals & Slots mechanism.

A key signal common to all RPC handlers is finished, which indicates the completion of an RPC. The handler emits this signal exactly once during its lifetime. This signal delivers the corresponding QGrpcStatus, providing additional information about the success or failure of the RPC.

There are also operation-specific functionalities, such as messageReceived for incoming messages, writeMessage for sending messages to the server, and writesDone for closing client-side communication. The table below outlines the supported functionality of the RPC client handlers:

Functionality | QGrpcCallReply | QGrpcServerStream | QGrpcClientStream | QGrpcBidiStream
--- | --- | --- | --- | ---
finished | ✓ (read final response) | ✓ | ✓ (read final response) | ✓
messageReceived | ✗ | ✓ | ✗ | ✓
writeMessage | ✗ | ✗ | ✓ | ✓
writesDone | ✗ | ✗ | ✓ | ✓

#### Getting Started

To use the Qt GRPC C++ API, start by using an already available protobuf schema or define your own. We will use the `clientguide.proto` file as an example:

     syntax = "proto3";
     package client.guide; // enclosing namespace

     message Request {
         int64 time = 1;
         sint32 num = 2;
     }

     message Response {
         int64 time = 1;
         sint32 num = 2;
     }

     service ClientGuideService {
         rpc UnaryCall (Request) returns (Response);
         rpc ServerStreaming (Request) returns (stream Response);
         rpc ClientStreaming (stream Request) returns (Response);
         rpc BidirectionalStreaming (stream Request) returns (stream Response);
     }

To use this .proto file for our Qt GRPC client in C++, we must run the `protoc` compiler with the Qt generator plugins on it. Fortunately, Qt provides the qt_add_grpc and qt_add_protobuf CMake functions to streamline this process.

     set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/clientguide.proto")

     find_package(Qt6 COMPONENTS Protobuf Grpc)
     qt_standard_project_setup(REQUIRES 6.9)

     qt_add_executable(clientguide_client main.cpp interceptors.cpp)

     # Using the executable as input target will append the generated files to it.
     qt_add_protobuf(clientguide_client
         PROTO_FILES ${proto_files}
     )
     qt_add_grpc(clientguide_client CLIENT
         PROTO_FILES ${proto_files}
     )

     target_link_libraries(clientguide_client PRIVATE Qt6::Protobuf Qt6::Grpc)

This results in two header files being generated in the current build directory:

- clientguide.qpb.h: Generated by qtprotobufgen. Declares the `Request` and `Response` protobuf messages from the schema.
- clientguide_client.grpc.qpb.h: Generated by qtgrpcgen. Declares the client interface for calling the methods of a gRPC server implementing the `ClientGuideService` from the schema.

The following client interface is generated:

     namespace client::guide {
     namespace ClientGuideService {

     class Client : public QGrpcClientBase
     {
         ...
         std::unique_ptr<QGrpcCallReply> UnaryCall(const client::guide::Request &arg);
         std::unique_ptr<QGrpcServerStream> ServerStreaming(const client::guide::Request &arg);
         std::unique_ptr<QGrpcClientStream> ClientStreaming(const client::guide::Request &arg);
         std::unique_ptr<QGrpcBidiStream> BidirectionalStreaming(const client::guide::Request &arg);
         ...
     };

     } // namespace ClientGuideService
     } // namespace client::guide

Note: Users are responsible for managing the unique RPC handlers returned by the `Client` interface, ensuring their existence at least until the finished signal is emitted. After receiving this signal, the handler can be safely reassigned or destroyed.

##### Server Setup

The server implementation for the `ClientGuideService` follows a straightforward approach. It validates the request message's `time` field, returning the `INVALID_ARGUMENT` status code if the time is in the future:

     const auto time = now();
     if (request->time() > time)
         return { grpc::StatusCode::INVALID_ARGUMENT, "Request time is in the future!" };

Additionally, the server sets the current time in every response message:

     response->set_num(request->num());
     response->set_time(time);
     return grpc::Status::OK;

For valid `time` requests, the service methods behave as follows:

- `UnaryCall`: Responds with the `num` field from the request.
- `ServerStreaming`: Sends `num` responses matching the request message.
- `ClientStreaming`: Counts the number of request messages and sets this count as `num`.
- `BidirectionalStreaming`: Immediately responds with the `num` field from each incoming request message.

##### Client Setup

We begin by including the generated header files:

     #include "clientguide.qpb.h"
     #include "clientguide_client.grpc.qpb.h"

For this example, we create the `ClientGuide` class to manage all communication, making it easier to follow. We begin by setting up the backbone of all gRPC communication: a channel.

     channel = std::make_shared<QGrpcHttp2Channel>(
         QUrl("http://localhost:50056")
         /* without channel options. */
     );
     ClientGuide clientGuide(channel);

The Qt GRPC library offers QGrpcHttp2Channel, which you can attach to the generated client interface:

     explicit ClientGuide(std::shared_ptr<QAbstractGrpcChannel> channel)
     {
         m_client.attachChannel(std::move(channel));
     }

With this setup, the client will communicate over HTTP/2 using TCP as the transport protocol. The communication will be unencrypted (i.e. without SSL/TLS setup).

###### Creating a request message

Here's a simple wrapper to create request messages:

     static guide::Request createRequest(int32_t num,
                                         ExpectedResult expected = ExpectedResult::Success)
     {
         guide::Request request;
         request.setNum(num);
         // The server-side logic fails the RPC if the time is in the future.
         const auto time = expected == ExpectedResult::Failure ?
             std::numeric_limits<int64_t>::max() : now();
         request.setTime(time);
         return request;
     }

This function takes an integer and an optional boolean. By default its messages use the current time, so the server logic should accept them. When called with `fail` set to `true`, however, it produces messages that the server shall reject.

##### Single Shot RPCs

There are different paradigms for working with RPC client handlers. Specifically, you can choose a class-based design where the RPC handler is a member of the enclosing class, or you can manage the lifetime of the RPC handler through the finished signal.

There are two important things to remember when applying the single-shot paradigm. The code below demonstrates how it would work for unary calls, but it's the same for any other RPC type.

     std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(requestMessage);
     const auto *replyPtr = reply.get(); // 1
     QObject::connect(
         replyPtr, &QGrpcCallReply::finished, replyPtr,
         [reply = std::move(reply)](const QGrpcStatus &status) {
             ...
         },
         Qt::SingleShotConnection        // 2
     );

- 1: Since we manage the lifetime of the unique RPC object within the lambda, moving it into the lambda's capture would invalidate `get()` and other member functions. Therefore, we must copy the pointers address before moving it.
- 2: The finished signal is emitted only once, making this a true single-shot connection. It is important to mark this connection as SingleShotConnection! If not, the capture of `reply` will not be destroyed, leading to a hidden memory leak that is hard to discover.

The SingleShotConnection argument in the `connect` call ensures that the slot functor (the lambda) is destroyed after being emitted, freeing the resources associated with the slot, including its captures.

#### Remote Procedure Calls

##### Unary Calls

Unary calls require only the finished signal to be handled. When this signal is emitted, we can check the status of the RPC to determine if it was successful. If it was, we can read the single and final response from the server.

In this example, we use the single-shot paradigm. Ensure you carefully read the Single Shot RPCs section.

     void unaryCall(const guide::Request &request, const QGrpcCallOptions &opts = { })
     {
         std::unique_ptr<QGrpcCallReply> reply = m_client.UnaryCall(request, opts);
         const auto *replyPtr = reply.get();
         connect(
             replyPtr, &QGrpcCallReply::finished, replyPtr,
             [reply = std::move(reply)](const QGrpcStatus &status) {
                 if (status.isOk()) {
                     if (const auto response = reply->read<guide::Response>())
                         qDebug() << "Client (UnaryCall) finished, received:" << *response;
                     else
                         qDebug("Client (UnaryCall) deserialization failed");
                 } else {
                     qDebug() << "Client (UnaryCall) failed:" << status;
                 }
             },
             Qt::SingleShotConnection);
     }

The function starts the RPC by invoking the `UnaryCall` member function of the generated client interface `m_client`. The lifetime is solely managed by the finished signal.

Running the code

In `main`, we simply invoke this function three times, letting the second invocation fail:

     clientGuide.unaryCall(ClientGuide::createRequest(1));
     clientGuide.unaryCall(ClientGuide::createRequest(2, ExpectedResult::Failure));
     clientGuide.unaryCall(ClientGuide::createRequest(3));

A possible output of running this could look like the following:

     Welcome to the clientguide!
     Starting the server process ...
         Server listening on: localhost:50056
         Server (UnaryCall): Request( time: 1733498584776, num: 1 )
         Server (UnaryCall): Request( time: 9223372036854775807, num: 2 )
         Server (UnaryCall): Request( time: 1733498584776, num: 3 )
     Client (UnaryCall) finished, received: Response( time:  1733498584778257 , num:  1  )
     Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::InvalidArgument, message: "Request time is in the future!" )
     Client (UnaryCall) finished, received: Response( time:  1733498584778409 , num:  3  )

We see the server receiving the three messages, with the second containing a large value for its time. On the client side, the first and last calls returned an Ok status code, but the second message failed with the InvalidArgument status code due to the message time being in the future.

##### Server Streaming

In a server stream, the client sends an initial request, and the server responds with one or more messages. In addition to the finished signal you also have to handle the messageReceived signal.

In this example, we use the single-shot paradigm to manage the streaming RPC lifecycle. Ensure you carefully read the Single Shot RPCs section.

As with any RPC, we connect to the finished signal first:

     void serverStreaming(const guide::Request &initialRequest)
     {
         std::unique_ptr<QGrpcServerStream> stream = m_client.ServerStreaming(initialRequest);
         const auto *streamPtr = stream.get();

         connect(
             streamPtr, &QGrpcServerStream::finished, streamPtr,
             [stream = std::move(stream)](const QGrpcStatus &status) {
                 if (status.isOk())
                     qDebug("Client (ServerStreaming) finished");
                 else
                     qDebug() << "Client (ServerStreaming) failed:" << status;
             },
             Qt::SingleShotConnection);

To handle the server messages, we connect to the messageReceived signal and read the response when the signal is emitted.

         connect(streamPtr, &QGrpcServerStream::messageReceived, streamPtr, [streamPtr] {
             if (const auto response = streamPtr->read<guide::Response>())
                 qDebug() << "Client (ServerStream) received:" << *response;
             else
                 qDebug("Client (ServerStream) deserialization failed");
         });
     }

Running the code

The server logic streams back the amount received in the initial request to the client. We create such a request and invoke the function.

     clientGuide.serverStreaming(ClientGuide::createRequest(3));

A possible output of running the server streaming could look like this:

     Welcome to the clientguide!
     Starting the server process ...
         Server listening on: localhost:50056
         Server (ServerStreaming): Request( time: 1733504435800, num: 3 )
     Client (ServerStream) received: Response( time:  1733504435801724 , num:  0  )
     Client (ServerStream) received: Response( time:  1733504435801871 , num:  1  )
     Client (ServerStream) received: Response( time:  1733504435801913 , num:  2  )
     Client (ServerStreaming) finished

Once the server starts, it receives a request with a num value of 3 and responds with three `Response` messages before completing the communication.

##### Client Streaming

In a client stream, the client sends one or more requests, and the server responds with a single final response. The finished signal must be handled, and messages can be sent using the writeMessage function. The writesDone function can then be used to indicate that the client has finished writing and that no more messages will be sent.

We use a class-based approach to interact with the streaming RPC, incorporating the handler as a member of the class. As with any RPC, we connect to the finished signal:

     void clientStreaming(const guide::Request &initialRequest)
     {
         m_clientStream = m_client.ClientStreaming(initialRequest);
         for (int32_t i = 1; i < 3; ++i)
             m_clientStream->writeMessage(createRequest(initialRequest.num() + i));
         m_clientStream->writesDone();

         connect(m_clientStream.get(), &QGrpcClientStream::finished, m_clientStream.get(),
             [this](const QGrpcStatus &status) {
                 if (status.isOk()) {
                     if (const auto response = m_clientStream->read<guide::Response>()) {
                         qDebug() << "Client (ClientStreaming) finished, received:"
                                  << *response;
                     }
                     m_clientStream.reset();
                 } else {
                     qDebug() << "Client (ClientStreaming) failed:" << status;
                     qDebug("Restarting the client stream");
                     clientStreaming(createRequest(0));
                 }
             });
     }

The function starts the client stream with an initial message. Then it continues to write two additional messages before signaling the end of communication by calling writesDone. If the streaming RPC succeeds, we read the final response from the server and `reset` the RPC object. If the RPC fails, we retry by invoking the same function, which overwrites the `m_clientStream` member and reconnects the finished signal. We cannot simply reassign the `m_clientStream` member within the lambda, as this would lose the necessary connection.

Running the code

In `main`, we invoke the `clientStreaming` function with a failing message, triggering an RPC failure and executing the retry logic.

     clientGuide.clientStreaming(ClientGuide::createRequest(0, ExpectedResult::Failure));

A possible output of running the client streaming could look like this:

     Welcome to the clientguide!
     Starting the server process ...
         Server listening on: localhost:50056
         Server (ClientStreaming): Request( time: 9223372036854775807, num: 0 )
     Client (ClientStreaming) failed: QGrpcStatus( code: QtGrpc::StatusCode::InvalidArgument, message: "Request time is in the future!" )
     Restarting the client stream
         Server (ClientStreaming): Request( time: 1733912946696, num: 0 )
         Server (ClientStreaming): Request( time: 1733912946697, num: 1 )
         Server (ClientStreaming): Request( time: 1733912946697, num: 2 )
     Client (ClientStreaming) finished, received: Response( time:  1733912946696922 , num:  3  )

The server receives an initial message that causes the RPC to fail, triggering the retry logic. The retry starts the RPC with a valid message, after which three messages are sent to the server before completing gracefully.

##### Bidirectional Streaming

Bidirectional streaming offers the most flexibility, allowing both the client and server to send and receive messages simultaneously. It requires the finished and messageReceived signal to be handled and provides the write functionality through writeMessage.

We use a class-based approach with member function slot connections to demonstrate the functionality, incorporating the handler as a member of the class. Additionally, we utilize the pointer-based read function. The two members used are:

     std::unique_ptr<QGrpcBidiStream> m_bidiStream;
     guide::Response m_bidiResponse;

We create a function to start the bidirectional streaming from an initial message and connect the slot functions to the respective finished and messageReceived signals.

     void bidirectionalStreaming(const guide::Request &initialRequest)
     {
         m_bidiStream = m_client.BidirectionalStreaming(initialRequest);
         connect(m_bidiStream.get(), &QGrpcBidiStream::finished, this, &ClientGuide::bidiFinished);
         connect(m_bidiStream.get(), &QGrpcBidiStream::messageReceived, this,
                 &ClientGuide::bidiMessageReceived);
     }

The slot functionality is straightforward. The finished slot simply prints and resets the RPC object:

     void bidiFinished(const QGrpcStatus &status)
     {
         if (status.isOk())
             qDebug("Client (BidirectionalStreaming) finished");
         else
             qDebug() << "Client (BidirectionalStreaming) failed:" << status;
         m_bidiStream.reset();
     }

The messageReceived slot reads into the `m_bidiResponse` member, continuing to write messages until the received response number hits zero. At that point, we half-close the client-side communication using writesDone.

     void bidiMessageReceived()
     {
         if (m_bidiStream->read(&m_bidiResponse)) {
             qDebug() << "Client (BidirectionalStreaming) received:" << m_bidiResponse;
             if (m_bidiResponse.num() > 0) {
                 m_bidiStream->writeMessage(createRequest(m_bidiResponse.num() - 1));
                 return;
             }
         } else {
             qDebug("Client (BidirectionalStreaming) deserialization failed");
         }
         m_bidiStream->writesDone();
     }

Running the code

The server logic simply returns a message as soon as it reads something, creating a response with the number from the request. In `main`, we create such a request, which ultimately serves as a counter.

     clientGuide.bidirectionalStreaming(ClientGuide::createRequest(3));

A possible output of running the bidirectional streaming could look like this:

     Welcome to the clientguide!
     Starting the server process ...
         Server listening on: localhost:50056
         Server (BidirectionalStreaming): Request( time: 1733503832107, num: 3 )
     Client (BidirectionalStreaming) received: Response( time:  1733503832108708 , num:  3  )
         Server (BidirectionalStreaming): Request( time: 1733503832109, num: 2 )
     Client (BidirectionalStreaming) received: Response( time:  1733503832109024 , num:  2  )
         Server (BidirectionalStreaming): Request( time: 1733503832109, num: 1 )
     Client (BidirectionalStreaming) received: Response( time:  1733503832109305 , num:  1  )
         Server (BidirectionalStreaming): Request( time: 1733503832109, num: 0 )
     Client (BidirectionalStreaming) received: Response( time:  1733503832109529 , num:  0  )
     Client (BidirectionalStreaming) finished

#### ClientGuide Interceptors

For an introduction to the interception mechanism and how interceptor interfaces participate in the lifecycle of an RPC, see the Qt GRPC Interceptors Overview.

The client guide example demonstrates three interceptors combined in a single chain:

- A metrics interceptor that records RPC statistics and injects a trace id.
- An authentication interceptor that attaches a bearer token.
- A sanitizing interceptor that validates and normalizes messages.

Together they demonstrate how interceptors can observe, modify, and validate RPC traffic at different stages of the call lifecycle.

##### Metrics Interceptor

The metrics interceptor is placed first in the chain so it can observe the entire RPC lifecycle. It records basic statistics such as received payload sizes, RPC durations, and success or failure counts for each RPC method.

     class MetricsInterceptor final : public QGrpcStartInterceptor,
                                      public QGrpcMessageReceivedInterceptor,
                                      public QGrpcFinishedInterceptor
     {
     public:
         ~MetricsInterceptor() override;

         Continuation onStart(QGrpcInterceptionContext &context,
                              QProtobufMessage &message,
                              QGrpcCallOptions &callOptions) override;

         void onMessageReceived(QGrpcInterceptionContext &context,
                                QByteArray &messageData) override;

         void onFinished(QGrpcInterceptionContext &context,
                         QGrpcStatus &status) override;

     private:
         using Clock = std::chrono::steady_clock;
         using Ms = std::chrono::duration<double, std::milli>;

         struct RpcMetrics {
             quint64 bytesReceived = 0;
             quint64 completed = 0;
             quint64 failed = 0;
             QList<double> durations;
         };

         QHash<quint64, Clock::time_point> m_activeRPCs;
         QMap<QtGrpc::RpcDescriptor, RpcMetrics> m_metrics;
     };

Metrics Interceptor Implementation

When an RPC starts, the interceptor records the current timestamp and stores it using the unique operationId as a key. A trace identifier is also added to the outgoing metadata so the call can be correlated across services.

     QGrpcStartInterceptor::Continuation
     MetricsInterceptor::onStart(QGrpcInterceptionContext &context,
                                 QProtobufMessage & /*message*/,
                                 QGrpcCallOptions &callOptions)
     {
         m_activeRPCs.insert(context.operationId(), std::chrono::steady_clock::now());
         callOptions.addMetadata("x-trace-id"_L1, QUuid::createUuid().toByteArray());

         return Continuation::Proceed;
     }

     void MetricsInterceptor::onMessageReceived(QGrpcInterceptionContext &context,
                                                QByteArray &messageData)
     {
         m_metrics[context.descriptor()].bytesReceived += messageData.size();
     }

     void MetricsInterceptor::onFinished(QGrpcInterceptionContext &context,
                                         QGrpcStatus &status)
     {
         const auto it = m_activeRPCs.find(context.operationId());
         Q_ASSERT(it != m_activeRPCs.cend());

         const auto duration = Ms(Clock::now() - it.value()).count();
         m_activeRPCs.erase(it);

         auto &metrics = m_metrics[context.descriptor()];
         if (!status.isOk()) {
             ++metrics.failed;
             return;
         }
         ++metrics.completed;
         metrics.durations.append(duration);
     }

While the RPC is active, incoming messages contribute to the collected metrics by accumulating the number of received payload bytes.

When the RPC finishes, the interceptor computes the total duration and updates aggregated statistics per RPC descriptor.

For demonstration purposes, the collected metrics are printed when the interceptor instance is destroyed.

     MetricsInterceptor::~MetricsInterceptor()
     {
         Q_ASSERT(m_activeRPCs.isEmpty());
         const auto keys = m_metrics.keys();
         for (const auto &k : keys) {
             qInfo() << "Metrics for:" << k;
             const auto &m = m_metrics[k];
             qInfo() << "  Bytes received    :" << m.bytesReceived;
             qInfo() << "  Failed RPCs       :" << m.failed;
             qInfo() << "  Completed RPCs    :" << m.completed;
             qInfo() << "  Completed Durations (ms):" << m.durations;
         }
     }

##### Authentication Interceptor

The authentication interceptor attaches a bearer token to outgoing RPC calls. If the application already provides an `authorization` header, the interceptor leaves it unchanged.

If the server reports an authentication failure, the interceptor refreshes the token so that subsequent RPC calls use updated credentials.

     class TokenProvider
     {
     public:
         TokenProvider();
         QByteArray token();
         void refresh();

     private:
         QByteArray m_token;
     };

     class AuthInterceptor final : public QGrpcStartInterceptor,
                                   public QGrpcTrailingMetadataInterceptor,
                                   public QGrpcFinishedInterceptor
     {
     public:
         Continuation onStart(QGrpcInterceptionContext &context,
                              QProtobufMessage &message,
                              QGrpcCallOptions &callOptions) override;

         void onTrailingMetadata(QGrpcInterceptionContext &context,
                                 QMultiHash<QByteArray, QByteArray> &metadata) override;

         void onFinished(QGrpcInterceptionContext &context,
                         QGrpcStatus &status) override;

     private:
         TokenProvider m_tokenProvider;
         QByteArray m_serverAuthHint;
     };

Auth Interceptor Implementation

The `TokenProvider` encapsulates token management. In this example, it generates a simplified token string for demonstration purposes only. It does not represent a real authentication token. In real applications, tokens would typically be obtained and refreshed through an external identity system responsible for issuing and managing authentication tokens.

     TokenProvider::TokenProvider() { refresh(); }
     QByteArray TokenProvider::token() { return m_token; }
     void TokenProvider::refresh()
     {
         // NOTE: This is NOT a real JWT or secure token.
         // It only mimics the structure: header.payload.signature
         // The payload is just a timestamp used for expiry checks.
         auto header = R"({"alg":"none","typ":"JWT"})"_ba; // fake header
         auto payload = QByteArray::number(now()); // fake "iat"
         auto signature = "no_signature"_ba; // fake signing

         m_token = header + '.' + payload + '.' + signature;
     }

On the server side, incoming calls validate the `authorization` metadata. If validation fails, the server returns an `Unauthenticated` status and provides additional information in trailing metadata.

     if (auto authStatus = validateAuth(context); !authStatus.ok())
         return authStatus;

When an RPC starts, the interceptor checks whether the call already contains an `authorization` header. If not, it attaches a bearer token to the outgoing metadata.

Trailing metadata is inspected for authentication hints returned by the server. If present, the hint is stored so it can be reported if the call ultimately fails with an authentication error.

When the RPC finishes with an `Unauthenticated` status, the interceptor refreshes the token. In this example, the refresh simply regenerates a token with a new current time. In real-world scenarios, this would involve requesting a new token from an external identity system.

     QGrpcStartInterceptor::Continuation
     AuthInterceptor::onStart(QGrpcInterceptionContext & /*context*/,
                              QProtobufMessage & /*messasge*/,
                              QGrpcCallOptions &callOptions)
     {
         constexpr QByteArrayView AuthKey("authorization");

         if (!callOptions.metadata(QtGrpc::MultiValue).contains(AuthKey))
             callOptions.addMetadata(AuthKey, "Bearer " + m_tokenProvider.token());

         return Continuation::Proceed;
     }

     void AuthInterceptor::onTrailingMetadata(QGrpcInterceptionContext & /*context*/,
                                              QMultiHash<QByteArray, QByteArray> &metadata)
     {
         constexpr QByteArrayView AuthErrorKey("www-authenticate");
         const auto it = metadata.constFind(AuthErrorKey);
         if (it == metadata.cend())
             return;
         m_serverAuthHint = it.value();
     }

     void AuthInterceptor::onFinished(QGrpcInterceptionContext & /*context*/, QGrpcStatus &status)
     {
         // Only handle Unauthenticated status codes.
         if (status.code() != QtGrpc::StatusCode::Unauthenticated)
             return;

         qDebug("[AuthInterceptor] Refreshing token for next call");
         // NOTE: Simplified for example. Real tokens are obtained via proper auth flow.
         m_tokenProvider.refresh();
         if (!m_serverAuthHint.isEmpty()) {
             status = QGrpcStatus{ status.code(), status.message() + u", Hint: "_s + m_serverAuthHint };
             m_serverAuthHint.clear();
         }
     }

##### Sanitizing Interceptor

The sanitizing interceptor validates and normalizes request and response data. It demonstrates how interceptors can enforce basic client-side constraints before messages are sent and after responses are received.

In this example the interceptor clamps request values to acceptable ranges, normalizes timestamps, and rejects calls whose metadata exceeds a predefined size limit.

     class SanitizingInterceptor final : public QGrpcStartInterceptor,
                                         public QGrpcWriteMessageInterceptor,
                                         public QGrpcMessageReceivedInterceptor
     {
     public:
         Continuation onStart(QGrpcInterceptionContext &context,
                              QProtobufMessage &message,
                              QGrpcCallOptions &callOptions) override;

         void onWriteMessage(QGrpcInterceptionContext &context,
                             QProtobufMessage &message) override;

         void onMessageReceived(QGrpcInterceptionContext &context,
                                QByteArray &messageData) override;
     };

Sanitizing Interceptor Implementation

Helper functions are used to calculate the total metadata size and to apply basic normalization rules to request and response messages.

     qsizetype getTotalBytes(const QMultiHash<QByteArray, QByteArray> &md)
     {
         qsizetype total = 0;
         for (const auto &[k, v] : md.asKeyValueRange())
             total += k.size() + v.size();
         return total;
     }

     template <typename T>
     void sanitizeMessage(T &request)
     {
         const auto num = request.num();
         if (num < 0 || num > 5)
             request.setNum(qBound(0, num, 5));

         const auto time = request.time();
         const auto currentTime = now();
         if (time < 0 || time > currentTime)
             request.setTime(qMax(0, currentTime));
     }

When an RPC starts, the interceptor first verifies that the outgoing metadata does not exceed the allowed size. If the limit is exceeded, the call is aborted by returning Continuation::Drop

If the request message matches the expected type, its fields are sanitized before the RPC is sent.

For streaming calls, outgoing messages are also sanitized in the `onWriteMessage` hook.

Finally, incoming responses are intercepted. The serialized payload is deserialized, normalized, and serialized again before it is delivered to the application.

     QGrpcStartInterceptor::Continuation
     SanitizingInterceptor::onStart(QGrpcInterceptionContext &context, QProtobufMessage &message,
                                    QGrpcCallOptions &callOptions)
     {
         constexpr quint16 MaxMetadata = 1024 * 8; // 8 KiB
         auto totalMdBytes = getTotalBytes(callOptions.metadata(QtGrpc::MultiValue));
         if (totalMdBytes > MaxMetadata) {
             qDebug() << "[Sanitizing] Aborting call with metadata size of" << totalMdBytes << "bytes";
             return Continuation::Drop;
         }

         onWriteMessage(context, message);

         return Continuation::Proceed;
     }

     void SanitizingInterceptor::onWriteMessage(QGrpcInterceptionContext & /*context*/,
                                                QProtobufMessage &message)
     {
         // For handling more complex scenarios with various Request message types,
         // it would make sense to branch on the context.descriptor() appropriately.
         if (auto *request = qprotobufmessage_cast<client::guide::Request *>(&message))
             sanitizeMessage(*request);
     }

     void SanitizingInterceptor::onMessageReceived(QGrpcInterceptionContext &context,
                                                   QByteArray &messageData)
     {
         // messageData can only be of type client::guide::Response. For more
         // complex handling add appropriate handling with context.descriptor().
         // Note that the deserialization and serialization introduces overhead.
         client::guide::Response response;
         auto serializer = context.channel().serializer();
         if (serializer->deserialize(&response, messageData))
             return;
         sanitizeMessage(response);
         messageData = serializer->serialize(&response);
     }

##### Interceptor Usage

Construct the interceptor chain in a helper function and return it to the caller.

     inline QGrpcInterceptorChain createInterceptors()
     {
         QGrpcInterceptorChain chain;
         chain.set(
             std::make_unique<MetricsInterceptor>(),
             std::make_unique<AuthInterceptor>(),
             std::make_unique<SanitizingInterceptor>()
         );
         return chain;
     }

Before attaching the chain to the channel, verify that it was populated successfully (for example by checking `isEmpty()`) and apply an appropriate error-handling strategy if creation failed.

     auto interceptors = createInterceptors();
     if (interceptors.isEmpty()) {
         qWarning("Failed to create the interceptor chain");
         return EXIT_FAILURE; // or some other suitable fallback
     }
     channel = std::make_shared<QGrpcHttp2Channel>(
         QUrl("http://localhost:50056"),
         std::move(interceptors)
     );

For unary calls, the example schedules a sequence of requests with a short delay between them. This makes it easier to observe how the interceptors affect successive calls: trace metadata is added, authentication failures refresh the token for later requests, and invalid metadata can cause a call to be dropped before it is sent.

     int delayMs = 150;
     for (int i = 1; i < 6; ++i) {
         QTimer::singleShot(delayMs, [&, i] {
             auto expected = i % 2 == 0 ? ExpectedResult::Failure : ExpectedResult::Success;
             clientGuide.unaryCall(ClientGuide::createRequest(i, expected));
         });
         delayMs += 150;
     }
     QTimer::singleShot(delayMs, [&] {
         QGrpcCallOptions invalidOpts;
         invalidOpts.addMetadata("huge_key"_ba, "huge_value"_ba.repeated(8'000));
         clientGuide.unaryCall(guide::Request{ }, invalidOpts); // this call will fail.
     });
     delayMs += 150;

The example also performs a bidirectional streaming RPC. The `bidirectionalStreaming()` implementation would normally exchange `666` messages, but the sanitizing interceptor clamps the request values so that the stream terminates after five exchanges.

     QTimer::singleShot(delayMs, [&] {
         clientGuide.bidirectionalStreaming(ClientGuide::createRequest(666));
     });

Running the code

Run `clientguide_client` with the `-I` option to enable interceptor support. A possible output is shown below:

     Welcome to the clientguide!
     Running with Interceptor support
     Starting the server process ...
         Server listening on: localhost:50056
         Server (UnaryCall): Request( time: 1774360832277, num: 1 ), Metadata: { x-trace-id: {360bb041-9ab5-4940-95c1-0d4c89491570} }
     Client (UnaryCall) finished, received: Response( time: 1774360832278, num: 1 )
         Server (UnaryCall): Request( time: 1774360832427, num: 2 ), Metadata: { x-trace-id: {01817a8f-9f0e-404e-a21c-767f81e74a2d} }
     Client (UnaryCall) finished, received: Response( time: 1774360832429, num: 2 )
         Server (UnaryCall): Request( time: 1774360832577, num: 3 ), Metadata: { x-trace-id: {78cb336f-0396-4bbc-b1ee-59914651feef} }
     Client (UnaryCall) finished, received: Response( time: 1774360832578, num: 3 )
     [AuthInterceptor] Refreshing token for next call
     Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::Unauthenticated, message: "Token expired with age: 603 ms, Hint: Bearer realm=\"clientguide\", error=\"invalid_token\"" )
         Server (UnaryCall): Request( time: 1774360832876, num: 5 ), Metadata: { x-trace-id: {63a40327-2f46-43c4-9e2c-44b67f297bd2} }
     Client (UnaryCall) finished, received: Response( time: 1774360832877, num: 5 )
     [Sanitizing] Aborting call with metadata size of 80129 bytes
     Client (UnaryCall) failed: QGrpcStatus( code: QtGrpc::StatusCode::Aborted, message: "Interceptors dropped the call" )
         Server (BidirectionalStreaming) accepted call, Metadata: { x-trace-id: {fc945bdb-5c8a-4c65-8bfb-8655800d5774} }
         Server (BidirectionalStreaming): Request( time: 1774360833177, num: 5 )
     Client (BidirectionalStreaming) received: Response( time: 1774360833178, num: 5 )
         Server (BidirectionalStreaming): Request( time: 1774360833178, num: 4 )
     Client (BidirectionalStreaming) received: Response( time: 1774360833179, num: 4 )
         Server (BidirectionalStreaming): Request( time: 1774360833179, num: 3 )
     Client (BidirectionalStreaming) received: Response( time: 1774360833179, num: 3 )
         Server (BidirectionalStreaming): Request( time: 1774360833179, num: 2 )
     Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 2 )
         Server (BidirectionalStreaming): Request( time: 1774360833180, num: 1 )
     Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 1 )
         Server (BidirectionalStreaming): Request( time: 1774360833180, num: 0 )
     Client (BidirectionalStreaming) received: Response( time: 1774360833180, num: 0 )
     Client (BidirectionalStreaming) finished
     All operations completed! Automatically shutting down...
     Metrics for: QtGrpc::RpcDescriptor( service: "client.guide.ClientGuideService", method: "BidirectionalStreaming", type: QtGrpc::RpcType::BidiStreaming )
     Bytes received    : 52
     Failed RPCs       : 0
     Completed RPCs    : 1
     Completed Durations (ms): QList(3.97233)
     Metrics for: QtGrpc::RpcDescriptor( service: "client.guide.ClientGuideService", method: "UnaryCall", type: QtGrpc::RpcType::UnaryCall )
     Bytes received    : 36
     Failed RPCs       : 2
     Completed RPCs    : 4
     Completed Durations (ms): QList(1.937, 1.90971, 1.624, 1.50429)

The output shows the interceptor chain in action. Outgoing unary calls reach the server with trace metadata added by the metrics interceptor. When the server reports an authentication failure, the authentication interceptor refreshes the token for subsequent calls and augments the error message with the server-provided hint. A later call is rejected locally by the sanitizing interceptor because its metadata exceeds the configured limit. The bidirectional streaming call shows that interceptors also apply to streaming RPCs: the sanitizing interceptor clamps the request value, so the stream terminates after five message exchanges instead of continuing from `666`. Finally, when the client shuts down, the metrics interceptor prints the collected statistics for each RPC method.

Example project @ code.qt.io