Gaoxy HTTP/3

gaoxy_http3 serves Plug applications and Gaoxy HTTP endpoints over QUIC v1 with ALPN h3. It provides an explicit, separate HTTP/3 listener through Gaoxy.HTTP.HTTP3.

Install

Add to mix.exs, then run mix deps.get:

defp deps do
  [
    {:gaoxy_http3, "~> 0.12.1"}
  ]
end

This brings in gaoxy_http, elixir_quic_http3, elixir_quic and ex_ssl. Use Elixir 1.18 or newer with Erlang/OTP crypto and public-key support. Core Abyss 0.9.0 excludes its former DHCP NIF and Rustler build; Gaoxy does not depend on abyss_raw. Keep the matching QUIC/TLS dependency versions selected by Mix.

Serve a Plug

defmodule MyApp.Hello do
  def init(options), do: options

  def call(conn, _options) do
    Plug.Conn.send_resp(conn, 200, "hello over h3\n")
  end
end

Add the listener to your application's supervision tree, using existing PEM certificate and private-key files:

children = [
  {Gaoxy.HTTP.HTTP3,
   id: :https3,
   ip: {127, 0, 0, 1},
   port: 4443,
   tls: %{certfile: "/path/to/cert.pem", keyfile: "/path/to/key.pem"},
   plug: {MyApp.Hello, []}}
]

Supervisor.start_link(children, strategy: :one_for_one)

Use a certificate valid for the hostname your client requests. The listener binds UDP; allow UDP traffic to the configured port. A client must support HTTP/3 and trust the certificate. For example, when curl -V lists HTTP3:

curl --http3-only --cacert /path/to/ca.pem https://localhost:4443/

Options and composition

Supply exactly one of plug: {Module, options} or endpoint: {Module, options}. The latter implements Gaoxy.HTTP.Endpoint. The binding requires a concrete IPv4 tuple; wildcard and IPv6 bindings are currently rejected. The defaults are loopback, port 0 and ID :http3. TLS credentials are required. Transport is :quic, and ALPN is fixed to h3.

Use abyss_options: [event_batch: 16, poll_interval: 5] to tune the backend's bounded event batch and polling interval. The defaults remain 32 events and 10 ms. The same entry belongs inside http3 for ListenerSet and Phoenix. QUIC socket tuning and resource/lifecycle overrides are rejected; see the backend support matrix.

Gaoxy.HTTP.HTTP3.service_spec/1 returns {:ok, service} for use in a shared {Gaoxy, services: [...]} instance alongside TCP or HTTP/1–2 services. Gaoxy.HTTP accepts HTTP/1 and HTTP/2; protocols: [:http3] does not enable H3. Configure TCP HTTPS separately when clients also need H1/H2.

Start with the supplied resource limits. :limits, :http_limits and :h3_limits are maps validated together before activation. H3 defaults allow eight request streams per connection and 32,768-byte header blocks. Dynamic QPACK table capacity and blocked-stream limits must both remain zero.

Request lifetimes remain finite. http_limits: %{request_timeout: :infinity} is rejected with :unsupported_request_timeout. Response-only lifetime overrides remain unsupported (:unsupported_stream_timeout). The native H1/H2 storage opt-in does not apply to H3.

WebSocket and Phoenix

RFC 9220 WebSocket support requires explicit extended_connect: true. For an external Phoenix mount, run the Phoenix Endpoint with server: false and pass its exact socket paths through the compatibility wrapper:

plug: {Gaoxy.PhoenixWebSocket,
       {MyAppWeb.Endpoint, ["/live/websocket", "/socket/websocket"]}},
extended_connect: true

These replace plug: {MyApp.Hello, []} in the listener options. Include only paths declared by your Endpoint. Follow the Phoenix guide for dependencies, Endpoint supervision and socket configuration.

Browser transport selection remains client-controlled; serving a page over H3 does not prove its WebSocket uses H3. See the HTTP/3 guide for protocol limits and the native HTTP guide for shared Plug/endpoint APIs.

For one Phoenix Endpoint across H1/H2/H3, use the adapter's explicit http3: option or Gaoxy.HTTP.ListenerSet; see the Phoenix guide and ListenerSet contract. This package supplies the optional versioned H3 listener provider. HTTP-only consumers do not resolve or load it, and existing Gaoxy.HTTP.HTTP3.service_spec/1 remains a single service builder.