Skip to content

net.http: add server-side HTTP/2 (ALPN + h2 frame demux)#27382

Merged
JalonSolov merged 2 commits into
vlang:masterfrom
quaesitor-scientiam:net-http-server-h2-pr
Jun 8, 2026
Merged

net.http: add server-side HTTP/2 (ALPN + h2 frame demux)#27382
JalonSolov merged 2 commits into
vlang:masterfrom
quaesitor-scientiam:net-http-server-h2-pr

Conversation

@quaesitor-scientiam

Copy link
Copy Markdown
Contributor

What

Lets net.http.Server speak HTTP/2, building on the TLS-termination PR
(#27373). When a client negotiates h2 via ALPN, the server serves the request
through a new HTTP/2 driver; otherwise the existing HTTP/1.1 path is unchanged.

This completes a full client and server HTTP/2 stack in pure V (framing,
HPACK, ALPN, TLS, client, server) with no external dependencies.

mut srv := &http.Server{
    cert:                   cert_pem
    cert_key:               key_pem
    in_memory_verification: true
    enable_http2:           true     // advertise ALPN h2, http/1.1
    handler:                MyHandler{}
}
srv.listen_and_serve()

Changes

File Change
h2_server.v H2ServerConn: preface, SETTINGS, frame loop, HEADERS→Request, Response→HEADERS/DATA
server.v new opt-in enable_http2 field
server_tls_notd_use_openssl.v advertise ALPN when enabled; dispatch to the h2 driver when the handshake selects h2
h2_server_test.v, server_tls_test.v tests

Design

  • Opt-in, additive. With enable_http2 unset (or for non-TLS servers),
    behaviour is exactly as before — no ALPN advertised, no new code on that path.
  • The Handler interface is untouched. Handler.handle(Request) Response
    is invoked once per request stream. req.url is the request-target (the
    :path pseudo-header) and Host comes from :authority, so existing
    HTTP/1.1 handlers work as-is on the new transport.
  • Serial for v1. The driver advertises SETTINGS_MAX_CONCURRENT_STREAMS=1,
    so the peer sends one request at a time. Stream multiplexing with a
    background reader is a planned follow-up.
  • Robustness: HEADERS+CONTINUATION assembly with the §6.10 ordering rule,
    flow-control replenishment on DATA, hop-by-hop response headers dropped
    (§8.1.2.2), an 8 MiB request-body cap with RST_STREAM(REFUSED_STREAM), and a
    best-effort GOAWAY on protocol error.
  • mbedtls backend. Like the TLS PR, the h2 server path is on the default
    backend; -d use_openssl keeps the TLS-server stub. An OpenSSL server
    listener is a follow-up.

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, and a non-200
    status all round-trip, exercising the real framing + HPACK on both sides.
  • server_tls_test.v adds a TLS + ALPN end-to-end test: a real HTTPS
    listener with enable_http2: true serves h2 to http.fetch(enable_http2: true) and http/1.1 to a plain http.fetch on the same port.
./vnew test vlib/net/http/h2_server_test.v vlib/net/http/server_tls_test.v   # pass
./vnew -W -cstrict -cc clang test vlib/net/http/h2_server_test.v             # pass
./vnew -silent test vlib/net/http/                                          # 20 passed, 1 skipped (Windows)
./vnew -d use_openssl -silent test vlib/net/http/                           # 20 passed, 1 skipped

Follow-ups

  • Stream multiplexing + a background reader (concurrent streams per connection).
  • OpenSSL server listener (so -d use_openssl can serve TLS / h2 too).

🤖 Generated with Claude Code

Build on TLS termination (vlang#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>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

@quaesitor-scientiam

quaesitor-scientiam commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

Running local codex review given usage uses are exceeded

• Findings

  • High: Windows default clients never exercise the new HTTP/2 TLS path. On Windows, ssl_do() immediately routes to SChannel at /abs?. In
    this repo: vlib/net/http/backend.c.v:9 calls vlib/net/http/backend_vschannel_windows.c.v:21, which builds and sends an HTTP/1.1 request
    and has no ALPN/enable_http2 handling. The new test at vlib/net/http/server_tls_test.v:110 fails in the default Windows configuration:
    resp.version() is HTTP/1.1, not HTTP/2.0. Either SChannel needs ALPN/H2 support, or this test/feature path needs to be gated to -d
    no_vschannel.

  • Medium: Server response DATA ignores HTTP/2 outbound flow control. vlib/net/http/h2_server.v:128 tracks WINDOW_UPDATE into connection and
    stream send windows, but vlib/net/http/h2_server.v:364 sends the whole body chunked only by max frame size, without waiting for/
    decrementing either window. Large responses or clients that lower SETTINGS_INITIAL_WINDOW_SIZE can receive more DATA than permitted and
    reset with FLOW_CONTROL_ERROR.

Tests Run

  • .\vnew.exe test vlib\net\http\h2_server_test.v passed.
  • .\vnew.exe test vlib\net\http\server_tls_test.v failed on Windows default SChannel path: expected HTTP/2.0, got HTTP/1.1.
  • .\vnew.exe -d no_vschannel test vlib\net\http\server_tls_test.v passed.

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>
@quaesitor-scientiam

Copy link
Copy Markdown
Contributor Author

Thanks for running the local review — both addressed in ed34646:

High (Windows ALPN): test_server_tls_h2_negotiation now skips on the default Windows/SChannel configuration ($if windows && !no_vschannel ?), since the SChannel client can't advertise ALPN and so can't negotiate HTTP/2. The path stays covered under -d no_vschannel (mbedtls client), matching how the rest of the suite handles the SChannel limitation. Full SChannel ALPN is a separate follow-up. The server side itself is unaffected (it uses the mbedtls listener regardless).

Medium (server send flow control): send_body now bounds each DATA frame by min(connection, stream) window, decrements both, and waits for WINDOW_UPDATE (servicing SETTINGS/PING/WINDOW_UPDATE and RST_STREAM for the active stream) when a window is exhausted. apply_settings also adjusts active streams' send windows by the delta on SETTINGS_INITIAL_WINDOW_SIZE change (§6.9.2). Added test_h2_server_respects_send_window: a raw client advertises INITIAL_WINDOW_SIZE=10, and the test asserts the first DATA frame is ≤ 10 bytes and the full 100-byte body arrives after a WINDOW_UPDATE.

Verified: -d no_vschannel test server_tls_test.v passes; full vlib/net/http suite green on mbedtls and -d use_openssl; -W -cstrict -cc clang clean.

@quaesitor-scientiam

Copy link
Copy Markdown
Contributor Author

Filed #27383 to track full SChannel ALPN / HTTP/2 support — removing the $if windows && !no_vschannel ? skip added here is listed as one of its acceptance criteria.

@JalonSolov JalonSolov merged commit 35ffbab into vlang:master Jun 8, 2026
76 of 85 checks passed
dy-tea pushed a commit to wenxuanjun/v that referenced this pull request Jun 19, 2026
* net.http: add server-side HTTP/2 (ALPN + h2 frame demux)

Build on TLS termination (vlang#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