Skip to content

net.http: add TLS termination to Server (HTTPS support)#27373

Merged
JalonSolov merged 1 commit into
vlang:masterfrom
quaesitor-scientiam:net-http-server-tls
Jun 7, 2026
Merged

net.http: add TLS termination to Server (HTTPS support)#27373
JalonSolov merged 1 commit into
vlang:masterfrom
quaesitor-scientiam:net-http-server-tls

Conversation

@quaesitor-scientiam

Copy link
Copy Markdown
Contributor

What

net.http.Server has been plain-HTTP only. This PR adds TLS termination so
an HTTP/1.1 server can listen on https:// directly, without a separate reverse
proxy. It is the prerequisite for landing server-side HTTP/2 (the next PR in the
series).

mut srv := &http.Server{
    addr:                   ':9043'
    cert:                   pem_cert_string   // or filesystem path
    cert_key:               pem_key_string    // or filesystem path
    in_memory_verification: true              // false when cert/cert_key are paths
    handler:                MyHandler{}
}
srv.listen_and_serve()

Files

File Change
server.v adds cert, cert_key, in_memory_verification fields; default_https_server_port = 9043; listen_and_serve delegates to listen_and_serve_tls when cert+key are set
server_tls_notd_use_openssl.v mbedtls-based TLS listener + worker pool that serves HTTP/1.1 over TLS using the existing Handler contract
server_tls_d_use_openssl.v matching stub for -d use_openssl: prints a clear runtime error, so the module still builds and existing plain-HTTP code keeps working
server_tls_test.v hermetic round-trip test

Design

  • Opt-in, additive. When cert / cert_key are unset, the existing plain
    TCP path is unchanged — no new code on that branch, same Handler contract,
    same workers.
  • Backend gating via file suffixes. The mbedtls implementation lives in
    *_notd_use_openssl.v (default backend) and the matching stub in
    *_d_use_openssl.v reports a clear runtime error. This avoids pulling
    mbedtls into -d use_openssl builds that only used the OpenSSL client.
  • No public-API break. The new fields are optional with safe defaults; the
    Handler interface is untouched.

Tests

server_tls_test.v spins up a local TLS Server with an in-memory
self-signed cert/key, exercises it via http.fetch(url, validate: false), and
asserts a 200 response body. Skips itself under -d use_openssl with a clear
message so the suite stays green there.

./vnew test vlib/net/http/server_tls_test.v                       # passes
./vnew -W -cstrict -cc clang test vlib/net/http/server_tls_test.v # passes
./vnew -silent test vlib/net/http/                                # 19 passed, 1 skipped (Windows)
./vnew -d use_openssl -silent test vlib/net/http/                 # 19 passed, 1 skipped

Notes / follow-ups

  • OpenSSL server listener. The OpenSSL backend doesn't yet expose a
    server-side listener in net.openssl. The stub here reports a clear error;
    adding an OpenSSL listener (parallel to the mbedtls one in this PR) is the
    natural follow-up.
  • Server-side HTTP/2. This PR is the prerequisite. The next PR adds ALPN
    on the TLS listener and an h2 path in the worker that demuxes streams onto
    the existing Handler.handle(Request) Response contract — again with no
    public-API change.

🤖 Generated with Claude Code

net.http.Server has been plain-HTTP only. Add TLS termination so an HTTP/1.1
server can listen on https:// directly, without a separate reverse proxy.

- Server gains three opt-in fields: `cert`, `cert_key`, and
  `in_memory_verification`, mirroring the client-side SSL config naming. When
  both `cert` and `cert_key` are set, `listen_and_serve` delegates to
  `listen_and_serve_tls`, which uses the mbedtls SSL listener to accept TLS
  connections, hands them off to a separate worker pool, and serves HTTP/1.1
  requests over those TLS sockets with the existing Handler interface.
- New default port `default_https_server_port = 9043` for HTTPS listeners.
- The TLS path is provided only on the default mbedtls backend
  (server_tls_notd_use_openssl.v). On `-d use_openssl`, a matching
  server_tls_d_use_openssl.v provides a clear-error stub at runtime so the
  module builds and existing plain-HTTP servers keep working; an OpenSSL
  server listener is a follow-up.
