Revision control
++ // Copyright (C) 2024 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
++
++ /*!
++ \example clientguide
++ \title Qt GRPC Client Guide
++ \ingroup qtgrpc-examples
++ \examplecategory {Networking}
++ \meta tag {network,protobuf,grpc,serialization,overview}
++ \brief The Qt GRPC client guide.
++
++ \section1 Service Methods
++
++ In \gRPC, service methods can be defined in a protobuf schema to specify the
++ communication between clients and servers. The protobuf compiler,
++ \c{protoc}, can then generate the required server and client interfaces
++ based on these definitions. \gRPC supports four types of service methods:
++
++ \list
++ \li \l{Unary Calls} — The client sends a single request and receives a
++ single response.
++ \badcode
++ rpc UnaryCall (Request) returns (Response);
++ \endcode
++ The corresponding client handler is QGrpcCallReply.
++ \li \l{Server Streaming} — The client sends a single request and
++ receives multiple responses.
++ \badcode
++ rpc ServerStreaming (Request) returns (stream Response);
++ \endcode
++ The corresponding client handler is QGrpcServerStream.
++ \li \l{Client Streaming} — The client sends multiple requests and
++ receives a single response.
++ \badcode
++ rpc ClientStreaming (stream Request) returns (Response);
++ \endcode
++ The corresponding client handler is QGrpcClientStream.
++ \li \l{Bidirectional Streaming} — The client and server exchange
++ multiple messages.
++ \badcode
++ rpc BidirectionalStreaming (stream Request) returns (stream Response);
++ \endcode
++ The corresponding client handler is QGrpcBidiStream.
++ \endlist
++
++ \gRPC communication always starts with the client, which initiates the
++ \l{https://en.wikipedia.org/wiki/Remote_procedure_call}{remote procedure
++ call} (RPC) by sending the first message to the server. The server then
++ concludes any type of communication by returning a \l{QtGrpc::}
++ {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 \l{Signals & Slots} mechanism.
++
++ A key signal common to all RPC handlers is \l{QGrpcOperation::} {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 \l{QGrpcServerStream::}
++ {messageReceived} for incoming messages, \l{QGrpcClientStream::}
++ {writeMessage} for sending messages to the server, and
++ \l{QGrpcBidiStream::} {writesDone} for closing client-side communication.
++ The table below outlines the supported functionality of the RPC client
++ handlers:
++
++ \table 85 %
++ \header
++ \li Functionality
++ \li QGrpcCallReply
++ \li QGrpcServerStream
++ \li QGrpcClientStream
++ \li QGrpcBidiStream
++ \row
++ \li \l {QGrpcOperation::}{finished}
++ \li ✓ (\l{QGrpcOperation::}{read} final response)
++ \li ✓
++ \li ✓ (\l{QGrpcOperation::}{read} final response)
++ \li ✓
++ \row
++ \li \l {QGrpcServerStream::}{messageReceived}
++ \li ✗
++ \li ✓
++ \li ✗
++ \li ✓
++ \row
++ \li \l {QGrpcClientStream::}{writeMessage}
++ \li ✗
++ \li ✗
++ \li ✓
++ \li ✓
++ \row
++ \li \l {QGrpcBidiStream::}{writesDone}
++ \li ✗
++ \li ✗
++ \li ✓
++ \li ✓
++ \endtable
++
++ \section1 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 \c {clientguide.proto} file as
++ an example:
++
++ \snippet clientguide/proto/clientguide.proto 0
++
++ To use this \e {.proto} file for our Qt GRPC client in C++, we must run
++ the \c protoc compiler with the Qt generator plugins on it. Fortunately, Qt
++ provides the \l{qt_add_grpc} and \l{qt_add_protobuf} CMake functions to
++ streamline this process.
++
++ \snippet clientguide/client/CMakeLists.txt 0
++
++ This results in two header files being generated in the current build
++ directory:
++ \list
++ \li \e {clientguide.qpb.h}: Generated by \l{The qtprotobufgen
++ Tool}{qtprotobufgen}. Declares the \c Request and \c Response
++ protobuf messages from the schema.
++ \li \e {clientguide_client.grpc.qpb.h}: Generated by \l{The qtgrpcgen
++ Tool}{qtgrpcgen}. Declares the client interface for calling the
++ methods of a \gRPC server implementing the \c ClientGuideService
++ from the schema.
++ \endlist
++
++ The following client interface is generated:
++
++ \code
++ 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
++ \endcode
++
++ \include qtgrpc-shared.qdocinc rpc-lifetime-note
++
++ \section2 Server Setup
++
++ The server implementation for the \c ClientGuideService follows a
++ straightforward approach. It validates the request message's \c time field,
++ returning the \c INVALID_ARGUMENT status code if the time is in the future:
++
++ \snippet clientguide/server/main.cpp time
++
++ Additionally, the server sets the current time in every response message:
++
++ \snippet clientguide/server/main.cpp response
++
++ For valid \c time requests, the service methods behave as follows:
++
++ \list
++ \li \c{UnaryCall}: Responds with the \c num field from the request.
++ \li \c{ServerStreaming}: Sends \c num responses matching the request
++ message.
++ \li \c{ClientStreaming}: Counts the number of request messages and sets
++ this count as \c num.
++ \li \c{BidirectionalStreaming}: Immediately responds with the \c num
++ field from each incoming request message.
++ \endlist
++
++ \section2 Client Setup
++
++ We begin by including the generated header files:
++
++ \snippet clientguide/client/main.cpp gen-includes
++
++ For this example, we create the \c ClientGuide class to manage all
++ communication, making it easier to follow. We begin by setting up the
++ backbone of all \gRPC communication: a channel.
++
++ \snippet clientguide/client/main.cpp basic-0a
++ \snippet clientguide/client/main.cpp basic-0b
++
++ The Qt GRPC library offers QGrpcHttp2Channel, which you can
++ \l{QGrpcClientBase::attachChannel} {attach} to the generated
++ client interface:
++
++ \snippet clientguide/client/main.cpp basic-1
++
++ 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).
++
++ \section3 Creating a request message
++
++ Here's a simple wrapper to create request messages:
++
++ \snippet clientguide/client/main.cpp basic-2
++
++ This function takes an integer and an optional boolean. By default its
++ messages use the current time, so the \l{Server Setup}{server logic} should
++ accept them. When called with \c fail set to \c true, however, it produces
++ messages that the server shall reject.
++
++ \section2 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 \l {QGrpcOperation::} {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.
++
++ \code
++ 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
++ );
++ \endcode
++
++ \list
++ \li \b{1}: Since we manage the lifetime of the unique RPC object within
++ the lambda, moving it into the lambda's capture would invalidate \c
++ {get()} and other member functions. Therefore, we must copy the
++ pointers address before moving it.
++ \li \b{2}: The \l{QGrpcOperation::}{finished} signal is emitted only
++ once, making this a true single-shot connection. It is \b{important}
++ to mark this connection as \l{Qt::}{SingleShotConnection}! If not,
++ the capture of \c reply will not be destroyed, leading to a \b
++ {hidden memory leak} that is hard to discover.
++ \endlist
++
++ The \l{Qt::}{SingleShotConnection} argument in the \c{connect} call ensures
++ that the slot functor (the lambda) is destroyed after being emitted,
++ freeing the resources associated with the slot, including its captures.
++
++ \section1 Remote Procedure Calls
++
++ \section2 Unary Calls
++
++ Unary calls require only the \l {QGrpcOperation::} {finished} signal to be
++ handled. When this signal is emitted, we can check the \l {QGrpcStatus}
++ {status} of the RPC to determine if it was successful. If it was, we can \l
++ {QGrpcOperation::} {read} the single and final response from the server.
++
++ In this example, we use the single-shot paradigm. Ensure you carefully read
++ the \l {Single Shot RPCs} section.
++
++ \snippet clientguide/client/main.cpp unary-0
++
++ The function starts the RPC by invoking the \c UnaryCall member function
++ of the generated client interface \c m_client. The lifetime is solely
++ managed by the \l{QGrpcCallReply::} {finished} signal.
++
++ \details {Running the code}
++ In \c main, we simply invoke this function three times, letting the
++ second invocation fail:
++
++ \snippet clientguide/client/main.cpp unary-1
++
++ A possible output of running this could look like the following:
++
++ \badcode
++ 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 )
++ \endcode
++
++ 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 \l {QtGrpc::StatusCode::} {Ok} status code,
++ but the second message failed with the \l {QtGrpc::StatusCode::}
++ {InvalidArgument} status code due to the message time being in the future.
++ \enddetails
++
++ \section2 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 \l{QGrpcOperation::}
++ {finished} signal you also have to handle the \l{QGrpcServerStream::}
++ {messageReceived} signal.
++
++ In this example, we use the single-shot paradigm to manage the streaming RPC
++ lifecycle. Ensure you carefully read the \l {Single Shot RPCs} section.
++
++ As with any RPC, we connect to the \l{QGrpcOperation::} {finished} signal
++ first:
++
++ \snippet clientguide/client/main.cpp sstream-0
++
++ To handle the server messages, we connect to the \l{QGrpcServerStream::}
++ {messageReceived} signal and \l{QGrpcOperation::} {read} the response when
++ the signal is emitted.
++
++ \snippet clientguide/client/main.cpp sstream-1
++
++ \details {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.
++
++ \snippet clientguide/client/main.cpp sstream-2
++
++ A possible output of running the server streaming could look like this:
++
++ \badcode
++ 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
++ \endcode
++
++ Once the server starts, it receives a request with a \e num value of 3
++ and responds with three \c Response messages before completing the
++ communication.
++ \enddetails
++
++ \section2 Client Streaming
++
++ In a client stream, the client sends one or more requests, and the server
++ responds with a single final response. The \l {QGrpcOperation::} {finished}
++ signal must be handled, and messages can be sent using the
++ \l{QGrpcClientStream::} {writeMessage} function. The
++ \l{QGrpcClientStream::} {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 \l {QGrpcOperation::} {finished} signal:
++
++ \snippet clientguide/client/main.cpp cstream-0
++
++ The function starts the client stream with an initial message and then
++ paces its writes. Rather than enqueuing every message up front, it sends the
++ next message from the \l{QGrpcClientStream::}{messageWritten} handler, which
++ fires once the channel has passed the previous message to the transport and
++ so respects HTTP/2 flow control. It calls \l{QGrpcClientStream::}{writesDone}
++ once all messages have been sent. Pacing this way keeps the outgoing message
++ queue bounded even when the application produces messages faster than the
++ network drains them. If the streaming RPC succeeds, we
++ \l {QGrpcOperation::} {read} the final response from the server and \c reset
++ the RPC object. If the RPC fails, we retry by invoking the same function,
++ which overwrites the \c m_clientStream member and reconnects its signals. We
++ cannot simply reassign the \c m_clientStream member within the lambda, as
++ this would lose the necessary connections.
++
++ \details {Running the code}
++ In \c main, we invoke the \c clientStreaming function with a failing
++ message, triggering an RPC failure and executing the retry logic.
++
++ \snippet clientguide/client/main.cpp cstream-1
++
++ A possible output of running the client streaming could look like this:
++
++ \badcode
++ 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 )
++ \endcode
++
++ 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.
++ \enddetails
++
++ \section2 Bidirectional Streaming
++
++ Bidirectional streaming offers the most flexibility, allowing both the
++ client and server to send and receive messages simultaneously. It requires
++ the \l {QGrpcOperation::} {finished} and \l {QGrpcBidiStream::}
++ {messageReceived} signal to be handled and provides the write functionality
++ through \l {QGrpcBidiStream::} {writeMessage}.
++
++ We use a class-based approach with member function \e slot connections to
++ demonstrate the functionality, incorporating the handler as a member of the
++ class. Additionally, we utilize the pointer-based \l {QGrpcOperation::}
++ {read} function. The two members used are:
++
++ \snippet clientguide/client/main.cpp bstream-0
++
++ We create a function to start the bidirectional streaming from an initial
++ message and connect the slot functions to the respective \l
++ {QGrpcOperation::} {finished} and \l {QGrpcBidiStream::} {messageReceived}
++ signals.
++
++ \snippet clientguide/client/main.cpp bstream-1
++
++ The slot functionality is straightforward. The \l {QGrpcOperation::}
++ {finished} slot simply prints and resets the RPC object:
++
++ \snippet clientguide/client/main.cpp bstream-2
++
++ The \l {QGrpcBidiStream::} {messageReceived} slot \l {QGrpcOperation::}
++ {read}s into the \c 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 \l {QGrpcBidiStream::} {writesDone}.
++
++ \snippet clientguide/client/main.cpp bstream-3
++
++ \details {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 \c
++ main, we create such a request, which ultimately serves as a counter.
++
++ \snippet clientguide/client/main.cpp bstream-4
++
++ A possible output of running the bidirectional streaming could look
++ like this:
++
++ \badcode
++ 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
++ \endcode
++ \enddetails
++
++ \section1 ClientGuide Interceptors
++
++ \include qtgrpc-shared.qdocinc interceptors_overview_ref
++
++ The client guide example demonstrates three interceptors combined in a
++ single chain:
++
++ \list
++ \li A \e{metrics} interceptor that records RPC statistics and injects a trace id.
++ \li An \e{authentication} interceptor that attaches a bearer token.
++ \li A \e{sanitizing} interceptor that validates and normalizes messages.
++ \endlist
++
++ Together they demonstrate how interceptors can observe, modify, and
++ validate RPC traffic at different stages of the call lifecycle.
++
++ \section2 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.
++
++ \snippet clientguide/client/interceptors.h metrics-0
++
++ \details {Metrics Interceptor Implementation}
++ When an RPC starts, the interceptor records the current timestamp and
++ stores it using the unique \l{QGrpcOperationContext::}{operationId} as
++ a key. A trace identifier is also added to the outgoing metadata so the
++ call can be correlated across services.
++
++ \snippet clientguide/client/interceptors.cpp metrics-1
++
++ 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.
++
++ \snippet clientguide/client/interceptors.cpp metrics-2
++ \enddetails
++
++ \section2 Authentication Interceptor
++
++ The authentication interceptor attaches a bearer token to outgoing RPC
++ calls. If the application already provides an \c 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.
++
++ \snippet clientguide/client/interceptors.h auth-0
++
++ \details {Auth Interceptor Implementation}
++ The \c 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.
++
++ \snippet clientguide/client/interceptors.cpp auth-1
++
++ On the server side, incoming calls validate the \c authorization
++ metadata. If validation fails, the server returns an \c Unauthenticated
++ status and provides additional information in trailing metadata.
++
++ \snippet clientguide/server/main.cpp auth
++
++ When an RPC starts, the interceptor checks whether the call already
++ contains an \c 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 \c 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.
++
++ \snippet clientguide/client/interceptors.cpp auth-2
++ \enddetails
++
++ \section2 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.
++
++ \snippet clientguide/client/interceptors.h sanitizing-0
++
++ \details {Sanitizing Interceptor Implementation}
++ Helper functions are used to calculate the total metadata size and to
++ apply basic normalization rules to request and response messages.
++
++ \snippet clientguide/client/interceptors.cpp sanitizing-1
++
++ 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 \l{QGrpcStartInterceptor::}{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 \c
++ onWriteMessage hook.
++
++ Finally, incoming responses are intercepted. The serialized payload is
++ deserialized, normalized, and serialized again before it is delivered to
++ the application.
++
++ \snippet clientguide/client/interceptors.cpp sanitizing-2
++ \enddetails
++
++ \section2 Interceptor Usage
++
++ Construct the interceptor chain in a helper function and return it to the
++ caller.
++
++ \snippet clientguide/client/interceptors.h interceptorchain-0
++
++ Before attaching the chain to the channel, verify that it was populated
++ successfully (for example by checking \c isEmpty()) and apply an
++ appropriate error-handling strategy if creation failed.
++
++ \snippet clientguide/client/main.cpp interceptorchain-1
++
++ 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.
++
++ \snippet clientguide/client/main.cpp interceptors-0a
++ \snippet clientguide/client/main.cpp interceptors-0b
++
++ The example also performs a bidirectional streaming RPC. The
++ \c{bidirectionalStreaming()} implementation would normally exchange \c{666}
++ messages, but the sanitizing interceptor clamps the request values so that
++ the stream terminates after five exchanges.
++
++ \snippet clientguide/client/main.cpp interceptors-1
++
++ \details {Running the code}
++
++ Run \c clientguide_client with the \c{-I} option to enable interceptor
++ support. A possible output is shown below:
++
++ \badcode
++ 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)
++ \endcode
++
++ 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 \c 666. Finally, when the client shuts down, the metrics interceptor
++ prints the collected statistics for each RPC method.
++
++ \enddetails
++ */