-
-
Notifications
You must be signed in to change notification settings - Fork 7k
Description
I did this
Description
When Alt-Svc suggests HTTP/3 upgrade, curl's Happy Eyeballs v2 implementation incorrectly creates two QUIC connections instead of one QUIC connection with one TCP fallback connection.
Environment
curl version: 8.12.1
Platform: macOS (likely affects all platforms)
Build options: HTTP/3 enabled with OpenSSL, nghttp2 and nghttp3
Protocol: HTTPS with Alt-Svc enabled
Cpp Demo
/*
* test_happy_eyeballs.c
*
* Test program for curl Happy Eyeballs v2 with Alt-Svc
* Tests QUIC (HTTP/3) with TCP (HTTP/2) fallback
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <curl/curl.h>
/* Callback to capture response headers */
static size_t header_callback(char *buffer, size_t size, size_t nitems, void *userdata) {
size_t numbytes = size * nitems;
printf("< %.*s", (int)numbytes, buffer);
return numbytes;
}
/* Callback to capture response body */
static size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata) {
size_t numbytes = size * nmemb;
/* Just count bytes, don't print body */
(*(size_t *)userdata) += numbytes;
return numbytes;
}
/* Debug callback to see connection details */
static int debug_callback(CURL *handle, curl_infotype type,
char *data, size_t size, void *userptr) {
const char *text;
(void)handle;
(void)userptr;
switch(type) {
case CURLINFO_TEXT:
fprintf(stderr, "* %.*s", (int)size, data);
return 0;
case CURLINFO_HEADER_OUT:
text = "=> Send header";
break;
case CURLINFO_DATA_OUT:
text = "=> Send data";
break;
case CURLINFO_SSL_DATA_OUT:
text = "=> Send SSL data";
break;
case CURLINFO_HEADER_IN:
text = "<= Recv header";
break;
case CURLINFO_DATA_IN:
text = "<= Recv data";
break;
case CURLINFO_SSL_DATA_IN:
text = "<= Recv SSL data";
break;
default:
return 0;
}
/* Only print non-data info */
if(type != CURLINFO_DATA_IN && type != CURLINFO_DATA_OUT &&
type != CURLINFO_SSL_DATA_IN && type != CURLINFO_SSL_DATA_OUT) {
fprintf(stderr, "%s, %zu bytes\n", text, size);
}
return 0;
}
int main(int argc, char *argv[]) {
CURL *curl;
CURLcode res;
const char *url;
const char *altsvc_file = "alt-svc.txt";
size_t body_size = 0;
long http_version = 0;
char *effective_url = NULL;
/* Get URL from command line or use default */
if(argc > 1) {
url = argv[1];
} else {
url = "https://cloudflare.com";
}
/* Check if custom alt-svc file specified */
if(argc > 2) {
altsvc_file = argv[2];
}
printf("========================================\n");
printf(" Happy Eyeballs v2 Test Program\n");
printf("========================================\n\n");
printf("URL: %s\n", url);
printf("Alt-Svc file: %s\n", altsvc_file);
printf("\n");
/* Initialize libcurl */
curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if(!curl) {
fprintf(stderr, "Error: curl_easy_init() failed\n");
curl_global_cleanup();
return 1;
}
/* Set the URL */
curl_easy_setopt(curl, CURLOPT_URL, url);
/* Enable Alt-Svc with H3 support, read-only mode */
curl_easy_setopt(curl, CURLOPT_ALTSVC, altsvc_file);
curl_easy_setopt(curl, CURLOPT_ALTSVC_CTRL,
CURLALTSVC_H3 | CURLALTSVC_READONLYFILE);
/* Force fresh connection (no connection reuse) */
curl_easy_setopt(curl, CURLOPT_FRESH_CONNECT, 1L);
/* Enable verbose output to see Happy Eyeballs in action */
curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
/* Set debug callback for detailed connection info */
curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_callback);
/* Set header callback */
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, header_callback);
/* Set write callback */
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body_size);
/* Follow redirects */
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
/* Set reasonable timeout */
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);
/* Disable SSL verification for testing (insecure!) */
/* In production, use proper CA bundle: curl_easy_setopt(curl, CURLOPT_CAINFO, "path/to/ca-bundle.crt"); */
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
printf("========================================\n");
printf(" Performing Request\n");
printf("========================================\n\n");
/* Perform the request */
res = curl_easy_perform(curl);
printf("\n========================================\n");
printf(" Result\n");
printf("========================================\n\n");
/* Check for errors */
if(res != CURLE_OK) {
fprintf(stderr, "❌ Request failed: %s\n", curl_easy_strerror(res));
} else {
printf("✅ Request succeeded\n\n");
/* Get HTTP version used */
curl_easy_getinfo(curl, CURLINFO_HTTP_VERSION, &http_version);
/* Get effective URL */
curl_easy_getinfo(curl, CURLINFO_EFFECTIVE_URL, &effective_url);
printf("Results:\n");
printf(" - Effective URL: %s\n", effective_url ? effective_url : url);
printf(" - HTTP Version: ");
switch(http_version) {
case CURL_HTTP_VERSION_1_0:
printf("HTTP/1.0\n");
break;
case CURL_HTTP_VERSION_1_1:
printf("HTTP/1.1\n");
break;
case CURL_HTTP_VERSION_2_0:
printf("HTTP/2\n");
break;
case CURL_HTTP_VERSION_3:
printf("HTTP/3\n");
break;
default:
printf("Unknown (%ld)\n", http_version);
break;
}
printf(" - Body size: %zu bytes\n", body_size);
/* Get connection info */
long response_code = 0;
double total_time = 0;
double connect_time = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
curl_easy_getinfo(curl, CURLINFO_TOTAL_TIME, &total_time);
curl_easy_getinfo(curl, CURLINFO_CONNECT_TIME, &connect_time);
printf(" - Response code: %ld\n", response_code);
printf(" - Connect time: %.3f seconds\n", connect_time);
printf(" - Total time: %.3f seconds\n", total_time);
printf("\n");
/* Check if Happy Eyeballs was triggered */
if(http_version == CURL_HTTP_VERSION_3) {
printf("🎯 HTTP/3 (QUIC) connection successful!\n");
} else if(http_version == CURL_HTTP_VERSION_2_0) {
printf("🔄 Fallback to HTTP/2 (TCP) - QUIC may be blocked\n");
} else {
printf("📡 Using HTTP/1.x\n");
}
}
/* Cleanup */
curl_easy_cleanup(curl);
curl_global_cleanup();
printf("\n========================================\n");
printf(" Test Complete\n");
printf("========================================\n");
return (res == CURLE_OK) ? 0 : 1;
}
alt-svc.txt
# Your alt-svc cache. https://curl.se/docs/alt-svc.html
# This file was generated by libcurl! Edit at your own risk.
h1 dg01xmppapi.zoom.com.cn 443 h3 dg01xmppapi.zoom.com.cn 443 "20251129 07:17:35" 0 0
Actual Behavior
* !!! WARNING !!!
* This is a debug build of libcurl, do not use in production.
* STATE: INIT => SETUP handle 0x131808208; line 2393
* STATE: SETUP => CONNECT handle 0x131808208; line 2409
* check Alt-Svc for host dg01xmppapi.zoom.com.cn
* Alt-svc connecting from [h1]dg01xmppapi.zoom.com.cn:443 to [h3]dg01xmppapi.zoom.com.cn:443
* Added connection 0. The cache now contains 1 members
* STATE: CONNECT => RESOLVING handle 0x131808208; line 2308
* Host dg01xmppapi.zoom.com.cn:443 was resolved.
* IPv6: (none)
* IPv4: 221.122.64.37, 221.122.64.38
* STATE: RESOLVING => CONNECTING handle 0x131808208; line 2266
* socket type: 2
* Trying 221.122.64.37:443...
* socket type: 2
* Trying 221.122.64.37:443...
Problems:
Both Happy Eyeballs ballers create UDP sockets (type 2 = SOCK_DGRAM)
No TCP fallback socket is created
When QUIC/UDP port 443 is blocked by firewall (common in enterprise networks), both connection attempts fail
Root Cause Analysis
I've traced the issue to two locations in the code:
Problem 1: Global transport set in Alt-Svc handling
In lib/url.c (around line 3164), when Alt-Svc suggests h3:
case ALPN_h3:
infof(data, "alpn provide h3");
conn->transport = TRNSPRT_QUIC; // ← Sets global transport
data->state.httpwant = CURL_HTTP_VERSION_3;
break;
This sets conn->transport globally to TRNSPRT_QUIC.
Problem 2: Both ballers inherit the same transport
static CURLcode cf_hc_connect(struct Curl_cfilter *cf, ...) {
...
// Initialize first baller (h3 - HTTP/3)
cf_hc_baller_init(&ctx->ballers[0], cf, data, cf->conn->transport);
// Initialize second baller (h2 - HTTP/2 fallback)
if(time_to_start_next(cf, data, 1, now)) {
cf_hc_baller_init(&ctx->ballers[1], cf, data, cf->conn->transport);
// ^^^^^^^^^^^^^^^^^^
// Uses same transport!
}
}
I expected the following
- Alt-svc connecting from [h1]example.com:443 to [h3]example.com:443
- socket type: 2 ← SOCK_DGRAM (UDP/QUIC) - First baller for HTTP/3
- Trying 1.2.3.4:443...
- socket type: 1 ← SOCK_STREAM (TCP) - Second baller for HTTP/2 fallback ✅
- Trying 1.2.3.4:443...
curl/libcurl version
curl 8.12.1
operating system
Darwin Kernel Version 24.6.0