- Existing plain-HTTP behavior is completely unchanged when `cert` / `cert_key`
  are not set: no new code on that path, same Handler contract, same workers.

Hermetic test: spins up a local TLS Server with an in-memory cert/key,
exercises it via http.fetch(validate: false), and asserts the 200 response
body. The test skips itself under -d use_openssl with a clear message. The
full vlib/net/http suite is green on both backends, and the TLS test passes
under -W -cstrict -cc clang.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@JalonSolov JalonSolov merged commit 0ff44dc into vlang:master Jun 7, 2026
76 of 84 checks passed
JalonSolov pushed a commit that referenced this pull request Jun 8, 2026
* net.http: add server-side HTTP/2 (ALPN + h2 frame demux)

Build on TLS termination (#27373) to let net.http.Server speak HTTP/2.

- New `enable_http2` field on Server. When set on a TLS listener, the listener
  advertises ALPN `h2, http/1.1`. After the handshake, the TLS worker checks
  `negotiated_alpn()`: if `h2`, it dispatches to the new HTTP/2 driver;
  otherwise the existing HTTP/1.1 path is unchanged.
- New h2_server.v (H2ServerConn): reads the client preface, exchanges SETTINGS
  (advertising SETTINGS_MAX_CONCURRENT_STREAMS=1 so requests serialize), and
  runs the frame loop. HEADERS+CONTINUATION are assembled and HPACK-decoded
  into a net.http.Request; DATA frames populate the body and replenish flow
  control; SETTINGS / PING / WINDOW_UPDATE / GOAWAY / RST_STREAM / PRIORITY are
  serviced inline. When the request stream closes, the existing
  Handler.handle(Request) Response interface is invoked unchanged; the Response
  is HPACK-encoded into HEADERS + DATA(END_STREAM) and sent back.
- Hop-by-hop response headers are dropped (RFC 7540 Section 8.1.2.2). The
  request body is capped at 8 MiB with RST_STREAM(REFUSED_STREAM) on overflow.
- The Handler contract is untouched: req.url is the request-target (the :path
  pseudo-header) and Host comes from :authority, so existing HTTP/1.1 handlers
  run with no changes on the new transport.

Tests: h2_server_test.v drives the server through an in-memory blocking pipe
with the existing HTTP/2 client (GET, POST with a body, non-200 status, all
round-trip). server_tls_test.v adds a TLS + ALPN end-to-end test asserting
http.fetch(enable_http2: true) negotiates h2 against the same listener that
still serves HTTP/1.1 to non-h2 clients. Full vlib/net/http suite is green on
both backends; passes under -W -cstrict -cc clang.

This is opt-in and additive: with enable_http2 unset (or for non-TLS servers),
behaviour is exactly as before. Stream multiplexing with a background reader is
a planned follow-up (this driver serializes requests).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* net.http: server-side h2 send flow control + gate Windows ALPN test

Address two findings from review of the server-side HTTP/2 PR:

- Server response DATA now respects the HTTP/2 send flow-control windows
  (RFC 7540 Section 6.9). send_body bounds each DATA frame by
  min(connection, stream) window, decrements both after sending, and waits for
  WINDOW_UPDATE (servicing SETTINGS / PING / WINDOW_UPDATE, and a RST_STREAM for
  the stream being written) when a window is exhausted. apply_settings now also
  adjusts every active stream's send window by the delta when the peer changes
  SETTINGS_INITIAL_WINDOW_SIZE (Section 6.9.2). Previously a client that lowered
  its initial window could be sent more DATA than permitted and reset the stream
  with FLOW_CONTROL_ERROR.

- Gate test_server_tls_h2_negotiation so it skips on the default Windows
  configuration: the SChannel client does not advertise ALPN, so it cannot
  negotiate HTTP/2 and the version assertion would fail. The path stays covered
  with `-d no_vschannel` (mbedtls client), matching how the rest of the suite
  treats the SChannel limitation.

Adds test_h2_server_respects_send_window: a raw client advertises
SETTINGS_INITIAL_WINDOW_SIZE=10, and the test asserts the server's first DATA
frame is <= 10 bytes and that the full 100-byte body is delivered after a
WINDOW_UPDATE.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Richard Wheeler <quaesitor.scientiam@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
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.

2 participants