From 444f0f5fbf2c916b7c3f36f51cde55479694949f Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Fri, 10 Jul 2026 15:32:21 -0700 Subject: [PATCH 1/2] Enable HTTP/2 and HTTP/3 across the NSURLSession, libcurl, and WinRT backends - Apple: assumesHTTP3Capable on macOS 11.3+/iOS 14.5+ (races QUIC vs TCP with fallback; h2 already automatic via ALPN). - Linux: runtime curl feature detection -- CURL_HTTP_VERSION_3 with fallback when curl >= 8.0 reports CURL_VERSION_HTTP3, else CURL_HTTP_VERSION_2TLS. Same binary lights up h3 as distros enable it; no vendoring, no size cost. - Windows: pin HttpBaseProtocolFilter.MaxVersion = Http20 explicitly (Windows 10 1607+); Windows.Web.Http has no h3 knob -- documented as arriving with a WinHTTP-based backend. - Readme: per-platform HTTP versions matrix incl. the Android/Cronet and WinHTTP follow-up plans. Only https:// transfers are affected; http:// and file:// stay HTTP/1.1, and every option negotiates downward, so behavior on servers without h2/h3 is unchanged. --- README.md | 16 ++++++++++++++++ Source/UrlRequest_Apple.mm | 9 +++++++++ Source/UrlRequest_Unix.cpp | 20 ++++++++++++++++++++ Source/UrlRequest_Windows_Shared.h | 9 ++++++++- 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a7d0434..b0e2eea 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,22 @@ Platform support: the Apple (`NSURLSession`) and Linux (`libcurl`) backends popu these accessors today; the Windows and Android backends currently always report empty/zero (contributions welcome — the plumbing in `UrlRequest_Base.h` is shared). +## HTTP versions + +`UrlRequest` opts each backend into the newest HTTP version the platform stack supports, +negotiating downward automatically — application code never has to care, and plain +`http://` / `file://` requests are unaffected. + +| Platform | Backend | HTTP/2 | HTTP/3 (QUIC) | +|---|---|---|---| +| macOS / iOS | `NSURLSession` | automatic (ALPN) since macOS 10.11 / iOS 9 | attempted per request via `assumesHTTP3Capable` on macOS 11.3+ / iOS 14.5+; races QUIC vs TCP with fallback | +| Linux | libcurl | `CURL_HTTP_VERSION_2TLS` when the runtime curl has nghttp2 (all mainstream distros) | `CURL_HTTP_VERSION_3` (with fallback) when the runtime curl ≥ 8.0 reports `CURL_VERSION_HTTP3` (e.g. Fedora today; other distros as they enable it) | +| Windows (Win32 + UWP) | `Windows.Web.Http` | `HttpBaseProtocolFilter.MaxVersion = Http20`, negotiated via ALPN; requires Windows 10 1607+ | not exposed by `Windows.Web.Http` (`HttpVersion` caps at `Http20`); planned via a WinHTTP-based backend, which no-ops the h3 flag before Windows 11 and negotiates down | +| Android | `java.net.HttpURLConnection` | not supported by the platform stack (HTTP/1.1 only) | planned via embedded Cronet (h2 + h3 + Brotli); Quest 1 (Android 10) and Pico 4 (Android 10) clear Cronet's Android 8 minimum | + +The Linux detection is at runtime, so the same binary lights up h3 when the system curl +gains it. No UrlLib build options are required. + ## Contributing Please read [CONTRIBUTING.md](./CONTRIBUTING.md) for details on our code of conduct, and diff --git a/Source/UrlRequest_Apple.mm b/Source/UrlRequest_Apple.mm index f6e8ea6..6e12b77 100644 --- a/Source/UrlRequest_Apple.mm +++ b/Source/UrlRequest_Apple.mm @@ -132,6 +132,15 @@ void Open(UrlMethod method, const std::string& url) mutableRequest.HTTPBody = requestBodyData; } + if (@available(macOS 11.3, iOS 14.5, *)) + { + // Let NSURLSession attempt HTTP/3 (QUIC) for this request directly instead of + // waiting to learn h3 support from a prior response's Alt-Svc header; the stack + // races QUIC against TCP and falls back to h2/h1.1 when the server lacks h3. + // HTTP/2 has been automatic via ALPN since iOS 9 / macOS 10.11. + mutableRequest.assumesHTTP3Capable = YES; + } + request = [mutableRequest copy]; __block arcana::task_completion_source taskCompletionSource{}; diff --git a/Source/UrlRequest_Unix.cpp b/Source/UrlRequest_Unix.cpp index 8ed83cf..fdfb45c 100644 --- a/Source/UrlRequest_Unix.cpp +++ b/Source/UrlRequest_Unix.cpp @@ -149,6 +149,26 @@ namespace UrlLib // Request-specific failure detail (host/port/path specifics) lands here during // the transfer; see the error handling in PerformAsync. curl_check(curl_easy_setopt(m_curl, CURLOPT_ERRORBUFFER, m_curlErrorBuffer.data())); + + // Prefer the newest HTTP version this libcurl build supports, detected at + // runtime so the same binary upgrades transparently with the system curl. + // These options only affect https:// transfers (plain http:// and file:// + // stay HTTP/1.1), and both negotiate downward when the server lacks support. + const auto* versionInfo = curl_version_info(CURLVERSION_NOW); +#if defined(CURL_VERSION_HTTP3) + // HTTP/3 with fallback. Gated on runtime curl >= 8.0.0: earlier curls treat + // CURL_HTTP_VERSION_3 as h3-or-fail instead of h3-with-fallback. + if ((versionInfo->features & CURL_VERSION_HTTP3) && versionInfo->version_num >= 0x080000) + { + curl_check(curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3)); + } + else +#endif + if (versionInfo->features & CURL_VERSION_HTTP2) + { + // HTTP/2 over TLS via ALPN, HTTP/1.1 otherwise. + curl_check(curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS)); + } } } diff --git a/Source/UrlRequest_Windows_Shared.h b/Source/UrlRequest_Windows_Shared.h index 2d04b5a..c8c12a7 100644 --- a/Source/UrlRequest_Windows_Shared.h +++ b/Source/UrlRequest_Windows_Shared.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -95,7 +96,13 @@ namespace UrlLib ); } - Web::Http::HttpClient client; + // Explicitly allow HTTP/2 (negotiated via ALPN for https, supported since + // Windows 10 1607) rather than relying on the OS-build default for MaxVersion. + // HTTP/3 has no knob in Windows.Web.Http -- HttpVersion caps at Http20; it + // arrives with a WinHTTP-based backend (see the Readme's HTTP versions matrix). + Web::Http::Filters::HttpBaseProtocolFilter filter; + filter.MaxVersion(Web::Http::HttpVersion::Http20); + Web::Http::HttpClient client{filter}; return arcana::create_task(client.SendRequestAsync(requestMessage)) .then(arcana::inline_scheduler, m_cancellationSource, [this](Web::Http::HttpResponseMessage responseMessage) { From 08d3c90c70211e1eb0f576ba5611d34013efcfbc Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Fri, 10 Jul 2026 17:57:04 -0700 Subject: [PATCH 2/2] H2/H3: restrict opt-in to https and harden compile-time guards Addresses review on #35: - Apple: guard assumesHTTP3Capable with an SDK-version #if (__MAC_OS_X_VERSION_MAX_ALLOWED / __IPHONE_OS_VERSION_MAX_ALLOWED) so building against a pre-11.3/14.5 SDK compiles -- @available only gates the runtime deployment target, not SDK symbol availability. Restrict the opt-in to https:// (h3 is TLS-only). - Unix: restrict CURLOPT_HTTP_VERSION to https:// scheme. Guard the h3 and h2 blocks on LIBCURL_VERSION_NUM (a real curlver.h macro) instead of the CURL_HTTP_VERSION_* enum constants, which are invisible to the preprocessor -- '#if defined(CURL_HTTP_VERSION_3)' is always false. h3 gated at >= 7.66.0, 2TLS at >= 7.47.0, so older system curl headers that lack the enums still build. Verified: full suite green on macOS (8/8); Unix TU compiles -Wall -Wextra against Homebrew curl 8.21 and Xcode SDK curl 8.7, and the guard pattern compiles against fabricated 7.47 and 7.32 headers lacking the enums. --- Source/UrlRequest_Apple.mm | 25 ++++++++++++++----- Source/UrlRequest_Unix.cpp | 49 +++++++++++++++++++++++++------------- 2 files changed, 52 insertions(+), 22 deletions(-) diff --git a/Source/UrlRequest_Apple.mm b/Source/UrlRequest_Apple.mm index 6e12b77..ff74a84 100644 --- a/Source/UrlRequest_Apple.mm +++ b/Source/UrlRequest_Apple.mm @@ -132,13 +132,26 @@ void Open(UrlMethod method, const std::string& url) mutableRequest.HTTPBody = requestBodyData; } - if (@available(macOS 11.3, iOS 14.5, *)) + // HTTP/3 (QUIC) is TLS-only, so restrict the opt-in to https:// (app:// has been + // rewritten to file:// by now, and file:///http:// gain nothing). HTTP/2 has been + // automatic via ALPN since iOS 9 / macOS 10.11 and needs no opt-in. + // + // The #if guards compilation on the *SDK* version: assumesHTTP3Capable was added in + // the macOS 11.3 / iOS 14.5 SDKs, and @available only gates the runtime deployment + // target -- referencing the property against an older SDK is a hard compile error. + // Together: #if keeps it out of older-SDK builds, @available keeps it off older OSes. + if ([m_url.scheme caseInsensitiveCompare:@"https"] == NSOrderedSame) { - // Let NSURLSession attempt HTTP/3 (QUIC) for this request directly instead of - // waiting to learn h3 support from a prior response's Alt-Svc header; the stack - // races QUIC against TCP and falls back to h2/h1.1 when the server lacks h3. - // HTTP/2 has been automatic via ALPN since iOS 9 / macOS 10.11. - mutableRequest.assumesHTTP3Capable = YES; +#if (defined(__MAC_OS_X_VERSION_MAX_ALLOWED) && __MAC_OS_X_VERSION_MAX_ALLOWED >= 110300) || \ + (defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 140500) + if (@available(macOS 11.3, iOS 14.5, *)) + { + // Attempt h3 for this request directly rather than waiting to learn support + // from a prior response's Alt-Svc header; the stack races QUIC against TCP + // and falls back to h2/h1.1 when the server lacks h3. + mutableRequest.assumesHTTP3Capable = YES; + } +#endif } request = [mutableRequest copy]; diff --git a/Source/UrlRequest_Unix.cpp b/Source/UrlRequest_Unix.cpp index fdfb45c..b571818 100644 --- a/Source/UrlRequest_Unix.cpp +++ b/Source/UrlRequest_Unix.cpp @@ -150,24 +150,41 @@ namespace UrlLib // the transfer; see the error handling in PerformAsync. curl_check(curl_easy_setopt(m_curl, CURLOPT_ERRORBUFFER, m_curlErrorBuffer.data())); - // Prefer the newest HTTP version this libcurl build supports, detected at - // runtime so the same binary upgrades transparently with the system curl. - // These options only affect https:// transfers (plain http:// and file:// - // stay HTTP/1.1), and both negotiate downward when the server lacks support. - const auto* versionInfo = curl_version_info(CURLVERSION_NOW); -#if defined(CURL_VERSION_HTTP3) - // HTTP/3 with fallback. Gated on runtime curl >= 8.0.0: earlier curls treat - // CURL_HTTP_VERSION_3 as h3-or-fail instead of h3-with-fallback. - if ((versionInfo->features & CURL_VERSION_HTTP3) && versionInfo->version_num >= 0x080000) + // HTTP/2 and HTTP/3 are TLS-only, so only opt https:// transfers into a newer + // version; file://, app:// (rewritten to file above), and plain http:// keep + // libcurl's default. The version this build supports is detected at runtime so + // the same binary upgrades transparently with the system curl, and every value + // negotiates downward when the server or transport lacks support. + // + // The compile-time guards below key off LIBCURL_VERSION_NUM (a real macro from + // curlver.h) rather than the CURL_HTTP_VERSION_* constants: those are enum + // values, invisible to the preprocessor, so `#if defined(CURL_HTTP_VERSION_3)` + // would silently always be false. CURL_HTTP_VERSION_2TLS arrived in 7.47.0 and + // CURL_HTTP_VERSION_3 in 7.66.0. + if (scheme != nullptr && std::strcmp(scheme, "https") == 0) { - curl_check(curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_3)); - } - else + const auto* versionInfo = curl_version_info(CURLVERSION_NOW); + long httpVersion = CURL_HTTP_VERSION_NONE; +#if LIBCURL_VERSION_NUM >= 0x074200 /* 7.66.0 */ + // HTTP/3 with fallback. The runtime >= 8.0.0 gate matters because earlier + // curls treat CURL_HTTP_VERSION_3 as h3-or-fail instead of h3-with-fallback. + if ((versionInfo->features & CURL_VERSION_HTTP3) && versionInfo->version_num >= 0x080000) + { + httpVersion = CURL_HTTP_VERSION_3; + } + else #endif - if (versionInfo->features & CURL_VERSION_HTTP2) - { - // HTTP/2 over TLS via ALPN, HTTP/1.1 otherwise. - curl_check(curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS)); +#if LIBCURL_VERSION_NUM >= 0x072F00 /* 7.47.0 */ + if (versionInfo->features & CURL_VERSION_HTTP2) + { + // HTTP/2 over TLS via ALPN, HTTP/1.1 otherwise. + httpVersion = CURL_HTTP_VERSION_2TLS; + } +#endif + if (httpVersion != CURL_HTTP_VERSION_NONE) + { + curl_check(curl_easy_setopt(m_curl, CURLOPT_HTTP_VERSION, httpVersion)); + } } } }