Skip to content

improve HTTP/2 + TLS support#1229

Open
SSE4 wants to merge 3 commits intouserver-framework:developfrom
SSE4:http2_tls
Open

improve HTTP/2 + TLS support#1229
SSE4 wants to merge 3 commits intouserver-framework:developfrom
SSE4:http2_tls

Conversation

@SSE4
Copy link
Copy Markdown
Contributor

@SSE4 SSE4 commented May 6, 2026

I've got userver working with plain HTTP/2 and HTTP/1.1 + TLS on their own, however, HTTP/2 + TLS didn't work quite well for me, I've used the following config:

      listener:
        port: 8443
        task_processor: main-task-processor
        tls:
          cert: ng200ok.crt
          private-key: ng200ok.pem
        connection:
          http-version: '2'
          http2-session:
            max_concurrent_streams: 100
            max_frame_size: 16384
            initial_window_size: 65536

that I get with curl - it doesn't use HTTP/2, only fallbacks to HTTP/1.1:

curl log
curl -lvsk --http2 https://127.0.0.1:8443/hello\?name\=userver
*   Trying 127.0.0.1:8443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / RSASSA-PSS
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
*  subject: C=RU; ST=Russia; L=Moscow; O=Security; OU=IT Department; CN=*.example.org
*  start date: May  6 07:30:13 2026 GMT
*  expire date: Jul  4 07:30:13 2301 GMT
*  issuer: C=RU; ST=Russia; L=Moscow; O=Security; OU=IT Department; CN=*.example.org
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to 127.0.0.1 (127.0.0.1) port 8443
* using HTTP/1.x
> GET /hello?name=userver HTTP/1.1
> Host: 127.0.0.1:8443
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 200 OK
< Date: Wed, 06 May 2026 10:34:45 UTC
< Content-Type: application/octet-stream
< Server: userver/3.1-rc (20260506035656; rv:f078534)
< traceparent: 00-99db4a3530c2ed800d5a06288191e71a-55592ba5e52f48c7-01
< X-YaRequestId: e89e6d21a3bbb20100c5a1dbf5b78c19
< X-YaTraceId: 99db4a3530c2ed800d5a06288191e71a
< X-YaSpanId: 55592ba5e52f48c7
< Accept-Encoding: gzip, zstd, identity
< Connection: keep-alive
< Content-Length: 16
<
Hello, userver!
* Connection #0 to host 127.0.0.1 left intact

okay, this seems to be due to the fact userver never specifies ALPN, so I've added one via SSL_CTX_set_alpn_select_cb in 2d29453 (this is more or less same as nginx and envoy are doing)

it started to progress a little bit, curl now switches to HTTP/2, but results in an error in the middle:

curl log
curl -lvsk --http2 https://127.0.0.1:8443/hello\?name\=userver
*   Trying 127.0.0.1:8443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: C=RU; ST=Russia; L=Moscow; O=Security; OU=IT Department; CN=*.example.org
*  start date: May  6 07:30:13 2026 GMT
*  expire date: Jul  4 07:30:13 2301 GMT
*  issuer: C=RU; ST=Russia; L=Moscow; O=Security; OU=IT Department; CN=*.example.org
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to 127.0.0.1 (127.0.0.1) port 8443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://127.0.0.1:8443/hello?name=userver
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: 127.0.0.1:8443]
* [HTTP/2] [1] [:path: /hello?name=userver]
* [HTTP/2] [1] [user-agent: curl/8.14.1]
* [HTTP/2] [1] [accept: */*]
> GET /hello?name=userver HTTP/2
> Host: 127.0.0.1:8443
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200
< date: Wed, 06 May 2026 10:41:48 UTC
< content-type: application/octet-stream
< accept-encoding: gzip, zstd, identity
< x-yaspanid: f2f8730fcc0e57eb
< x-yatraceid: 23be0fa7912b6d7d905dbccbfed1a9a7
< x-yarequestid: 875862045156a7bce9fbbf5b6791357f
< traceparent: 00-23be0fa7912b6d7d905dbccbfed1a9a7-f2f8730fcc0e57eb-01
< server: userver/3.1-rc (20260506035656; rv:f078534)
<
* TLSv1.3 (OUT), TLS alert, unexpected message (522):
* OpenSSL SSL_read: OpenSSL/3.5.5: error:0A0001BB:SSL routines::bad record type, errno 0
* Failed receiving HTTP2 data: 56(Failure when receiving data from the peer)
* OpenSSL SSL_write: SSL_ERROR_SYSCALL, errno 0
* Connection #0 to host 127.0.0.1 left intact

there is also error message in userver log in that case:

userver log
tskv	timestamp=2026-05-06T17:41:48.410837	level=ERROR	module=TryParseRequests ( userver/core/src/server/net/connection_base.cpp:82 )	task_id=7F68B83DF400	thread_id=0x00007F68B9BFD400	stacktrace= 0# userver::v3_1_rc::utils::TracefulExceptionBase::TracefulExceptionBase(userver::v3_1_rc::utils::TracefulExceptionBase::TraceMode) in ./build-release/ng200ok\n 1# userver::v3_1_rc::engine::io::IoException::IoException(std::basic_string_view<char, std::char_traits<char> >) in ./build-release/ng200ok\n 2# userver::v3_1_rc::engine::io::TlsWrapper::WaitReadable(userver::v3_1_rc::engine::Deadline) [clone .cold] in ./build-release/ng200ok\n 3# userver::v3_1_rc::server::net::SocketBufferedReader::TryRead(userver::v3_1_rc::engine::io::RwBase&, int) in ./build-release/ng200ok\n 4# userver::v3_1_rc::server::net::ConnectionBase::TryParseRequests(userver::v3_1_rc::server::request::RequestParser&) in ./build-release/ng200ok\n 5# userver::v3_1_rc::server::net::Http2Connection::ListenForRequests() in ./build-release/ng200ok\n 6# userver::v3_1_rc::server::net::Http2Connection::Process() in ./build-release/ng200ok\n 7# userver::v3_1_rc::server::net::ListenerImpl::ProcessConnection(userver::v3_1_rc::engine::io::Socket, userver::v3_1_rc::server::net::PortConfig const&) in ./build-release/ng200ok\n 8# void std::__invoke_impl<void, userver::v3_1_rc::server::net::ListenerImpl::AcceptConnection(userver::v3_1_rc::engine::io::Socket&, userver::v3_1_rc::server::net::PortConfig const&)::{lambda(auto:1, auto:2)#1}, userver::v3_1_rc::engine::io::Socket, userver::v3_1_rc::utils::FastScopeGuard<userver::v3_1_rc::server::net::ListenerImpl::AcceptConnection(userver::v3_1_rc::engine::io::Socket&, userver::v3_1_rc::server::net::PortConfig const&)::{lambda()#1}> >(std::__invoke_other, userver::v3_1_rc::server::net::ListenerImpl::AcceptConnection(userver::v3_1_rc::engine::io::Socket&, userver::v3_1_rc::server::net::PortConfig const&)::{lambda(auto:1, auto:2)#1}&&, userver::v3_1_rc::engine::io::Socket&&, userver::v3_1_rc::utils::FastScopeGuard<userver::v3_1_rc::server::net::ListenerImpl::AcceptConnection(userver::v3_1_rc::engine::io::Socket&, userver::v3_1_rc::server::net::PortConfig const&)::{lambda()#1}>&&) [clone .isra.0] in ./build-release/ng200ok\n 9# [start of coroutine]\n	text=Error while receiving from peer ::ffff:127.0.0.1 on fd 36: WaitReadable failed: ssl/tls alert unexpected message (userver::v3_1_rc::engine::io::TlsException)

okay, I've figured out it's due to the TODO in the existing code:

// TODO: doesn't work with TLS?!

it only tries to work with io::Socket, but in case of TLS, implementation is actually TlsWrapper, so I've changed Http2Stream to use generic io::RwBase interface instead in 979c8da

a small caveat was with Http2Session using SendAll method accepting std::initializer_list, which doesn't match generic WriteAll signature, so I've concluded, as userver already switched to C++20, it would be better to use std::span in the interface, this way it will compile and still may use iovec implementation for real socket saving system calls, this was done in f4451e5.

finally, I've got working HTTP/2 over TLS via curl:

curl log
curl -lvsk --http2 https://127.0.0.1:8443/hello\?name\=userver
*   Trying 127.0.0.1:8443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: C=RU; ST=Russia; L=Moscow; O=Security; OU=IT Department; CN=*.example.org
*  start date: May  6 07:30:13 2026 GMT
*  expire date: Jul  4 07:30:13 2301 GMT
*  issuer: C=RU; ST=Russia; L=Moscow; O=Security; OU=IT Department; CN=*.example.org
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
*   Certificate level 0: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to 127.0.0.1 (127.0.0.1) port 8443
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://127.0.0.1:8443/hello?name=userver
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: 127.0.0.1:8443]
* [HTTP/2] [1] [:path: /hello?name=userver]
* [HTTP/2] [1] [user-agent: curl/8.14.1]
* [HTTP/2] [1] [accept: */*]
> GET /hello?name=userver HTTP/2
> Host: 127.0.0.1:8443
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/2 200
< date: Wed, 06 May 2026 10:47:44 UTC
< content-type: application/octet-stream
< accept-encoding: gzip, zstd, identity
< x-yaspanid: cddc0300e7cef253
< x-yatraceid: 801194176f3b9060942c7d8da83e7f9a
< x-yarequestid: c8f04df591e80145958fca9c51f422d3
< traceparent: 00-801194176f3b9060942c7d8da83e7f9a-cddc0300e7cef253-01
< server: userver/3.1-rc (20260506035656; rv:f078534)
<
Hello, userver!
* Connection #0 to host 127.0.0.1 left intact

and it also worked in web-browser (Chromium).

@SSE4 SSE4 force-pushed the http2_tls branch 2 times, most recently from 55b0272 to 63eefe9 Compare May 6, 2026 12:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant