## Problem Windows builds break with a current local toolchain (Scoop LLVM 22.1.8, CMake 4.4.0, VS 2026), in two independent ways: 1. The build stops at curl's deliberate guard: `#error "no non-blocking method was found/used/set"` in `third-party/curl/lib/nonblock.c`. 2. From the second configure onward, `cmake --build` re-runs CMake in an endless loop (observed 42 consecutive reconfigure cycles in a single build). Likely the same mechanism behind the "endlessly building" VS 2026 note in `docs/setup/dev/vs.md`. ## Root cause 1. `third-party/curl/CMake/CurlTests.c` passes `int *` to `ioctlsocket()`, whose third parameter is `u_long *`. Clang 22 promotes `-Wincompatible-pointer-types` to a hard error in C, so the `HAVE_IOCTLSOCKET_FIONBIO` try_compile silently fails and `curl_config.h` never defines it. Upstream CI does not see this because the windows-2022 runner image ships an older LLVM. GCC 14 promotes the same warning to a hard error, which is very likely the `CurlTests.c.obj` failure reported from MSYS2 in open-goal/jak-project#3551. Upstream curl hit the identical problem with GCC 14 and fixed the probe in curl 8.8.0 (curl/curl#13578). 2. The root CMakeLists copies the build tree's `compile_commands.json` into `<src>/build/` for clangd using `configure_file()`, which registers its input as a configure dependency. CMake rewrites `compile_commands.json` late in every generation, after `CTestTestfile.cmake` and `cmake_install.cmake` (outputs of the same Ninja regen rule), so once the dependency is registered the rule is deterministically dirty and every `ninja` invocation re-runs CMake. A pristine first configure is safe (the file does not exist yet, so the `if(EXISTS ...)` guard skips the copy), which is why the loop looks machine- or IDE-specific. ## Fix 1. Per review, re-vendor `third-party/curl` at the `curl-8_21_0` tag (previously `curl-8_3_0`), which carries the upstream probe fix plus two years of upstream development; `vendor.yaml` updated to match. Adjustments the version jump forced: - curl 8.15 removed the native macOS Secure Transport backend (`CURL_USE_SECTRANSP`), so macOS now builds curl against OpenSSL like Linux. The two macOS workflows install Homebrew `openssl@3` and export `OPENSSL_ROOT_DIR` (keg-only), and the macOS setup docs gained the same two lines. - `CURL_BROTLI` / `CURL_ZSTD` switched to AUTO-detection in curl 8.10; pinned OFF to keep the previous no-compression behavior and avoid silently linking whatever the CI images happen to have. - curl's new top-level `BUILD_EXAMPLES` cache option (default ON) leaked into discord-rpc's identically named option and broke configure at a nonexistent `examples/send-presence` directory; pinned OFF ahead of the third-party subdirectories. The diff is dominated by the mechanical tag-tree swap under `third-party/curl` (linguist-vendored, collapsed in review). The hand-written changes are `CMakeLists.txt`, the two macOS workflows, `docs/setup/system/macos.md`, and `vendor.yaml`. 2. Swap `configure_file()` for `file(COPY ...)`: the same clangd copy with no configure dependency registered. (`file(COPY_FILE ... ONLY_IF_DIFFERENT)` would be cleaner still but requires CMake 3.21, above the declared `cmake_minimum_required(VERSION 3.10)`.) ## Test plan - [x] Fresh `cmake --preset Release-windows-clang` (LLVM 22, no cache seeding) completes and logs `Enabled SSL backends: Schannel`; the FIONBIO probe passes without the previous `#error` - [x] Full Windows Release build from scratch in the branch worktree (all 1422 targets) - [x] goalc-test suite: 1509 passed, 0 failed - [x] Second consecutive configure with `compile_commands.json` present: the regen rule in `build.ninja` has no `compile_commands.json` input; `<src>/build/compile_commands.json` is still refreshed for clangd - [x] Repeated `ninja` invocations after a full build no longer re-run CMake - [x] macOS Intel and ARM CI green (first exercise of the OpenSSL backend switch) --- I work off a self-hosted forge, so this GitHub account is quiet; the configure logs and ninja dirty-node traces from the investigation are available if anyone wants the raw data. (AI-assisted)
12 KiB
Vendored
Generated
HTTP3 (and QUIC)
Resources
HTTP/3 Explained - the online free book describing the protocols involved.
quicwg.org - home of the official protocol drafts
QUIC libraries
QUIC libraries we are using:
quiche - EXPERIMENTAL
Experimental
HTTP/3 support using quiche in curl is considered EXPERIMENTAL until further notice. Only the ngtcp2 backend is not experimental.
Further development and tweaking of the HTTP/3 support in curl happens in the master branch using pull-requests like ordinary changes.
To fix before we remove the experimental label:
- the used QUIC library needs to consider itself non-beta
- it is fine to "leave" individual backends as experimental if necessary
ngtcp2 version
Building curl with ngtcp2 involves 3 components: ngtcp2 itself, nghttp3
and a QUIC supporting TLS library. The supported TLS libraries are covered
below.
While any version of ngtcp2 and nghttp3 from v1.0.0 on are expected to
work, using the latest versions often brings functional and performance
improvements.
The build examples use $NGHTTP3_VERSION and $NGTCP2_VERSION as
placeholders for the version you build.
Build with OpenSSL or fork
OpenSSL v3.5.0+ requires ngtcp2 v1.12.0+. Earlier versions do not work.
Build OpenSSL (v3.5.0+) or fork AWS-LC, BoringSSL, LibreSSL or quictls:
# Instructions for OpenSSL v3.5.0+
% git clone --depth 1 --branch openssl-$OPENSSL_VERSION https://github.com/openssl/openssl
% cd openssl
% ./config --prefix=/path/to/openssl --libdir=lib
% make
% make install
Build nghttp3:
% cd ..
% git clone --depth 1 --branch $NGHTTP3_VERSION https://github.com/ngtcp2/nghttp3
% cd nghttp3
% git submodule update --init
% autoreconf -fi
% ./configure --prefix=/path/to/nghttp3 --enable-lib-only
% make
% make install
Build ngtcp2:
% cd ..
% git clone --depth 1 --branch $NGTCP2_VERSION https://github.com/ngtcp2/ngtcp2
% cd ngtcp2
% autoreconf -fi
# Change --with-openssl to --with-boringssl for AWS-LC and BoringSSL
% ./configure PKG_CONFIG_PATH=/path/to/openssl/lib/pkgconfig:/path/to/nghttp3/lib/pkgconfig LDFLAGS="-Wl,-rpath,/path/to/openssl/lib" \
--prefix=/path/to/ngtcp2 --enable-lib-only --with-openssl
% make
% make install
Build curl (with autotools):
% cd ..
% git clone --depth 1 https://github.com/curl/curl
% cd curl
% autoreconf -fi
% ./configure PKG_CONFIG_PATH=/path/to/openssl/lib/pkgconfig LDFLAGS="-Wl,-rpath,/path/to/openssl/lib" \
--with-openssl=/path/to/openssl --with-ngtcp2=/path/to/ngtcp2 --with-nghttp3=/path/to/nghttp3
% make
% make install
Build curl (with CMake):
% cd ..
% git clone --depth 1 https://github.com/curl/curl
% cd curl
% PKG_CONFIG_PATH=/path/to/openssl/lib/pkgconfig:/path/to/ngtcp2/lib/pkgconfig:/path/to/nghttp3/lib/pkgconfig cmake -B bld \
-DOPENSSL_ROOT_DIR=/path/to/openssl -DUSE_NGTCP2=ON
% cmake --build bld
Build with GnuTLS
Build GnuTLS:
% git clone --depth 1 https://gitlab.com/gnutls/gnutls
% cd gnutls
% ./bootstrap
% ./configure --prefix=/path/to/gnutls
% make
% make install
Build nghttp3:
% cd ..
% git clone --depth 1 --branch $NGHTTP3_VERSION https://github.com/ngtcp2/nghttp3
% cd nghttp3
% git submodule update --init
% autoreconf -fi
% ./configure --prefix=/path/to/nghttp3 --enable-lib-only
% make
% make install
Build ngtcp2:
% cd ..
% git clone --depth 1 --branch $NGTCP2_VERSION https://github.com/ngtcp2/ngtcp2
% cd ngtcp2
% autoreconf -fi
% ./configure PKG_CONFIG_PATH=/path/to/gnutls/lib/pkgconfig:/path/to/nghttp3/lib/pkgconfig LDFLAGS="-Wl,-rpath,/path/to/gnutls/lib" \
--prefix=/path/to/ngtcp2 --enable-lib-only --with-gnutls
% make
% make install
Build curl (with autotools):
% cd ..
% git clone --depth 1 https://github.com/curl/curl
% cd curl
% autoreconf -fi
% ./configure PKG_CONFIG_PATH=/path/to/gnutls/lib/pkgconfig --with-gnutls=/path/to/gnutls --with-ngtcp2=/path/to/ngtcp2 --with-nghttp3=/path/to/nghttp3
% make
% make install
Build curl (with CMake):
% cd ..
% git clone --depth 1 https://github.com/curl/curl
% cd curl
% PKG_CONFIG_PATH=/path/to/gnutls/lib/pkgconfig:/path/to/ngtcp2/lib/pkgconfig:/path/to/nghttp3/lib/pkgconfig cmake -B bld -DCURL_USE_GNUTLS=ON -DUSE_NGTCP2=ON
% cmake --build bld
Build with wolfSSL
Build wolfSSL:
% git clone --depth 1 https://github.com/wolfSSL/wolfssl
% cd wolfssl
% autoreconf -fi
% ./configure --prefix=/path/to/wolfssl --enable-quic --enable-session-ticket --enable-earlydata --enable-psk --enable-harden --enable-altcertchains
% make
% make install
Build nghttp3:
% cd ..
% git clone --depth 1 --branch $NGHTTP3_VERSION https://github.com/ngtcp2/nghttp3
% cd nghttp3
% git submodule update --init
% autoreconf -fi
% ./configure --prefix=/path/to/nghttp3 --enable-lib-only
% make
% make install
Build ngtcp2:
% cd ..
% git clone --depth 1 --branch $NGTCP2_VERSION https://github.com/ngtcp2/ngtcp2
% cd ngtcp2
% autoreconf -fi
% ./configure PKG_CONFIG_PATH=/path/to/wolfssl/lib/pkgconfig:/path/to/nghttp3/lib/pkgconfig LDFLAGS="-Wl,-rpath,/path/to/wolfssl/lib" \
--prefix=/path/to/ngtcp2 --enable-lib-only --with-wolfssl
% make
% make install
Build curl (with autotools):
% cd ..
% git clone --depth 1 https://github.com/curl/curl
% cd curl
% autoreconf -fi
% ./configure PKG_CONFIG_PATH=/path/to/wolfssl/lib/pkgconfig --with-wolfssl=/path/to/wolfssl --with-ngtcp2=/path/to/ngtcp2 --with-nghttp3=/path/to/nghttp3
% make
% make install
Build curl (with CMake):
% cd ..
% git clone --depth 1 https://github.com/curl/curl
% cd curl
% PKG_CONFIG_PATH=/path/to/wolfssl/lib/pkgconfig:/path/to/ngtcp2/lib/pkgconfig:/path/to/nghttp3/lib/pkgconfig cmake -B bld -DCURL_USE_WOLFSSL=ON -DUSE_NGTCP2=ON
% cmake --build bld
quiche version
quiche support is EXPERIMENTAL
Since the quiche build manages its dependencies, curl can be built against the latest version. You are probably able to build against their main branch, but in case of problems, we recommend their latest release tag.
Build
Build quiche and BoringSSL (described here for quiche v0.29.1, the locations where BoringSSL is to be found vary with version):
% git clone --depth 1 --branch 0.29.1 --recursive https://github.com/cloudflare/quiche
% cd quiche
% cargo build --package quiche --release --features ffi,pkg-config-meta,qlog
% ln -s libquiche.so target/release/libquiche.so.0
% mkdir -p boringssl/lib
% find target/release \( -name libcrypto.a -o -name libssl.a \) -exec ln -vnf -- '{}' boringssl/lib \;
% find target/release/build/boring-sys-*/out/boringssl/src -maxdepth 1 \( -name include \) -exec ln -vsf -- '../{}' boringssl \;
Build curl:
% cd ..
% git clone --depth 1 https://github.com/curl/curl
% cd curl
% autoreconf -fi
% ./configure --with-openssl=$PWD/../quiche/boringssl \
--with-quiche=$PWD/../quiche/target/release
% make
% make install
If make install results in Permission denied error, you need to prepend
it with sudo.
--http3
Use only HTTP/3:
% curl --http3-only https://example.org:4433/
Use HTTP/3 with fallback to HTTP/2 or HTTP/1.1 (see "HTTPS eyeballing" below):
% curl --http3 https://example.org:4433/
Upgrade via Alt-Svc:
% curl --alt-svc altsvc.cache https://curl.se/
See this list of public HTTP/3 servers
HTTPS eyeballing
With option --http3 curl attempts earlier HTTP versions as well should the
connect attempt via HTTP/3 fail "fast enough". This strategy is similar
to IPv4/6 happy eyeballing where the alternate address family is used in
parallel after a short delay.
The IPv4/6 eyeballing has a default of 200ms and you may override that via
--happy-eyeballs-timeout-ms value. Since HTTP/3 is still relatively new, we
decided to use this timeout also for the HTTP eyeballing - with a slight
twist.
The happy-eyeballs-timeout-ms value is the hard timeout, meaning after
that time expired, a TLS connection is opened in addition to negotiate HTTP/2
or HTTP/1.1. At half of that value - currently - is the soft timeout. The
soft timeout fires, when there has been no data at all seen from the
server on the HTTP/3 connection.
Without you specifying anything, the hard timeout is 200ms and the soft is 100ms:
- Ideally, the whole QUIC handshake happens and curl has an HTTP/3 connection in less than 100ms.
- When QUIC is not supported (or UDP does not work for this network path), no reply is seen and the HTTP/2 TLS+TCP connection starts 100ms later.
- In the worst case, UDP replies start before 100ms, but drag on. This starts the TLS+TCP connection after 200ms.
- When the QUIC handshake fails, the TLS+TCP connection is attempted right away. For example, when the QUIC server presents the wrong certificate.
The whole transfer only fails, when both QUIC and TLS+TCP fail to handshake or time out.
Note that all this happens in addition to IP version happy eyeballing. If the name resolution for the server gives more than one IP address, curl tries all those until one succeeds - as with all other protocols. If those IP addresses contain both IPv6 and IPv4, those attempts happen, delayed, in parallel (the actual eyeballing).
Known Bugs
Check out the list of known HTTP3 bugs.
HTTP/3 Test server
This is not advice on how to run anything in production. This is for development and experimenting.
Prerequisite(s)
An existing local HTTP/1.1 server that hosts files. Preferably also a few huge
ones. You can easily create huge local files like truncate -s=8G 8GB - they
are huge but do not occupy that much space on disk since they are big holes.
In a Debian setup you can install apache2. It runs on port 80 and has a
document root in /var/www/html. Download the 8GB file from apache with curl localhost/8GB -o dev/null
In this description we setup and run an HTTP/3 reverse-proxy in front of the HTTP/1 server.
Setup
You can select either or both of these server solutions.
nghttpx
Get, build and install quictls, nghttp3 and ngtcp2 as described above.
Get, build and install nghttp2:
% git clone --depth 1 https://github.com/nghttp2/nghttp2
% cd nghttp2
% autoreconf -fi
% PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/path/to/quictls/lib/pkgconfig:/path/to/nghttp3/lib/pkgconfig:/path/to/ngtcp2/lib/pkgconfig \
LDFLAGS=-L/path/to/quictls/lib CFLAGS=-I/path/to/quictls/include ./configure --enable-maintainer-mode \
--prefix=/path/to/nghttp2 --disable-shared --enable-app --enable-http3 --without-jemalloc --without-libxml2 --without-systemd
% make && make install
Run the local h3 server on port 9443, make it proxy all traffic through to HTTP/1 on localhost port 80. For local toying, we can use the test cert that exists in curl's test dir.
% CERT=/path/to/stunnel.pem
% $HOME/bin/nghttpx $CERT $CERT --backend=localhost,80 \
--frontend="localhost,9443;quic"
Caddy
Install Caddy. For easiest use, the binary should be either in your PATH or your current directory.
Create a Caddyfile with the following content:
localhost:7443 {
respond "Hello, world! you are using {http.request.proto}"
}
Then run Caddy:
% ./caddy start
Making requests to https://localhost:7443 should tell you which protocol is
being used.
You can change the hard-coded response to something more useful by replacing
respond with reverse_proxy or file_server, for example: reverse_proxy localhost:80