Skip to content
Lee Yunjin edited this page Oct 7, 2026 · 1 revision

HTTP/2

Headers: <cwist/app.h>, <cwist/net/http/http2.h>, <cwist/net/http/http2_flow_control.h>

CWIST implements HTTP/2 itself (HPACK, framing, flow control); it does not use nghttp2 on the server side. Handlers are the same as for HTTP/1.1: requests decoded from HTTP/2 streams go through the normal middleware chain and router.

Enabling it

cwist_app_use_https(app, "cert.pem", "key.pem");
cwist_app_use_https2(app, true);   /* h2 over TLS, negotiated with ALPN */

or, without TLS:

cwist_app_use_http2(app, true);    /* h2c on the plain TCP port */
  • h2 (TLS). ALPN offers h2 and http/1.1; clients that do not ask for h2 keep using HTTP/1.1 on the same port. Enabling h2 also applies an HTTP/2-compatible TLS profile (RFC 7540 cipher requirements, server cipher preference).
  • h2c (cleartext). Prior knowledge only: a connection whose first bytes are the HTTP/2 connection preface is served as HTTP/2, anything else as HTTP/1.1. The HTTP/1.1 Upgrade: h2c handshake is not implemented.
  • h2c and HTTPS cannot be enabled on the same port, and h2 requires cwist_app_use_https(); Multiport checks both.

Behavior

Aspect Value
Concurrent streams per connection 100 (SETTINGS_MAX_CONCURRENT_STREAMS)
Initial windows 1.5 MiB connection, 1 MiB per stream; windows adapt to measured RTT
Idle timeout CWIST_HTTP2_IDLE_TIMEOUT_MS, default 300 s
Shutdown GOAWAY, then CWIST_HTTP2_GOAWAY_GRACE_MS (default 2 s) before close
Rapid-reset defense token bucket on client RST_STREAM: burst CWIST_HTTP2_MAX_RST_BURST (100), refill CWIST_HTTP2_MAX_RST_RATE (100/s)
CONTINUATION flood orphaned or endless CONTINUATION sequences are rejected (CVE-2024-27983 test in test_http2)
Output batching frames are batched up to CWIST_H2_BATCH_BYTES (default 64 KiB) per flush
Deferred responses completed on the connection thread; other streams keep being served (Async Handlers)
Idle TLS connections parked like HTTP/1.1 ones; on expiry the connection gets GOAWAY

Conformance: every push to the repository runs h2spec and diffs the result against a pinned baseline (scripts/ci/h2spec-baseline.txt); new failures fail the build.

Header fields whose HPACK index cannot be resolved are dropped and counted in the CWIST_METRIC_H2_HEADERS_DROPPED metric.

Server push

int cwist_http2_push_resource(cwist_http_request *req, const char *path, const char *content_type,
                              const unsigned char *data, size_t data_len);

Call from a handler on an HTTP/2 request: CWIST sends PUSH_PROMISE on the request's stream and the pushed response on a new server stream. The client must allow push (SETTINGS_ENABLE_PUSH). Returns 0 or -1. Most browsers no longer accept push.

Serving HTTP/2 yourself

typedef void (*cwist_http2_request_handler_func)(void *user_ctx, cwist_http_request *req,
                                                 cwist_http_response *res);

cwist_error_t cwist_http2_serve_connection(cwist_https_connection *conn, void *user_ctx,
                                           cwist_http2_request_handler_func handler);
cwist_error_t cwist_http2_serve_connection_ex(cwist_https_connection *conn, void *user_ctx,
                                              cwist_http2_request_handler_func handler,
                                              const cwist_http2_stream_hooks *hooks);

cwist_http2_serve_connection() reads the client preface, decodes streams, calls handler per request and writes the responses. It returns when the connection ends.

Stream hooks

cwist_http2_stream_hooks lets a higher layer take over individual streams; the gRPC server uses it (cwist_grpc_http2_hooks()):

Hook Called when
on_conn_open(user_ctx) / on_conn_close(conn_ctx) the connection starts and ends
on_headers(conn_ctx, req, stream) a request header block completes; return non-NULL to take the stream
on_data(conn_ctx, stream_ctx, data, len, end_stream) a DATA payload arrives for a taken stream (incremental, not buffered)
on_cancel(conn_ctx, stream_ctx) the client sent RST_STREAM
on_poll(conn_ctx, stream_ctx) every dispatcher iteration (enforce deadlines; return nonzero when finished)
next_deadline_ms(conn_ctx) nearest deadline, which bounds the socket wait
on_close(conn_ctx, stream_ctx) the taken stream is torn down

A taken stream writes its own frames, immediately and from its own thread:

int cwist_http2_stream_send_headers(cwist_h2_stream *stream, int status,
                                    const cwist_http2_header *headers, size_t header_count,
                                    int end_stream);
int cwist_http2_stream_send_data(cwist_h2_stream *stream, const unsigned char *data, size_t len);
int cwist_http2_stream_send_trailers(cwist_h2_stream *stream, const cwist_http2_header *trailers,
                                     size_t trailer_count);

send_data splits data to the peer's frame size and window. All three return 0 or -1.

HPACK helpers

h2_decode_integer(), h2_huffman_decode(), h2_decode_string(), h2_static_header(), h2_encode_integer() and h2_encode_string() expose the HPACK primitives (RFC 7541) for tools and tests.

Flow control module

<cwist/net/http/http2_flow_control.h> holds the connection and stream window accounting used by the server: receive and consume tracking, WINDOW_UPDATE decisions, send-window reservation, RTT-based network quality estimates and a pacing allowance. Applications do not need it directly. Handler threads that send on a gRPC stream with no credit wait for WINDOW_UPDATE instead of failing.

HTTP/2 clients

For outgoing requests see HTTP Clients (libcurl-based, with HTTP/2 multiplexing) and gRPC Client (CWIST's own HTTP/2 client).

Clone this wiki locally