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..ff74a84 100644 --- a/Source/UrlRequest_Apple.mm +++ b/Source/UrlRequest_Apple.mm @@ -132,6 +132,28 @@ void Open(UrlMethod method, const std::string& url) mutableRequest.HTTPBody = requestBodyData; } + // 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) + { +#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]; __block arcana::task_completion_source taskCompletionSource{}; diff --git a/Source/UrlRequest_Unix.cpp b/Source/UrlRequest_Unix.cpp index 8ed83cf..b571818 100644 --- a/Source/UrlRequest_Unix.cpp +++ b/Source/UrlRequest_Unix.cpp @@ -149,6 +149,43 @@ 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())); + + // 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) + { + 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 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)); + } + } } } 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) {