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.5.3"}
  ]
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. The transitive Abyss dependency builds a Rust NIF, so source builds also need Rust/Cargo and a working system linker. 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.

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.

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.