1// Copyright (C) 2022 The Qt Company Ltd.
2// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>, Viktor Kopp <vifactor@gmail.com>
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
4// Qt-Security score:critical reason:network-protocol
5
6#include <QtGrpc/private/qtgrpclogging_p.h>
7#include <QtGrpc/qgrpccalloptions.h>
8#include <QtGrpc/qgrpcchanneloptions.h>
9#include <QtGrpc/qgrpchttp2channel.h>
10#include <QtGrpc/qgrpcoperationcontext.h>
11#include <QtGrpc/qgrpcserializationformat.h>
12#include <QtGrpc/qgrpcstatus.h>
13
14#include <QtProtobuf/qprotobufjsonserializer.h>
15#include <QtProtobuf/qprotobufserializer.h>
16
17#include <QtNetwork/private/hpack_p.h>
18#include <QtNetwork/private/http2protocol_p.h>
19#include <QtNetwork/private/qhttp2connection_p.h>
20#if QT_CONFIG(localserver)
21# include <QtNetwork/qlocalsocket.h>
22#endif
23#include <QtNetwork/qtcpsocket.h>
24#if QT_CONFIG(ssl)
25# include <QtNetwork/qsslsocket.h>
26#endif
27
28#include <QtCore/private/qnoncontiguousbytedevice_p.h>
29#include <QtCore/qalgorithms.h>
30#include <QtCore/qbytearray.h>
31#include <QtCore/qendian.h>
32#include <QtCore/qiodevice.h>
33#include <QtCore/qlist.h>
34#include <QtCore/qmetaobject.h>
35#include <QtCore/qpointer.h>
36#include <QtCore/qqueue.h>
37#include <QtCore/qtimer.h>
38#include <QtCore/qvarlengtharray.h>
39
40#include <QtCore/q20algorithm.h>
41
42#include <functional>
43#include <optional>
44#include <utility>
45
46QT_BEGIN_NAMESPACE
47
48using namespace Qt::Literals::StringLiterals;
49using namespace QtGrpc;
50
51/*!
52 \class QGrpcHttp2Channel
53 \inmodule QtGrpc
54 \brief The QGrpcHttp2Channel class provides a HTTP/2 transport layer
55 for \gRPC communication.
56
57 The QGrpcHttp2Channel class implements QAbstractGrpcChannel, enabling \gRPC
58 communication carried over \l{https://datatracker.ietf.org/doc/html/rfc7540}
59 {HTTP/2 framing}.
60
61 HTTP/2 introduces several advantages over its predecessor, HTTP/1.1, making
62 QGrpcHttp2Channel well-suited for high-performance, real-time applications
63 that require efficient communication, without sacrificing security or
64 reliability, by using multiplexed TCP connections.
65
66 The channel can be customized with \l{Secure Sockets Layer (SSL)
67 Classes}{SSL} support, a custom \l{QGrpcChannelOptions::}
68 {serializationFormat}, or other options by constructing it with a
69 QGrpcChannelOptions containing the required customizations.
70
71 \note \l{QGrpcChannelOptions::filterServerMetadata} is enabled by default.
72
73 \section2 Transportation scheme
74
75 The QGrpcHttp2Channel implementation prefers different transportation
76 methods based on the provided \c{hostUri}, \l{QUrl::}{scheme} and options.
77 The following criteria applies:
78
79 \table
80 \header
81 \li Scheme
82 \li Description
83 \li Default Port
84 \li Requirements
85 \li Example
86 \row
87 \li \c{http}
88 \li Unencrypted HTTP/2 over TCP
89 \li 80
90 \li None
91 \li \c{http://localhost}
92 \row
93 \li \c{https}
94 \li TLS-encrypted HTTP/2 over TCP
95 \li 443
96 \li QSslSocket support \b{AND} (scheme \b{OR} \l{QGrpcChannelOptions::}{sslConfiguration})
97 \li \c{https://localhost}
98 \row
99 \li \c{unix}
100 \li Unix domain socket in filesystem path
101 \li
102 \li QLocalSocket support \b{AND} scheme
103 \li \c{unix:///tmp/grpc.socket}
104 \row
105 \li \c{unix-abstract}
106 \li Unix domain socket in abstract namespace
107 \li
108 \li QLocalSocket support \b{AND}
109 \l{QLocalSocket::AbstractNamespaceOption}{AbstractNamespace}
110 support \b{AND} scheme
111 \li \c{unix-abstract:app_grpc_channel}
112 \endtable
113
114 \section2 Content-Type
115
116 The \e{content-type} in \gRPC over HTTP/2 determines the message
117 serialization format. It must start with \c{application/grpc} and can
118 include a suffix. The format follows this scheme:
119
120 \code
121 "content-type": "application/grpc" [("+proto" / "+json" / {custom})]
122 \endcode
123
124 For example:
125 \list
126 \li \c{application/grpc+proto} specifies Protobuf encoding.
127 \li \c{application/grpc+json} specifies JSON encoding.
128 \endlist
129
130 The serialization format can be configured either by specifying the \c
131 {content-type} inside the metadata or by setting the \l{QGrpcChannelOptions::}
132 {serializationFormat} directly. By default, the \c {application/grpc}
133 content-type is used.
134
135 To configure QGrpcHttp2Channel with the JSON serialization format using
136 \c {content-type} metadata:
137
138 \code
139 auto jsonChannel = std::make_shared<QGrpcHttp2Channel>(
140 QUrl("http://localhost:50051"_L1),
141 QGrpcChannelOptions().setMetadata({
142 { "content-type"_ba, "application/grpc+json"_ba },
143 })
144 );
145 \endcode
146
147 For a custom serializer and \c {content-type}, you can directly set the
148 serialization format:
149
150 \include qgrpcserializationformat.cpp custom-serializer-code
151
152 \code
153 auto dummyChannel = std::make_shared<QGrpcHttp2Channel>(
154 QUrl("http://localhost:50051"_L1),
155 QGrpcChannelOptions().setSerializationFormat(dummyFormat)
156 );
157 \endcode
158
159 \include qgrpcserializationformat.cpp custom-serializer-desc
160
161 \sa QAbstractGrpcChannel, QGrpcChannelOptions, QGrpcSerializationFormat
162*/
163
164namespace {
165
166Q_STATIC_LOGGING_CATEGORY(lcChannel, "qt.grpc.channel.http2")
167Q_STATIC_LOGGING_CATEGORY(lcStream, "qt.grpc.channel.http2.stream")
168
169constexpr QLatin1String UnixScheme("unix");
170constexpr QLatin1String UnixAbstractScheme("unix-abstract");
171constexpr QLatin1String HttpScheme("http");
172constexpr QLatin1String HttpsScheme("https");
173
174const QByteArray HttpStatusHeader(":status");
175const QByteArray ContentTypeHeader("content-type");
176const QByteArray GrpcStatusHeader("grpc-status");
177const QByteArray GrpcStatusMessageHeader("grpc-message");
178const QByteArray DefaultContentType("application/grpc");
179const QByteArray GrpcStatusDetailsHeader("grpc-status-details-bin");
180const QByteArray GrpcAcceptEncodingHeader("grpc-accept-encoding");
181const QByteArray GrpcEncodingHeader("grpc-encoding");
182constexpr qsizetype GrpcMessageSizeHeaderSize = 5;
183
184// This HTTP/2 Error Codes to QGrpcStatus::StatusCode mapping should be kept in sync
185// with the following docs:
186// https://www.rfc-editor.org/rfc/rfc7540#section-7
187// https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
188// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
189constexpr StatusCode http2ErrorToStatusCode(const quint32 http2Error)
190{
191 using Http2Error = Http2::Http2Error;
192
193 switch (http2Error) {
194 case Http2Error::HTTP2_NO_ERROR:
195 case Http2Error::PROTOCOL_ERROR:
196 case Http2Error::INTERNAL_ERROR:
197 case Http2Error::FLOW_CONTROL_ERROR:
198 case Http2Error::SETTINGS_TIMEOUT:
199 case Http2Error::STREAM_CLOSED:
200 case Http2Error::FRAME_SIZE_ERROR:
201 return StatusCode::Internal;
202 case Http2Error::REFUSE_STREAM:
203 return StatusCode::Unavailable;
204 case Http2Error::CANCEL:
205 return StatusCode::Cancelled;
206 case Http2Error::COMPRESSION_ERROR:
207 case Http2Error::CONNECT_ERROR:
208 return StatusCode::Internal;
209 case Http2Error::ENHANCE_YOUR_CALM:
210 return StatusCode::ResourceExhausted;
211 case Http2Error::INADEQUATE_SECURITY:
212 return StatusCode::PermissionDenied;
213 case Http2Error::HTTP_1_1_REQUIRED:
214 return StatusCode::Unknown;
215 }
216 return StatusCode::Internal;
217}
218
219// Ref: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md
220constexpr StatusCode http2StatusToStatusCode(const int status)
221{
222 switch (status) {
223 case 200:
224 return StatusCode::Ok;
225 case 400:
226 return StatusCode::Internal;
227 case 401:
228 return StatusCode::Unauthenticated;
229 case 403:
230 return StatusCode::PermissionDenied;
231 case 404:
232 return StatusCode::Unimplemented;
233 case 429:
234 case 502:
235 case 503:
236 case 504:
237 return StatusCode::Unavailable;
238 default:
239 return StatusCode::Unknown;
240 }
241}
242
243bool hasSslConfiguration(const QGrpcChannelOptions &opts)
244{
245#if QT_CONFIG(ssl)
246 return opts.sslConfiguration().has_value();
247#else
248 Q_UNUSED(opts)
249 return false;
250#endif
251}
252
253} // namespace
254
255struct ExpectedData
256{
257 qsizetype expectedSize = 0;
258 QByteArray container;
259
260 bool updateExpectedSize()
261 {
262 if (expectedSize == 0) {
263 if (container.size() < GrpcMessageSizeHeaderSize)
264 return false;
265 expectedSize = qFromBigEndian<quint32>(src: container.data() + 1)
266 + GrpcMessageSizeHeaderSize;
267 }
268 return true;
269 }
270};
271
272// The Http2Handler manages an individual RPC over the HTTP/2 channel.
273// Each instance corresponds to an RPC initiated by the user.
274class Http2Handler : public QObject
275{
276 Q_OBJECT
277
278 enum class HeaderPhase { Invalid, Initial, Trailers, TrailersOnly };
279 Q_ENUM(HeaderPhase);
280
281public:
282 enum class State : uint8_t {
283 Idle,
284 RequestHeadersSent,
285 Active,
286 // Endpoints
287 Cancelled,
288 Finished,
289 };
290 Q_ENUM(State);
291
292 explicit Http2Handler(QGrpcHttp2ChannelPrivate *parent, QGrpcOperationContext *context,
293 bool endStream);
294 ~Http2Handler() override;
295
296 void sendInitialRequest();
297 void attachStream(QHttp2Stream *stream_);
298 void processQueue();
299
300 void finish(const QGrpcStatus &status);
301 void asyncFinish(const QGrpcStatus &status);
302 void cancelWithStatus(const QGrpcStatus &status);
303
304 [[nodiscard]] bool expired() const { return !m_context; }
305
306 [[nodiscard]] bool isStreamClosedForSending() const
307 {
308 // If stream pointer is nullptr this means we never opened it and should collect
309 // the incoming messages in queue until the stream is opened or the error occurred.
310 return m_stream != nullptr
311 && (m_stream->state() == QHttp2Stream::State::HalfClosedLocal
312 || m_stream->state() == QHttp2Stream::State::Closed);
313 }
314
315// context slot handlers:
316 void cancel() { cancelWithStatus(status: { StatusCode::Cancelled, tr(s: "Cancelled by client") }); }
317 void writesDone();
318 void writeMessage(QByteArrayView data);
319 void deadlineTimeout()
320 {
321 cancelWithStatus(status: { StatusCode::DeadlineExceeded, tr(s: "Deadline exceeded") });
322 }
323
324 void handleHeaders(const HPack::HttpHeader &headers, HeaderPhase phase);
325
326private:
327 [[nodiscard]] HPack::HttpHeader constructInitialHeaders() const;
328 [[nodiscard]] bool constructFilterServerMetadata() const;
329 [[nodiscard]] QGrpcHttp2ChannelPrivate *channelPriv() const;
330 [[nodiscard]] QGrpcHttp2Channel *channel() const;
331 [[nodiscard]] bool handleContextExpired();
332
333 QPointer<QGrpcOperationContext> m_context;
334 HPack::HttpHeader m_initialHeaders;
335 QQueue<QByteArray> m_queue;
336 QPointer<QHttp2Stream> m_stream;
337 ExpectedData m_expectedData;
338 State m_state = State::Idle;
339 const bool m_endStreamAtFirstData;
340 bool m_writesDoneSent = false;
341 bool m_filterServerMetadata;
342 QTimer m_deadlineTimer;
343
344 Q_DISABLE_COPY_MOVE(Http2Handler)
345};
346
347class QGrpcHttp2ChannelPrivate : public QObject
348{
349 Q_OBJECT
350public:
351 enum class SocketType : uint8_t { Tcp, Tls, Local, LocalAbstract };
352
353 explicit QGrpcHttp2ChannelPrivate(const QUrl &uri, QGrpcHttp2Channel *q);
354 ~QGrpcHttp2ChannelPrivate() override = default;
355
356 void processOperation(QGrpcOperationContext *operationContext, bool endStream = false);
357
358 QGrpcHttp2Channel *q_ptr = nullptr;
359 const SocketType socketType;
360 const QUrl hostUri;
361 const QByteArray contentType;
362 const QByteArray authorityHeader;
363 const QByteArray schemeHeader;
364
365private:
366 enum ConnectionState { Connecting = 0, Connected, SettingsReceived, Error };
367
368 static SocketType constructSocketType(const QUrl &rawUri, const QGrpcChannelOptions &chOpts);
369 QUrl sanitizeHostUri(const QUrl &rawUri, const QGrpcChannelOptions &chOpts) const;
370 QByteArray setupContentTypeNegotiation(QGrpcHttp2Channel *qPtr) const;
371 static QByteArray constructAuthorityHeader(const QUrl &hostUri, SocketType socketType);
372
373 bool createHttp2Stream(Http2Handler *handler);
374 void createHttp2Connection();
375
376#if QT_CONFIG(localserver)
377 void handleLocalSocketError(QLocalSocket::LocalSocketError error)
378 {
379 handleSocketError(errorCode: QDebug::toBytes(object: error));
380 }
381#endif
382 void handleAbstractSocketError(QAbstractSocket::SocketError error)
383 {
384 handleSocketError(errorCode: QDebug::toBytes(object: error));
385 }
386 void handleSocketError(const QByteArray &errorCode);
387
388 template <typename Projection = q20::identity>
389 void for_each_non_expired_handler(Projection proj)
390 {
391 QVarLengthArray<QObject *> expiredHandler;
392 for (QObject *child : children()) {
393 auto *handler = qobject_cast<Http2Handler *>(object: child);
394 if (!handler)
395 continue;
396 if (handler->expired()) {
397 expiredHandler.push_back(t: handler);
398 continue;
399 }
400 std::invoke(std::forward<Projection>(proj), handler);
401 }
402 // Perform deletions after the loop to avoid modifying the children
403 // list during iteration. Delete in reverse order to avoid
404 // quadratic-time updates in QObject's children list.
405 qDeleteAll(begin: expiredHandler.crbegin(), end: expiredHandler.crend());
406 }
407
408private:
409 std::unique_ptr<QIODevice> m_socket = nullptr;
410 std::function<void()> m_reconnectFunction;
411
412 bool m_isInsideSocketErrorOccurred = false;
413 QHttp2Connection *m_connection = nullptr;
414 ConnectionState m_state = Connecting;
415
416 Q_DISABLE_COPY_MOVE(QGrpcHttp2ChannelPrivate)
417};
418
419///
420/// ## Http2Handler Implementations
421///
422
423Http2Handler::Http2Handler(QGrpcHttp2ChannelPrivate *parent, QGrpcOperationContext *context,
424 bool endStream)
425 : QObject(parent), m_context(context), m_initialHeaders(constructInitialHeaders()),
426 m_endStreamAtFirstData(endStream), m_filterServerMetadata(constructFilterServerMetadata())
427{
428 // If the context (lifetime bound to the user) is destroyed, this handler
429 // can no longer perform any meaningful work. We allow it to be deleted;
430 // QHttp2Stream will handle any outstanding cancellations appropriately.
431 connect(sender: context, signal: &QGrpcOperationContext::destroyed, context: this, slot: &Http2Handler::deleteLater);
432 connect(sender: context, signal: &QGrpcOperationContext::cancelRequested, context: this, slot: &Http2Handler::cancel);
433 connect(sender: context, signal: &QGrpcOperationContext::writesDoneRequested, context: this, slot: &Http2Handler::writesDone);
434 if (!m_endStreamAtFirstData) {
435 connect(sender: context, signal: &QGrpcOperationContext::writeMessageRequested, context: this,
436 slot: &Http2Handler::writeMessage);
437 }
438
439 m_deadlineTimer.setSingleShot(true);
440
441 writeMessage(data: context->argument());
442}
443
444Http2Handler::~Http2Handler()
445{
446 qCDebug(lcStream, "[%p] Destroying Http2Handler (state=%s, stream=%p)", this,
447 QDebug::toBytes(m_state).constData(), m_stream.get());
448 if (m_stream) {
449 QHttp2Stream *streamPtr = m_stream.get();
450 m_stream.clear();
451 delete streamPtr;
452 }
453}
454
455// Attaches the HTTP/2 stream and sets up the necessary connections and
456// preconditions. The HTTP/2 connection is established, and the transport
457// is now ready for communication.
458void Http2Handler::attachStream(QHttp2Stream *stream_)
459{
460 Q_ASSERT(m_stream == nullptr);
461 Q_ASSERT(stream_ != nullptr);
462
463 m_stream = stream_;
464
465 connect(sender: m_stream.get(), signal: &QHttp2Stream::headersReceived, context: this,
466 slot: [this](const HPack::HttpHeader &headers, bool endStream) mutable {
467 if (m_state >= State::Cancelled) {
468 // In case we are Cancelled or Finished, a
469 // finished has been emitted already and the
470 // Handler should get deleted here.
471 qCDebug(lcStream, "[%p] Ignoring headers - already closed (state=%s)", this,
472 QDebug::toBytes(m_state).constData());
473 deleteLater();
474 return;
475 }
476
477 HeaderPhase phase = HeaderPhase::Invalid;
478 if (m_state == State::RequestHeadersSent && endStream)
479 phase = HeaderPhase::TrailersOnly;
480 else if (m_state == State::RequestHeadersSent && !endStream)
481 phase = HeaderPhase::Initial;
482 else if (m_state == State::Active && endStream) {
483 phase = HeaderPhase::Trailers;
484 } else {
485 qCWarning(lcStream,
486 "[%p] Received unexcpected %s HEADERS (state=%s, "
487 "endStream=%d)",
488 this, QDebug::toBytes(phase).constData(),
489 QDebug::toBytes(m_state).constData(), endStream);
490 return;
491 }
492
493 m_state = State::Active;
494 handleHeaders(headers, phase);
495 });
496
497 connect(
498 sender: m_stream.get(), signal: &QHttp2Stream::errorOccurred, context: this,
499 slot: [this](quint32 http2ErrorCode, const QString &errorString) {
500 qCDebug(lcStream, "[%p] Stream errorOccurred (state=%s)", this,
501 QDebug::toBytes(m_state).constData());
502 finish(status: { http2ErrorToStatusCode(http2Error: http2ErrorCode), errorString });
503 },
504 type: Qt::SingleShotConnection);
505
506 connect(sender: m_stream.get(), signal: &QHttp2Stream::dataReceived, context: m_context.get(),
507 slot: [this](const QByteArray &data, bool endStream) {
508 if (m_state == State::Cancelled)
509 return;
510
511 m_expectedData.container.append(a: data);
512
513 if (!m_expectedData.updateExpectedSize())
514 return;
515
516 while (m_expectedData.container.size() >= m_expectedData.expectedSize) {
517 qCDebug(lcStream,
518 "[%p] About to process message (receivedSize=%" PRIdQSIZETYPE ", "
519 "expectedSize=%" PRIdQSIZETYPE ", containerSize=%" PRIdQSIZETYPE ")",
520 this, data.size(), m_expectedData.expectedSize,
521 m_expectedData.container.size());
522 const auto len = m_expectedData.expectedSize - GrpcMessageSizeHeaderSize;
523 const auto msg = m_expectedData.container.mid(index: GrpcMessageSizeHeaderSize, len);
524 emit m_context->messageReceived(data: msg);
525
526 m_expectedData.container.remove(index: 0, len: m_expectedData.expectedSize);
527 m_expectedData.expectedSize = 0;
528 if (!m_expectedData.updateExpectedSize())
529 return;
530 }
531
532 if (endStream)
533 finish(status: {});
534 });
535
536 connect(sender: m_stream.get(), signal: &QHttp2Stream::uploadFinished, context: this, slot: &Http2Handler::processQueue);
537}
538
539// Builds HTTP/2 headers for the initial gRPC request.
540// The headers are sent once the HTTP/2 connection is established.
541HPack::HttpHeader Http2Handler::constructInitialHeaders() const
542{
543 const static QByteArray AuthorityHeader(":authority");
544 const static QByteArray MethodHeader(":method");
545 const static QByteArray MethodValue("POST");
546 const static QByteArray PathHeader(":path");
547 const static QByteArray SchemeHeader(":scheme");
548
549 const static QByteArray TEHeader("te");
550 const static QByteArray TEValue("trailers");
551 const static QByteArray GrpcServiceNameHeader("service-name");
552 const static QByteArray GrpcAcceptEncodingValue("identity,deflate,gzip");
553 const static QByteArray UserAgentHeader("user-agent");
554 const static QByteArray UserAgentValue("grpc-c++-qtgrpc/"_ba + QT_VERSION_STR + " ("_ba
555 + QSysInfo::productType().toUtf8() + '/'
556 + QSysInfo::productVersion().toUtf8() + ')');
557
558 const auto &channelOptions = channel()->channelOptions();
559 const auto *channel = channelPriv();
560
561 QByteArray service{ m_context->service() };
562 QByteArray method{ m_context->method() };
563 auto headers = HPack::HttpHeader{
564 { AuthorityHeader, channel->authorityHeader },
565 { MethodHeader, MethodValue },
566 { PathHeader, QByteArray('/' + service + '/' + method) },
567 { SchemeHeader, channel->schemeHeader },
568 { ContentTypeHeader, channel->contentType },
569 { GrpcServiceNameHeader, service },
570 { GrpcAcceptEncodingHeader, GrpcAcceptEncodingValue },
571 { UserAgentHeader, UserAgentValue },
572 { TEHeader, TEValue },
573 };
574
575 auto iterateMetadata = [&headers](const auto &metadata) {
576 for (const auto &[key, value] : metadata.asKeyValueRange()) {
577 const auto lowerKey = key.toLower();
578 if (lowerKey == AuthorityHeader || lowerKey == MethodHeader || lowerKey == PathHeader
579 || lowerKey == SchemeHeader || lowerKey == ContentTypeHeader) {
580 continue;
581 }
582 headers.emplace_back(lowerKey, value);
583 }
584 };
585
586 iterateMetadata(channelOptions.metadata(QtGrpc::MultiValue));
587 iterateMetadata(m_context->callOptions().metadata(QtGrpc::MultiValue));
588
589 return headers;
590}
591
592bool Http2Handler::constructFilterServerMetadata() const
593{
594 return m_context->callOptions()
595 .filterServerMetadata()
596 .value_or(u: channel()->channelOptions().filterServerMetadata().value_or(u: true));
597}
598
599QGrpcHttp2ChannelPrivate *Http2Handler::channelPriv() const
600{
601 return qobject_cast<QGrpcHttp2ChannelPrivate *>(object: this->parent());
602}
603QGrpcHttp2Channel *Http2Handler::channel() const
604{
605 return channelPriv()->q_ptr;
606}
607
608bool Http2Handler::handleContextExpired()
609{
610 if (m_context)
611 return false;
612 m_state = State::Cancelled;
613 deleteLater(); // m_stream will sendRST_STREAM on destruction
614 return true;
615}
616
617// Slot to enqueue a writeMessage request, either from the initial message
618// or from the user in client/bidirectional streaming RPCs.
619void Http2Handler::writeMessage(QByteArrayView data)
620{
621 if (m_writesDoneSent || m_state > State::Active || isStreamClosedForSending()) {
622 qCDebug(lcStream, "[%p] Cannot write message (state=%s, writesDone=%d, streamClosed=%d)",
623 this, QDebug::toBytes(m_state).data(), m_writesDoneSent,
624 isStreamClosedForSending());
625 return;
626 }
627
628 QByteArray msg(GrpcMessageSizeHeaderSize + data.size(), '\0');
629 // Args must be 4-byte unsigned int to fit into 4-byte big endian
630 qToBigEndian(src: static_cast<quint32>(data.size()), dest: msg.data() + 1);
631
632 // protect against nullptr data.
633 if (!data.isEmpty()) {
634 std::memcpy(dest: msg.begin() + GrpcMessageSizeHeaderSize, src: data.begin(),
635 n: static_cast<size_t>(data.size()));
636 }
637
638 m_queue.enqueue(t: msg);
639 processQueue();
640}
641
642// Sends the initial headers and processes the message queue containing the
643// initial message. At this point, the HTTP/2 connection is established, and
644// the stream is attached.
645void Http2Handler::sendInitialRequest()
646{
647 Q_ASSERT(!m_initialHeaders.empty());
648 Q_ASSERT(m_stream);
649 Q_ASSERT(m_state == State::Idle);
650
651 if (!m_stream->sendHEADERS(headers: m_initialHeaders, endStream: false)) {
652 asyncFinish(status: { StatusCode::Unavailable,
653 tr(s: "Unable to send initial headers to an HTTP/2 stream") });
654 return;
655 }
656 m_state = State::RequestHeadersSent;
657 m_initialHeaders.clear();
658 processQueue();
659
660 std::optional<std::chrono::milliseconds> deadline = m_context->callOptions().deadlineTimeout();
661 if (!deadline)
662 deadline = channel()->channelOptions().deadlineTimeout();
663 if (deadline) {
664 // We have an active stream, a deadline and the initial headers have
665 // just been sent. It's time to start the timer.
666 connect(sender: &m_deadlineTimer, signal: &QTimer::timeout, context: this, slot: &Http2Handler::deadlineTimeout);
667 m_deadlineTimer.start(value: *deadline);
668 }
669 qCDebug(lcStream, "[%p] Sending initial request (deadline=%s)", this,
670 deadline ? qPrintable(QString::number(deadline->count()) + " ms"_L1) : "None");
671}
672
673// The core logic for sending the already serialized data through the HTTP/2 stream.
674// This function is invoked either by the user via writeMessageRequested() or
675// writesDoneRequested(), or it is continuously polled after the previous uploadFinished()
676void Http2Handler::processQueue()
677{
678 if (!m_stream)
679 return;
680
681 if (m_stream->isUploadingDATA()) {
682 qCDebug(lcStream, "[%p] Stream busy uploading (queue size=%" PRIdQSIZETYPE ")", this,
683 m_queue.size());
684 return;
685 }
686
687 if (m_queue.isEmpty())
688 return;
689
690 const auto nextMessage = m_queue.dequeue();
691 const bool closeStream = nextMessage.isEmpty() || m_endStreamAtFirstData;
692 m_stream->sendDATA(payload: nextMessage, endStream: closeStream);
693}
694
695void Http2Handler::finish(const QGrpcStatus &status)
696{
697 if (handleContextExpired())
698 return;
699 if (m_state == State::Finished)
700 return;
701 if (m_state != State::Cancelled) // don't overwrite the Cancelled state
702 m_state = State::Finished;
703 m_deadlineTimer.stop();
704 emit m_context->finished(status);
705 deleteLater();
706}
707void Http2Handler::asyncFinish(const QGrpcStatus &status)
708{
709 if (handleContextExpired())
710 return;
711 QTimer::singleShot(interval: 0, receiver: m_context.get(), slot: [this, status]() { finish(status); });
712}
713
714void Http2Handler::cancelWithStatus(const QGrpcStatus &status)
715{
716 if (m_state >= State::Cancelled)
717 return;
718 qCDebug(lcStream, "[%p] Cancelling (state=%s)", this, QDebug::toBytes(m_state).data());
719 m_state = State::Cancelled;
720
721 // Immediate cancellation by sending the RST_STREAM frame.
722 if (m_stream && !m_stream->sendRST_STREAM(errorCode: Http2::Http2Error::CANCEL)) {
723 qCWarning(lcStream, "[%p] Failed cancellation (stream=%p, stream::state=%s)", this,
724 m_stream.get(), QDebug::toBytes(m_stream->state()).constData());
725 }
726
727 finish(status);
728}
729
730void Http2Handler::writesDone()
731{
732 if (m_writesDoneSent || m_state > State::Active)
733 return;
734 m_writesDoneSent = true;
735
736 qCDebug(lcStream, "[%p] Writes done received (streamClosed=%d)", this, isStreamClosedForSending());
737
738 // Stream is already (half)closed, skip sending the DATA frame with the end-of-stream flag.
739 if (isStreamClosedForSending())
740 return;
741
742 m_queue.enqueue(t: {});
743 processQueue();
744}
745
746void Http2Handler::handleHeaders(const HPack::HttpHeader &headers, HeaderPhase phase)
747{
748 // ABNF syntax: Rule, [Optional-Rule], *Variable-Repetition
749 // Response-Headers → HTTPStatus [GrpcEncoding] [GrpcAcceptEncoding]
750 // ContentType *Custom-Metadata
751 // Trailers → GrpcStatus [GrpcStatusMessage] [GrpcStatusDetails] *Custom-Metadata
752 // Trailers-Only → HTTPStatus ContentType Trailers
753 //
754 // It's either Response-Headers + Trailers OR Trailers-Only for calls that
755 // produce an immediate error. Any Trailers phase will finish the RPC.
756 Q_ASSERT(phase != HeaderPhase::Invalid);
757 struct HeaderValidation
758 {
759 const bool requireHttpStatus : 1;
760 const bool requireContentType : 1;
761 const bool requireGrpcStatus : 1;
762 bool hasHttpStatus : 1;
763 bool hasContentType : 1;
764 bool hasGrpcStatus : 1;
765 };
766
767 if (handleContextExpired())
768 return;
769
770 HeaderValidation validation{
771 .requireHttpStatus: (phase != HeaderPhase::Trailers),
772 .requireContentType: (phase != HeaderPhase::Trailers),
773 .requireGrpcStatus: (phase != HeaderPhase::Initial),
774 .hasHttpStatus: false,
775 .hasContentType: false,
776 .hasGrpcStatus: false,
777 };
778
779 QMultiHash<QByteArray, QByteArray> metadata;
780 std::optional<QtGrpc::StatusCode> statusCode;
781 QString statusMessage;
782
783 for (const auto &[k, v] : headers) {
784 if (validation.requireHttpStatus && k == HttpStatusHeader) {
785 if (const auto status = v.toInt(); status != 200) {
786 finish(status: { http2StatusToStatusCode(status), "Received HTTP/2 status: %1"_L1.arg(args: v) });
787 return;
788 }
789 validation.hasHttpStatus = true;
790 } else if (validation.requireContentType && k == ContentTypeHeader) {
791 if (!v.toLower().startsWith(bv: DefaultContentType)) {
792 finish(status: { StatusCode::Internal, "Unexpected content-type: %1"_L1.arg(args: v) });
793 return;
794 }
795 validation.hasContentType = true;
796 } else if (validation.requireGrpcStatus && k == GrpcStatusHeader) {
797 bool ok;
798 const auto parsed = v.toShort(ok: &ok);
799 if (!ok) {
800 finish(status: { StatusCode::Internal, "Failed to parse gRPC-status: %1"_L1.arg(args: v) });
801 return;
802 }
803 statusCode = static_cast<StatusCode>(parsed);
804 validation.hasGrpcStatus = true;
805 } else if (validation.requireGrpcStatus && k == GrpcStatusMessageHeader) {
806 // Allowed optional headers
807 statusMessage = QString::fromUtf8(ba: v);
808 } else if (validation.requireGrpcStatus && k == GrpcStatusDetailsHeader) {
809 // Allowed optional headers
810 // TODO: Implement status-details - QTBUG-138362
811 } else if (phase == HeaderPhase::Initial
812 && (k == GrpcEncodingHeader || k == GrpcAcceptEncodingHeader)) {
813 // Allowed optional headers
814 // TODO: Implement compression handling - QTBUG-129286
815 } else if (k.startsWith(c: ':')) {
816 qCWarning(lcStream,
817 "[%p] Received unhandled HTTP/2 pseudo-header: { key: '%s', value: '%s' } "
818 "in phase: %s",
819 this, k.data(), v.data(), QDebug::toBytes(phase).data());
820 } else if (k.startsWith(bv: "grpc-")) {
821 qCWarning(lcStream,
822 "[%p] Received unexcpected gRPC-reserved header: { key: %s, value: %s } "
823 "in phase: %s",
824 this, k.data(), v.data(), QDebug::toBytes(phase).data());
825 } else { // Custom-Metadata
826 metadata.insert(key: k, value: v);
827 continue;
828 }
829
830 if (!m_filterServerMetadata)
831 metadata.insert(key: k, value: v);
832 }
833
834 if (validation.requireHttpStatus && !validation.hasHttpStatus) {
835 finish(status: { StatusCode::Internal, "Missing valid '%1' header"_L1.arg(args: HttpStatusHeader) });
836 return;
837 }
838
839 if (validation.requireContentType && !validation.hasContentType) {
840 finish(status: { StatusCode::Internal, "Missing valid '%1' header"_L1.arg(args: ContentTypeHeader) });
841 return;
842 }
843
844 if (validation.requireGrpcStatus && !validation.hasGrpcStatus) {
845 finish(status: { StatusCode::Internal, "Missing status code in trailers"_L1 });
846 return;
847 }
848
849 switch (phase) {
850 case HeaderPhase::Initial:
851 m_context->setServerInitialMetadata(std::move(metadata));
852 break;
853 case HeaderPhase::TrailersOnly:
854 [[fallthrough]];
855 case HeaderPhase::Trailers:
856 m_context->setServerTrailingMetadata(std::move(metadata));
857 finish(status: { *statusCode, statusMessage });
858 break;
859 default:
860 Q_UNREACHABLE();
861 }
862}
863
864///
865/// ## QGrpcHttp2ChannelPrivate Implementations
866///
867
868QGrpcHttp2ChannelPrivate::QGrpcHttp2ChannelPrivate(const QUrl &uri, QGrpcHttp2Channel *q)
869 : q_ptr(q), socketType(constructSocketType(rawUri: uri, chOpts: q_ptr->channelOptions())),
870 hostUri(sanitizeHostUri(rawUri: uri, chOpts: q_ptr->channelOptions())),
871 contentType(setupContentTypeNegotiation(q_ptr)),
872 authorityHeader(constructAuthorityHeader(hostUri, socketType)),
873 schemeHeader(hostUri.scheme().toLatin1())
874{
875 switch (socketType) {
876 case SocketType::Tcp: {
877 auto socket = std::make_unique<QTcpSocket>();
878 connect(sender: socket.get(), signal: &QAbstractSocket::connected, context: this,
879 slot: &QGrpcHttp2ChannelPrivate::createHttp2Connection);
880 connect(sender: socket.get(), signal: &QAbstractSocket::errorOccurred, context: this,
881 slot: &QGrpcHttp2ChannelPrivate::handleAbstractSocketError);
882 m_reconnectFunction = [this, socket = socket.get()] {
883 qCDebug(lcChannel, "[%p] Connecting to TCP endpoint at: %s:%d", this,
884 qPrintable(hostUri.host()), hostUri.port());
885 socket->connectToHost(hostName: hostUri.host(), port: hostUri.port());
886 };
887 m_socket = std::move(socket);
888 break;
889 }
890
891 case SocketType::Tls: {
892#if QT_CONFIG(ssl)
893 auto socket = std::make_unique<QSslSocket>();
894 if (const auto &sslConfig = q_ptr->channelOptions().sslConfiguration()) {
895 socket->setSslConfiguration(*sslConfig);
896 } else {
897 static const QByteArray h2NexProtocol = "h2"_ba;
898 auto defaultSslConfig = QSslConfiguration::defaultConfiguration();
899 auto allowedNextProtocols = defaultSslConfig.allowedNextProtocols();
900 if (!allowedNextProtocols.contains(t: h2NexProtocol)) {
901 allowedNextProtocols.append(t: h2NexProtocol);
902 defaultSslConfig.setAllowedNextProtocols(allowedNextProtocols);
903 }
904 socket->setSslConfiguration(defaultSslConfig);
905 }
906 connect(sender: socket.get(), signal: &QSslSocket::encrypted, context: this,
907 slot: &QGrpcHttp2ChannelPrivate::createHttp2Connection);
908 connect(sender: socket.get(), signal: &QAbstractSocket::errorOccurred, context: this,
909 slot: &QGrpcHttp2ChannelPrivate::handleAbstractSocketError);
910 m_reconnectFunction = [this, socket = socket.get()] {
911 qCDebug(lcChannel, "[%p] Connecting to SSL endpoint at: %s:%d", this,
912 qPrintable(hostUri.host()), hostUri.port());
913 socket->connectToHostEncrypted(hostName: hostUri.host(), port: hostUri.port());
914 };
915 m_socket = std::move(socket);
916#else
917 m_reconnectFunction = [this] {
918 qCFatal(lcChannel, "[%p] QSslSocket support needed for TLS transportation", this);
919 };
920#endif
921 break;
922 }
923
924 case SocketType::Local:
925 case SocketType::LocalAbstract: {
926#if QT_CONFIG(localserver)
927 auto socket = std::make_unique<QLocalSocket>();
928 if (socketType == SocketType::LocalAbstract)
929 socket->setSocketOptions(QLocalSocket::AbstractNamespaceOption);
930 connect(sender: socket.get(), signal: &QLocalSocket::connected, context: this,
931 slot: &QGrpcHttp2ChannelPrivate::createHttp2Connection);
932 connect(sender: socket.get(), signal: &QLocalSocket::errorOccurred, context: this,
933 slot: &QGrpcHttp2ChannelPrivate::handleLocalSocketError);
934 m_reconnectFunction = [this, socket = socket.get()] {
935 const auto name = hostUri.host() + hostUri.path();
936 qCDebug(lcChannel, "[%p] Connecting to local socket at: %s", this, qPrintable(name));
937 socket->connectToServer(name);
938 };
939 m_socket = std::move(socket);
940#else
941 m_reconnectFunction = [this] {
942 qCFatal(lcChannel,
943 "[%p] QLocalSocket support needed for 'unix' or 'unix-abstract' transportation",
944 this);
945 };
946#endif
947 break;
948 }
949
950 } // switch (socketType)
951
952 m_reconnectFunction();
953}
954
955void QGrpcHttp2ChannelPrivate::processOperation(QGrpcOperationContext *operationContext,
956 bool endStream)
957{
958 Q_ASSERT_X(operationContext != nullptr, "QGrpcHttp2ChannelPrivate::processOperation",
959 "operation context is nullptr.");
960
961 // Send the finished signals asynchronously, so user connections work correctly.
962 if (!m_socket->isWritable() && m_state == ConnectionState::Connected) {
963 qCWarning(lcChannel, "[%p] Socket not writable for operation to %s (error=%s)", this,
964 qPrintable(hostUri.toString()), qPrintable(m_socket->errorString()));
965 QTimer::singleShot(interval: 0, receiver: operationContext,
966 slot: [operationContext, err = m_socket->errorString()]() {
967 emit operationContext->finished(status: { StatusCode::Unavailable, err });
968 });
969 return;
970 }
971
972 auto *handler = new Http2Handler(this, operationContext, endStream);
973 if (m_connection && !createHttp2Stream(handler))
974 return;
975
976 if (m_state == ConnectionState::SettingsReceived)
977 handler->sendInitialRequest();
978
979 if (m_state == ConnectionState::Error) {
980 Q_ASSERT_X(m_reconnectFunction, "QGrpcHttp2ChannelPrivate::processOperation",
981 "Socket reconnection function is not defined.");
982 if (m_isInsideSocketErrorOccurred) {
983 qCWarning(lcChannel,
984 "[%p] Inside socket error handler. Reconnect deferred to event loop.", this);
985 QTimer::singleShot(interval: 0, slot: [this]{ m_reconnectFunction(); });
986 } else {
987 m_reconnectFunction();
988 }
989 m_state = ConnectionState::Connecting;
990 qCDebug(lcChannel, "[%p] State changed to 'Connecting'. Reconnection initiated.", this);
991 }
992}
993
994void QGrpcHttp2ChannelPrivate::createHttp2Connection()
995{
996 Q_ASSERT_X(m_connection == nullptr, "QGrpcHttp2ChannelPrivate::createHttp2Connection",
997 "Attempt to create the HTTP/2 connection, but it already exists. This situation is "
998 "exceptional.");
999
1000 // Nagle's algorithm slows down gRPC communication when frequently sending small utility
1001 // HTTP/2 frames. Since an ACK is not sent until a predefined timeout if the TCP frame is
1002 // not full enough, communication hangs. In our case, this results in a 40ms delay when
1003 // WINDOW_UPDATE or PING frames are sent in a separate TCP frame.
1004 //
1005 // TODO: We should probably allow users to opt out of this using QGrpcChannelOptions,
1006 // see QTBUG-134428.
1007 if (QAbstractSocket *abstractSocket = qobject_cast<QAbstractSocket *>(object: m_socket.get()))
1008 abstractSocket->setSocketOption(option: QAbstractSocket::LowDelayOption, value: 1);
1009
1010 m_connection = QHttp2Connection::createDirectConnection(socket: m_socket.get(), config: {});
1011
1012 Q_ASSERT_X(m_connection, "QGrpcHttp2ChannelPrivate", "Unable to create the HTTP/2 connection");
1013 connect(sender: m_socket.get(), signal: &QAbstractSocket::readyRead, context: m_connection,
1014 slot: &QHttp2Connection::handleReadyRead);
1015
1016 m_state = ConnectionState::Connected;
1017 qCDebug(lcChannel, "[%p] Created new HTTP/2 connection to %s", this,
1018 qPrintable(hostUri.toString()));
1019
1020 connect(sender: m_connection, signal: &QHttp2Connection::settingsFrameReceived, context: this, slot: [this] {
1021 if (m_state == ConnectionState::SettingsReceived)
1022 return;
1023 m_state = ConnectionState::SettingsReceived;
1024 qCDebug(lcChannel, "[%p] SETTINGS frame received. Connection ready for use.", this);
1025 for_each_non_expired_handler(proj: [](Http2Handler *handler) { handler->sendInitialRequest(); });
1026 });
1027
1028 for_each_non_expired_handler(proj: [this](Http2Handler *handler) { createHttp2Stream(handler); });
1029}
1030
1031void QGrpcHttp2ChannelPrivate::handleSocketError(const QByteArray &errorCode)
1032{
1033 for_each_non_expired_handler(proj: [this, &errorCode](Http2Handler *handler) {
1034 if (m_isInsideSocketErrorOccurred) {
1035 qCCritical(lcChannel,
1036 "[%p] Socket errorOccurred signal triggered while "
1037 "already handling an error",
1038 this);
1039 return;
1040 }
1041 m_isInsideSocketErrorOccurred = true;
1042 auto reset = qScopeGuard(f: [this]() { m_isInsideSocketErrorOccurred = false; });
1043 emit handler->finish(status: { StatusCode::Unavailable,
1044 tr(s: "Network error occurred: %1").arg(a: errorCode) });
1045 });
1046
1047 qCDebug(lcChannel, "[%p] Socket error occurred (code=%s, details=%s, hostUri=%s)", this,
1048 errorCode.constData(), qPrintable(m_socket->errorString()),
1049 qPrintable(hostUri.toString()));
1050 delete m_connection;
1051 m_connection = nullptr;
1052 m_state = ConnectionState::Error;
1053}
1054
1055QUrl QGrpcHttp2ChannelPrivate::sanitizeHostUri(const QUrl &rawUri,
1056 const QGrpcChannelOptions &chOpts) const
1057{
1058 QUrl sanitizedUri(rawUri);
1059 auto check = [&](QLatin1StringView expected) {
1060 if (rawUri.scheme() != expected) {
1061 qCWarning(lcChannel,
1062 "[%p] Unsupported transport protocol scheme '%s'. Fall back to '%s'.", this,
1063 qPrintable(hostUri.scheme()), qPrintable(expected));
1064 sanitizedUri.setScheme(expected);
1065 }
1066 };
1067 const auto scheme = rawUri.scheme();
1068 if (scheme == UnixScheme || scheme == UnixAbstractScheme) {
1069 sanitizedUri.setScheme(HttpScheme);
1070 } else if (scheme == HttpsScheme || hasSslConfiguration(opts: chOpts)) {
1071 check(HttpsScheme);
1072 if (rawUri.port() < 0)
1073 sanitizedUri.setPort(443);
1074 } else {
1075 check(HttpScheme);
1076 if (rawUri.port() < 0)
1077 sanitizedUri.setPort(80);
1078 }
1079 return sanitizedUri;
1080}
1081
1082QGrpcHttp2ChannelPrivate::SocketType
1083QGrpcHttp2ChannelPrivate::constructSocketType(const QUrl &rawUri, const QGrpcChannelOptions &chOpts)
1084{
1085 const auto scheme = rawUri.scheme();
1086 if (scheme == UnixScheme)
1087 return SocketType::Local;
1088 if (scheme == UnixAbstractScheme)
1089 return SocketType::LocalAbstract;
1090 if (scheme == HttpsScheme || hasSslConfiguration(opts: chOpts))
1091 return SocketType::Tls;
1092 return SocketType::Tcp;
1093}
1094
1095QByteArray QGrpcHttp2ChannelPrivate::setupContentTypeNegotiation(QGrpcHttp2Channel *qPtr) const
1096{
1097 auto channelOptions = qPtr->channelOptions();
1098 auto formatSuffix = channelOptions.serializationFormat().suffix();
1099 const QByteArray defaultContentType = DefaultContentType;
1100 const QByteArray contentTypeFromOptions = !formatSuffix.isEmpty()
1101 ? defaultContentType + '+' + formatSuffix
1102 : defaultContentType;
1103
1104 bool warnAboutFormatConflict = !formatSuffix.isEmpty();
1105 QByteArray finalContentType = contentTypeFromOptions;
1106
1107 const auto it = channelOptions.metadata(QtGrpc::MultiValue).constFind(key: ContentTypeHeader.data());
1108 if (it != channelOptions.metadata(QtGrpc::MultiValue).cend()) {
1109 if (formatSuffix.isEmpty() && it.value() != DefaultContentType) {
1110 // Auto-detect format from content-type header
1111 if (it.value() == "application/grpc+json") {
1112 channelOptions.setSerializationFormat(SerializationFormat::Json);
1113 } else if (it.value() == "application/grpc+proto" || it.value() == DefaultContentType) {
1114 channelOptions.setSerializationFormat(SerializationFormat::Protobuf);
1115 } else {
1116 qCWarning(lcChannel,
1117 "[%p] Unable to determine serializer for entry { key: %s, value: %s }. "
1118 "Defaulting to format '%s'",
1119 this, it.key().data(), it.value().data(),
1120 QDebug::toBytes(SerializationFormat::Default).data());
1121 channelOptions.setSerializationFormat(SerializationFormat::Default);
1122 }
1123 qPtr->setChannelOptions(channelOptions);
1124 warnAboutFormatConflict = false;
1125 } else if (it.value() != contentTypeFromOptions) {
1126 warnAboutFormatConflict = true;
1127 } else {
1128 warnAboutFormatConflict = false;
1129 }
1130 } else {
1131 warnAboutFormatConflict = false;
1132 }
1133
1134 // Update final content type if format changed
1135 if (formatSuffix != channelOptions.serializationFormat().suffix()) {
1136 finalContentType = !channelOptions.serializationFormat().suffix().isEmpty()
1137 ? defaultContentType + '+' + channelOptions.serializationFormat().suffix()
1138 : defaultContentType;
1139 }
1140
1141 if (warnAboutFormatConflict) {
1142 qCWarning(lcChannel,
1143 "[%p] Manually specified serialization format '%s' does not "
1144 "match metadata entry { key: %s, value: %s }",
1145 this, contentTypeFromOptions.data(), it.key().data(), it.value().data());
1146 }
1147
1148 return finalContentType;
1149}
1150
1151QByteArray QGrpcHttp2ChannelPrivate::constructAuthorityHeader(const QUrl &hostUri,
1152 SocketType socketType)
1153{
1154 auto authority = hostUri.authority(options: QUrl::FullyEncoded | QUrl::RemoveUserInfo | QUrl::RemovePort)
1155 .toLatin1();
1156 const int port = hostUri.port();
1157 if ((socketType == SocketType::Tcp && port != 80)
1158 || (socketType == SocketType::Tls && port != 443)) {
1159 authority += ':';
1160 authority += QByteArray::number(port);
1161 }
1162
1163 return authority;
1164}
1165
1166bool QGrpcHttp2ChannelPrivate::createHttp2Stream(Http2Handler *handler)
1167{
1168 Q_ASSERT(handler != nullptr);
1169 Q_ASSERT(m_connection);
1170
1171 const auto streamAttempt = m_connection->createStream();
1172 if (!streamAttempt.ok()) {
1173 handler->asyncFinish(status: { StatusCode::Unavailable,
1174 tr(s: "Unable to create an HTTP/2 stream (%1)")
1175 .arg(a: QDebug::toString(object: streamAttempt.error())) });
1176 return false;
1177 }
1178 handler->attachStream(stream_: streamAttempt.unwrap());
1179 return true;
1180}
1181
1182///
1183/// ## QGrpcHttp2Channel Implementations
1184///
1185
1186/*!
1187 Constructs QGrpcHttp2Channel with \a hostUri. Please see the
1188 \l{Transportation scheme} section for more information.
1189*/
1190QGrpcHttp2Channel::QGrpcHttp2Channel(const QUrl &hostUri)
1191 : d_ptr(std::make_unique<QGrpcHttp2ChannelPrivate>(args: hostUri, args: this))
1192{
1193}
1194
1195/*!
1196 Constructs QGrpcHttp2Channel with \a hostUri and \a options. Please see the
1197 \l{Transportation scheme} section for more information.
1198*/
1199QGrpcHttp2Channel::QGrpcHttp2Channel(const QUrl &hostUri, const QGrpcChannelOptions &options)
1200 : QAbstractGrpcChannel(options),
1201 d_ptr(std::make_unique<QGrpcHttp2ChannelPrivate>(args: hostUri, args: this))
1202{
1203}
1204
1205/*!
1206 Destroys the QGrpcHttp2Channel object.
1207*/
1208QGrpcHttp2Channel::~QGrpcHttp2Channel() = default;
1209
1210/*!
1211 Returns the host URI for this channel.
1212*/
1213QUrl QGrpcHttp2Channel::hostUri() const
1214{
1215 return d_ptr->hostUri;
1216}
1217
1218/*!
1219 \internal
1220 Initiates a unary \gRPC call.
1221*/
1222void QGrpcHttp2Channel::call(std::shared_ptr<QGrpcOperationContext> operationContext)
1223{
1224 d_ptr->processOperation(operationContext: operationContext.get(), endStream: true);
1225}
1226
1227/*!
1228 \internal
1229 Initiates a server-side \gRPC stream.
1230*/
1231void QGrpcHttp2Channel::serverStream(std::shared_ptr<QGrpcOperationContext> operationContext)
1232{
1233 d_ptr->processOperation(operationContext: operationContext.get(), endStream: true);
1234}
1235
1236/*!
1237 \internal
1238 Initiates a client-side \gRPC stream.
1239*/
1240void QGrpcHttp2Channel::clientStream(std::shared_ptr<QGrpcOperationContext> operationContext)
1241{
1242 d_ptr->processOperation(operationContext: operationContext.get());
1243}
1244
1245/*!
1246 \internal
1247 Initiates a bidirectional \gRPC stream.
1248*/
1249void QGrpcHttp2Channel::bidiStream(std::shared_ptr<QGrpcOperationContext> operationContext)
1250{
1251 d_ptr->processOperation(operationContext: operationContext.get());
1252}
1253
1254/*!
1255 \internal
1256 Returns the serializer of the channel.
1257*/
1258std::shared_ptr<QAbstractProtobufSerializer> QGrpcHttp2Channel::serializer() const
1259{
1260 return channelOptions().serializationFormat().serializer();
1261}
1262
1263QT_END_NAMESPACE
1264
1265#include "qgrpchttp2channel.moc"
1266

source code of qtgrpc/src/grpc/qgrpchttp2channel.cpp