Skip to content

net.http: add synchronous HTTP/2 client connection (H2Conn)#27361

Merged
JalonSolov merged 2 commits into
vlang:masterfrom
quaesitor-scientiam:net-http2-client-pr
Jun 6, 2026
Merged

net.http: add synchronous HTTP/2 client connection (H2Conn)#27361
JalonSolov merged 2 commits into
vlang:masterfrom
quaesitor-scientiam:net-http2-client-pr

Conversation

@quaesitor-scientiam

Copy link
Copy Markdown
Contributor

What

Adds a minimal, synchronous, single-stream HTTP/2 client connection to
net.http, building on the now-merged ALPN (#27343), HPACK (#27353), and
frame-codec (#27356) PRs.

Purely additive — new files in the http module, no change to any existing
code path. Nothing wires it into http.fetch yet (that ALPN shim is the next
PR), so there is no user-visible behaviour change.

Files

File Contents
h2_conn.v H2Transport interface, H2Conn, H2ClientRequest/H2ClientResponse, the request/response state machine
h2_conn_test.v Hermetic tests over an in-memory transport

Design

  • H2Transport interface — the byte transport the connection runs over. Its
    read/write signatures match net.ssl.SSLConn, so an ALPN-negotiated h2
    TLS socket satisfies it directly; tests use an in-memory mock, so the
    connection logic is exercised without a socket.
  • Handshake — connection preface + SETTINGS, sent lazily on the first
    request.
  • H2Conn.do(req) — HPACK-encodes the request headers, sends HEADERS (plus
    DATA for a body, chunked to the peer's SETTINGS_MAX_FRAME_SIZE and bounded
    by the connection flow-control window), then reads frames until the stream
    closes.
  • Connection-frame servicing, inline: SETTINGS (apply + ACK), PING (echo
    ACK), WINDOW_UPDATE (grow the send window), GOAWAY (fail). Receive-side flow
    control is replenished with a WINDOW_UPDATE per DATA frame.
  • CONTINUATION reassembly for response header blocks; RST_STREAM on the
    request stream is surfaced as an error.

This is deliberately synchronous and single-stream — the smallest shape that
is genuinely useful and fully testable. Stream multiplexing (with a background
reader thread) is a clear follow-up.

Tests

h2_conn_test.v (internal module http test) drives the client over a
MockTransport that plays a scripted server side and records what the client
writes. Coverage:

  • basic GET (asserts the preface, the client SETTINGS, and the request
    pseudo-headers by HPACK-decoding the client's HEADERS frame),
  • multi-DATA-frame response body,
  • POST with a request body (verifies HEADERS-without-END_STREAM then a DATA
    frame with END_STREAM),
  • GOAWAY and RST_STREAM surfaced as errors,
  • CONTINUATION-split response headers,
  • PING answered with a PING ACK.
./vnew test vlib/net/http/h2_conn_test.v                       # passes
./vnew -W -cstrict -cc clang test vlib/net/http/h2_conn_test.v # passes
./vnew -silent test vlib/net/http/                             # 17 passed, 1 skipped (Windows)

Roadmap

Next steps, each its own PR:

  1. Wire this into http.fetch via ALPN (h2 negotiated → use H2Conn, else the
    existing HTTP/1.1 path) — the user-facing unification.
  2. Stream multiplexing + connection pooling (the HTTP/2 performance win).
  3. Server-side HTTP/2.

🤖 Generated with Claude Code

Build on the ALPN (vlang#27343), HPACK (vlang#27353), and frame-codec (vlang#27356) PRs to add
a minimal, synchronous, single-stream HTTP/2 client. Additive only: new files in
the http module, no change to existing code paths. Nothing wires it into
http.fetch yet (that ALPN shim is a follow-up), so there is no user-visible
behaviour change.

- H2Transport interface — the byte transport the connection runs over. Its
  read/write signatures match net.ssl.SSLConn, so an ALPN-negotiated `h2` TLS
  socket satisfies it directly; tests use an in-memory mock, so the connection
  is exercised without a socket.
- Connection preface + SETTINGS handshake (sent lazily on the first request).
- H2Conn.do(req): HPACK-encodes the request headers, sends HEADERS (plus DATA
  for a body, chunked to the peer's max frame size and bounded by the
  connection flow-control window), then reads frames until the stream closes.
- Inline servicing of connection-level frames: SETTINGS (apply + ACK), PING
  (echo ACK), WINDOW_UPDATE (grow the send window), GOAWAY (fail). Receive-side
  flow control is replenished with a WINDOW_UPDATE per DATA frame.
- CONTINUATION reassembly for response header blocks; RST_STREAM on the request
  stream is surfaced as an error.

Hermetic tests over the mock transport cover: a basic GET, a multi-DATA
response, a POST with a body, GOAWAY, RST_STREAM, CONTINUATION-split response
headers, and PING ACK. Passes under -W -cstrict -cc clang; the full
vlib/net/http suite is green.

Not included here, by design (follow-ups): stream multiplexing with a
background reader thread, connection pooling, wiring into http.fetch via ALPN,
and the server side.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b62a632e19

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread vlib/net/http/h2_conn.v Outdated
Comment thread vlib/net/http/h2_conn.v
Address two send-path correctness gaps in H2Conn:

- Split request header blocks larger than the peer's SETTINGS_MAX_FRAME_SIZE
  into a HEADERS frame followed by CONTINUATION frames (RFC 7540 Section 4.3),
  via send_header_block. Previously a large header set (e.g. big Cookie or
  Authorization) was sent as one oversized HEADERS frame that a compliant peer
  would reject.

- Track the per-stream send window in addition to the connection window
  (RFC 7540 Section 6.9). DATA is now bounded by min(connection, stream)
  window, stream-level WINDOW_UPDATE credits the active stream, and a change to
  SETTINGS_INITIAL_WINDOW_SIZE retroactively adjusts the stream window by the
  delta (Section 6.9.2). Previously only the connection window was tracked, so
  a peer lowering the initial window or granting stream-level updates could be
  overrun or could stall the client.

Adds tests for a request whose header block spans HEADERS + CONTINUATION, and
for a body larger than the initial window that resumes after the peer grows
both windows (no DATA frame exceeding the max frame size).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@JalonSolov JalonSolov merged commit 03c6723 into vlang:master Jun 6, 2026
77 of 86 checks passed
quaesitor-scientiam pushed a commit to quaesitor-scientiam/v that referenced this pull request Jun 6, 2026
Wire the HTTP/2 client connection into the existing fetch path so a single
http.fetch call transparently speaks HTTP/2 when the server supports it.

- New opt-in field `enable_http2` on Request and FetchConfig (default false, so
  existing callers are unaffected and nothing changes on the wire).
- In net_ssl_do, when enabled, advertise ALPN `h2, http/1.1`. After the
  handshake, if the server selected `h2`, run the request over H2Conn; otherwise
  fall back to the existing HTTP/1.1 path unchanged.
- Conversion glue (h2_client.v, pure/backend-agnostic): build an H2 request from
  a net.http Request — lowercasing header names, dropping hop-by-hop headers,
  mapping Host to the :authority pseudo-header, and collapsing cookies — and
  convert the H2 response back to a net.http Response (decoding Content-Encoding
  the same way the HTTP/1.1 path does, and reporting version 2.0).

Usage:

    resp := http.fetch(url: 'https://example.com/', enable_http2: true)!
    assert resp.version() == .v2_0 // when the server offers h2

Tests: pure unit tests for the request/response conversions, a full glue
round-trip (Request -> H2Conn over an in-memory transport -> Response), and an
opt-in end-to-end test against a real h2 server (run with `-d network`).
Verified manually against https://www.google.com/ (HTTP/2.0, 200), with the
default path still selecting HTTP/1.1. Passes under -W -cstrict -cc clang.

Depends on the H2Conn client (vlang#27361).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
JalonSolov pushed a commit that referenced this pull request Jun 6, 2026
* net.http: negotiate HTTP/2 in fetch via ALPN (opt-in)

Wire the HTTP/2 client connection into the existing fetch path so a single
http.fetch call transparently speaks HTTP/2 when the server supports it.

- New opt-in field `enable_http2` on Request and FetchConfig (default false, so
  existing callers are unaffected and nothing changes on the wire).
- In net_ssl_do, when enabled, advertise ALPN `h2, http/1.1`. After the
  handshake, if the server selected `h2`, run the request over H2Conn; otherwise
  fall back to the existing HTTP/1.1 path unchanged.
- Conversion glue (h2_client.v, pure/backend-agnostic): build an H2 request from
  a net.http Request — lowercasing header names, dropping hop-by-hop headers,
  mapping Host to the :authority pseudo-header, and collapsing cookies — and
  convert the H2 response back to a net.http Response (decoding Content-Encoding
  the same way the HTTP/1.1 path does, and reporting version 2.0).

Usage:

    resp := http.fetch(url: 'https://example.com/', enable_http2: true)!
    assert resp.version() == .v2_0 // when the server offers h2

Tests: pure unit tests for the request/response conversions, a full glue
round-trip (Request -> H2Conn over an in-memory transport -> Response), and an
opt-in end-to-end test against a real h2 server (run with `-d network`).
Verified manually against https://www.google.com/ (HTTP/2.0, 200), with the
default path still selecting HTTP/1.1. Passes under -W -cstrict -cc clang.

Depends on the H2Conn client (#27361).

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

* net.http: honor explicit Host and keep streaming on HTTP/1.1 for h2 fetch

Address two send-path correctness gaps in the HTTP/2 fetch shim:

- Derive the :authority pseudo-header from an explicit Host header when the
  caller set one, matching the HTTP/1.1 path. Previously virtual-host /
  host-override requests sent the URL host as :authority over HTTP/2 even though
  they worked over HTTP/1.1.

- Do not negotiate HTTP/2 for requests that use streaming response callbacks
  (on_progress / on_progress_body) or stop limits (stop_copying_limit /
  stop_receiving_limit). The HTTP/2 path buffers the full response, so those
  would be silently ignored. The ALPN offer is now gated on this before
  negotiation — a server cannot select h2 for such requests — so they keep the
  existing HTTP/1.1 streaming behavior. (Streaming over HTTP/2 is a planned
  follow-up.) The enable_http2 docs note this.

Adds tests for Host-derived authority and for uses_response_streaming().

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

---------

Co-authored-by: Richard Wheeler <quaesitor.scientiam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dy-tea pushed a commit to wenxuanjun/v that referenced this pull request Jun 19, 2026
)

* net.http: add synchronous HTTP/2 client connection (H2Conn)

Build on the ALPN (vlang#27343), HPACK (vlang#27353), and frame-codec (vlang#27356) PRs to add
a minimal, synchronous, single-stream HTTP/2 client. Additive only: new files in
the http module, no change to existing code paths. Nothing wires it into
http.fetch yet (that ALPN shim is a follow-up), so there is no user-visible
behaviour change.

- H2Transport interface — the byte transport the connection runs over. Its
  read/write signatures match net.ssl.SSLConn, so an ALPN-negotiated `h2` TLS
  socket satisfies it directly; tests use an in-memory mock, so the connection
  is exercised without a socket.
- Connection preface + SETTINGS handshake (sent lazily on the first request).
- H2Conn.do(req): HPACK-encodes the request headers, sends HEADERS (plus DATA
  for a body, chunked to the peer's max frame size and bounded by the
  connection flow-control window), then reads frames until the stream closes.
- Inline servicing of connection-level frames: SETTINGS (apply + ACK), PING
  (echo ACK), WINDOW_UPDATE (grow the send window), GOAWAY (fail). Receive-side
  flow control is replenished with a WINDOW_UPDATE per DATA frame.
- CONTINUATION reassembly for response header blocks; RST_STREAM on the request
  stream is surfaced as an error.

Hermetic tests over the mock transport cover: a basic GET, a multi-DATA
response, a POST with a body, GOAWAY, RST_STREAM, CONTINUATION-split response
headers, and PING ACK. Passes under -W -cstrict -cc clang; the full
vlib/net/http suite is green.

Not included here, by design (follow-ups): stream multiplexing with a
background reader thread, connection pooling, wiring into http.fetch via ALPN,
and the server side.

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

* net.http: HTTP/2 client header splitting and per-stream flow control

Address two send-path correctness gaps in H2Conn:

- Split request header blocks larger than the peer's SETTINGS_MAX_FRAME_SIZE
  into a HEADERS frame followed by CONTINUATION frames (RFC 7540 Section 4.3),
  via send_header_block. Previously a large header set (e.g. big Cookie or
  Authorization) was sent as one oversized HEADERS frame that a compliant peer
  would reject.

- Track the per-stream send window in addition to the connection window
  (RFC 7540 Section 6.9). DATA is now bounded by min(connection, stream)
  window, stream-level WINDOW_UPDATE credits the active stream, and a change to
  SETTINGS_INITIAL_WINDOW_SIZE retroactively adjusts the stream window by the
  delta (Section 6.9.2). Previously only the connection window was tracked, so
  a peer lowering the initial window or granting stream-level updates could be
  overrun or could stall the client.

Adds tests for a request whose header block spans HEADERS + CONTINUATION, and
for a body larger than the initial window that resumes after the peer grows
both windows (no DATA frame exceeding the max frame size).

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

---------

Co-authored-by: Richard Wheeler <quaesitor.scientiam@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
dy-tea pushed a commit to wenxuanjun/v that referenced this pull request Jun 19, 2026
* net.http: negotiate HTTP/2 in fetch via ALPN (opt-in)

Wire the HTTP/2 client connection into the existing fetch path so a single
http.fetch call transparently speaks HTTP/2 when the server supports it.

- New opt-in field `enable_http2` on Request and FetchConfig (default false, so
  existing callers are unaffected and nothing changes on the wire).
- In net_ssl_do, when enabled, advertise ALPN `h2, http/1.1`. After the
  handshake, if the server selected `h2`, run the request over H2Conn; otherwise
  fall back to the existing HTTP/1.1 path unchanged.
- Conversion glue (h2_client.v, pure/backend-agnostic): build an H2 request from
  a net.http Request — lowercasing header names, dropping hop-by-hop headers,
  mapping Host to the :authority pseudo-header, and collapsing cookies — and
  convert the H2 response back to a net.http Response (decoding Content-Encoding
  the same way the HTTP/1.1 path does, and reporting version 2.0).

Usage:

    resp := http.fetch(url: 'https://example.com/', enable_http2: true)!
    assert resp.version() == .v2_0 // when the server offers h2

Tests: pure unit tests for the request/response conversions, a full glue
round-trip (Request -> H2Conn over an in-memory transport -> Response), and an
opt-in end-to-end test against a real h2 server (run with `-d network`).
Verified manually against https://www.google.com/ (HTTP/2.0, 200), with the
default path still selecting HTTP/1.1. Passes under -W -cstrict -cc clang.

Depends on the H2Conn client (vlang#27361).

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

* net.http: honor explicit Host and keep streaming on HTTP/1.1 for h2 fetch

Address two send-path correctness gaps in the HTTP/2 fetch shim:

- Derive the :authority pseudo-header from an explicit Host header when the
  caller set one, matching the HTTP/1.1 path. Previously virtual-host /
  host-override requests sent the URL host as :authority over HTTP/2 even though
  they worked over HTTP/1.1.

- Do not negotiate HTTP/2 for requests that use streaming response callbacks
  (on_progress / on_progress_body) or stop limits (stop_copying_limit /
  stop_receiving_limit). The HTTP/2 path buffers the full response, so those
  would be silently ignored. The ALPN offer is now gated on this before
  negotiation — a server cannot select h2 for such requests — so they keep the
  existing HTTP/1.1 streaming behavior. (Streaming over HTTP/2 is a planned
  follow-up.) The enable_http2 docs note this.

Adds tests for Host-derived authority and for uses_response_streaming().

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

---------

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