]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
http: negotiation and room for alt-svc/https rr to navigate
authorStefan Eissing <stefan@eissing.org>
Tue, 28 Jan 2025 13:11:59 +0000 (14:11 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Thu, 20 Feb 2025 14:45:46 +0000 (15:45 +0100)
Add a 'wanted' major HTTP version bitmask next to the 'allowed' bitmask
in HTTP version negotiation. This will try connections as specified in
'wanted', but enabled Alt-Svc and HTTPS-RR to redirect to other major
HTTP versions, if those are 'allowed'.

Changes libcurl internal default to `CURL_HTTP_VERSION_NONE` and removes
the code in curl that sets `CURL_HTTP_VERSION_2TLS` if the command line
does not say anything else.

Closes #16117

lib/cf-https-connect.c
lib/http.c
lib/http.h
lib/http2.c
lib/multi.c
lib/setopt.c
lib/url.c
lib/vtls/vtls.c
lib/ws.c
src/tool_operate.c
tests/http/test_12_reuse.py

index 812d8b1d9160cb74c12400c9dfe20d090736d040..7b14a01abe0f78a9f6038b60507fe33031455bcc 100644 (file)
@@ -192,7 +192,7 @@ static void cf_hc_reset(struct Curl_cfilter *cf, struct Curl_easy *data)
     ctx->state = CF_HC_INIT;
     ctx->result = CURLE_OK;
     ctx->hard_eyeballs_timeout_ms = data->set.happy_eyeballs_timeout;
-    ctx->soft_eyeballs_timeout_ms = data->set.happy_eyeballs_timeout / 2;
+    ctx->soft_eyeballs_timeout_ms = data->set.happy_eyeballs_timeout / 4;
   }
 }
 
@@ -263,8 +263,8 @@ static bool time_to_start_next(struct Curl_cfilter *cf,
       break;
   }
   if(i == idx) {
-    CURL_TRC_CF(data, cf, "all previous ballers have failed, time to start "
-                "baller %zu [%s]", idx, ctx->ballers[idx].name);
+    CURL_TRC_CF(data, cf, "all previous attempts failed, starting %s",
+                ctx->ballers[idx].name);
     return TRUE;
   }
   elapsed_ms = Curl_timediff(now, ctx->started);
@@ -315,7 +315,7 @@ static CURLcode cf_hc_connect(struct Curl_cfilter *cf,
     cf_hc_baller_init(&ctx->ballers[0], cf, data, cf->conn->transport);
     if(ctx->baller_count > 1) {
       Curl_expire(data, ctx->soft_eyeballs_timeout_ms, EXPIRE_ALPN_EYEBALLS);
-      CURL_TRC_CF(data, cf, "set expire for starting next baller in %ums",
+      CURL_TRC_CF(data, cf, "set next attempt to start in %ums",
                   ctx->soft_eyeballs_timeout_ms);
     }
     ctx->state = CF_HC_CONNECT;
@@ -351,7 +351,7 @@ static CURLcode cf_hc_connect(struct Curl_cfilter *cf,
 
     if(failed_ballers == ctx->baller_count) {
       /* all have failed. we give up */
-      CURL_TRC_CF(data, cf, "connect, all failed");
+      CURL_TRC_CF(data, cf, "connect, all attempts failed");
       for(i = 0; i < ctx->baller_count; i++) {
         if(ctx->ballers[i].result) {
           result = ctx->ballers[i].result;
@@ -450,7 +450,6 @@ static bool cf_hc_data_pending(struct Curl_cfilter *cf,
   if(cf->connected)
     return cf->next->cft->has_data_pending(cf->next, data);
 
-  CURL_TRC_CF((struct Curl_easy *)data, cf, "data_pending");
   for(i = 0; i < ctx->baller_count; i++)
     if(cf_hc_baller_data_pending(&ctx->ballers[i], data))
       return TRUE;
@@ -606,8 +605,6 @@ static CURLcode cf_hc_create(struct Curl_cfilter **pcf,
   ctx->baller_count = alpn_count;
 
   result = Curl_cf_create(&cf, &Curl_cft_http_connect, ctx);
-  CURL_TRC_CF(data, cf, "created with %zu ALPNs -> %d",
-              ctx->baller_count, result);
   if(result)
     goto out;
   ctx = NULL;
@@ -637,6 +634,17 @@ out:
   return result;
 }
 
+static bool cf_https_alpns_contain(enum alpnid id,
+                                   enum alpnid *list, size_t len)
+{
+  size_t i;
+  for(i = 0; i < len; ++i) {
+    if(id == list[i])
+      return TRUE;
+  }
+  return FALSE;
+}
+
 CURLcode Curl_cf_https_setup(struct Curl_easy *data,
                              struct connectdata *conn,
                              int sockindex,
@@ -645,41 +653,55 @@ CURLcode Curl_cf_https_setup(struct Curl_easy *data,
   enum alpnid alpn_ids[2];
   size_t alpn_count = 0;
   CURLcode result = CURLE_OK;
+  struct Curl_cfilter cf_fake, *cf = NULL;
 
   (void)sockindex;
   (void)remotehost;
+  /* we want to log for the filter before we create it, fake it. */
+  memset(&cf_fake, 0, sizeof(cf_fake));
+  cf_fake.cft = &Curl_cft_http_connect;
+  cf = &cf_fake;
 
   if(conn->bits.tls_enable_alpn) {
 #ifdef USE_HTTPSRR
+    /* Is there a HTTPSRR and if so, do its ALPNs it apply here?
+     * We are here after having selected a connection to a host+port and
+     * can no longer change that. Any HTTPSRR advice for other hosts and ports
+     * we need to ignore. */
     if(conn->dns_entry && conn->dns_entry->hinfo &&
-       !conn->dns_entry->hinfo->no_def_alpn) {
-      size_t i, j;
+       !conn->dns_entry->hinfo->no_def_alpn &&  /* ALPNs are defaults */
+       (!conn->dns_entry->hinfo->target ||      /* for same host */
+        !conn->dns_entry->hinfo->target[0] ||
+        (conn->dns_entry->hinfo->target[0] == '.' &&
+         !conn->dns_entry->hinfo->target[0])) &&
+       (conn->dns_entry->hinfo->port < 0 ||    /* for same port */
+        conn->dns_entry->hinfo->port == conn->remote_port)) {
+      size_t i;
       for(i = 0; i < CURL_ARRAYSIZE(conn->dns_entry->hinfo->alpns) &&
                  alpn_count < CURL_ARRAYSIZE(alpn_ids); ++i) {
-        bool present = FALSE;
         enum alpnid alpn = conn->dns_entry->hinfo->alpns[i];
-        for(j = 0; j < alpn_count; ++j) {
-          if(alpn == alpn_ids[j]) {
-            present = TRUE;
-            break;
-          }
-        }
-        if(present)
+        if(cf_https_alpns_contain(alpn, alpn_ids, alpn_count))
           continue;
         switch(alpn) {
         case ALPN_h3:
           if(Curl_conn_may_http3(data, conn))
             break;  /* not possible */
-          if(data->state.http_neg.allowed & CURL_HTTP_V3x)
+          if(data->state.http_neg.allowed & CURL_HTTP_V3x) {
+            CURL_TRC_CF(data, cf, "adding h3 via HTTPS-RR");
             alpn_ids[alpn_count++] = alpn;
+          }
           break;
         case ALPN_h2:
-          if(data->state.http_neg.allowed & CURL_HTTP_V2x)
+          if(data->state.http_neg.allowed & CURL_HTTP_V2x) {
+            CURL_TRC_CF(data, cf, "adding h2 via HTTPS-RR");
             alpn_ids[alpn_count++] = alpn;
+          }
           break;
         case ALPN_h1:
-          if(data->state.http_neg.allowed & CURL_HTTP_V1x)
+          if(data->state.http_neg.allowed & CURL_HTTP_V1x) {
+            CURL_TRC_CF(data, cf, "adding h1 via HTTPS-RR");
             alpn_ids[alpn_count++] = alpn;
+          }
           break;
         default: /* ignore */
           break;
@@ -688,18 +710,28 @@ CURLcode Curl_cf_https_setup(struct Curl_easy *data,
     }
 #endif
 
-    if(!alpn_count) {
-      if(data->state.http_neg.allowed & CURL_HTTP_V3x) {
-        result = Curl_conn_may_http3(data, conn);
-        if(!result)
-          alpn_ids[alpn_count++] = ALPN_h3;
-        else if(data->state.http_neg.allowed == CURL_HTTP_V3x)
-          goto out; /* only h3 allowed, not possible, error out */
+    if((alpn_count < CURL_ARRAYSIZE(alpn_ids)) &&
+       (data->state.http_neg.wanted & CURL_HTTP_V3x) &&
+       !cf_https_alpns_contain(ALPN_h3, alpn_ids, alpn_count)) {
+      result = Curl_conn_may_http3(data, conn);
+      if(!result) {
+        CURL_TRC_CF(data, cf, "adding wanted h3");
+        alpn_ids[alpn_count++] = ALPN_h3;
       }
-      if(data->state.http_neg.allowed & CURL_HTTP_V2x)
-        alpn_ids[alpn_count++] = ALPN_h2;
-      else if(data->state.http_neg.allowed & CURL_HTTP_V1x)
-        alpn_ids[alpn_count++] = ALPN_h1;
+      else if(data->state.http_neg.wanted == CURL_HTTP_V3x)
+        goto out; /* only h3 allowed, not possible, error out */
+    }
+    if((alpn_count < CURL_ARRAYSIZE(alpn_ids)) &&
+       (data->state.http_neg.wanted & CURL_HTTP_V2x) &&
+       !cf_https_alpns_contain(ALPN_h2, alpn_ids, alpn_count)) {
+      CURL_TRC_CF(data, cf, "adding wanted h2");
+      alpn_ids[alpn_count++] = ALPN_h2;
+    }
+    else if((alpn_count < CURL_ARRAYSIZE(alpn_ids)) &&
+            (data->state.http_neg.wanted & CURL_HTTP_V1x) &&
+            !cf_https_alpns_contain(ALPN_h1, alpn_ids, alpn_count)) {
+      CURL_TRC_CF(data, cf, "adding wanted h1");
+      alpn_ids[alpn_count++] = ALPN_h1;
     }
   }
 
index 52bb368d5afd60c063e1d8eef7cb55190273c069..21510a34a7049a4ab3f36d942f708aeb03b76843 100644 (file)
@@ -193,31 +193,33 @@ void Curl_http_neg_init(struct Curl_easy *data, struct http_negotiation *neg)
   neg->accept_09 = data->set.http09_allowed;
   switch(data->set.httpwant) {
   case CURL_HTTP_VERSION_1_0:
-    neg->allowed = (CURL_HTTP_V1x);
+    neg->wanted = neg->allowed = (CURL_HTTP_V1x);
     neg->only_10 = TRUE;
     break;
   case CURL_HTTP_VERSION_1_1:
-    neg->allowed = (CURL_HTTP_V1x);
+    neg->wanted = neg->allowed = (CURL_HTTP_V1x);
     break;
   case CURL_HTTP_VERSION_2_0:
-    neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x);
+    neg->wanted = neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x);
     neg->h2_upgrade = TRUE;
     break;
   case CURL_HTTP_VERSION_2TLS:
-    neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x);
+    neg->wanted = neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x);
     break;
   case CURL_HTTP_VERSION_2_PRIOR_KNOWLEDGE:
-    neg->allowed = (CURL_HTTP_V2x);
+    neg->wanted = neg->allowed = (CURL_HTTP_V2x);
     data->state.http_neg.h2_prior_knowledge = TRUE;
     break;
   case CURL_HTTP_VERSION_3:
-    neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x | CURL_HTTP_V3x);
+    neg->wanted = (CURL_HTTP_V1x | CURL_HTTP_V2x | CURL_HTTP_V3x);
+    neg->allowed = neg->wanted;
     break;
   case CURL_HTTP_VERSION_3ONLY:
-    neg->allowed = (CURL_HTTP_V3x);
+    neg->wanted = neg->allowed = (CURL_HTTP_V3x);
     break;
   case CURL_HTTP_VERSION_NONE:
   default:
+    neg->wanted = (CURL_HTTP_V1x | CURL_HTTP_V2x);
     neg->allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x | CURL_HTTP_V3x);
     break;
   }
@@ -229,7 +231,7 @@ CURLcode Curl_http_setup_conn(struct Curl_easy *data,
   /* allocate the HTTP-specific struct for the Curl_easy, only to survive
      during this request */
   connkeep(conn, "HTTP default");
-  if(data->state.http_neg.allowed == CURL_HTTP_V3x) {
+  if(data->state.http_neg.wanted == CURL_HTTP_V3x) {
     /* only HTTP/3, needs to work */
     CURLcode result = Curl_conn_may_http3(data, conn);
     if(result)
@@ -573,6 +575,7 @@ CURLcode Curl_http_auth_act(struct Curl_easy *data)
        (data->req.httpversion_sent > 11)) {
       infof(data, "Forcing HTTP/1.1 for NTLM");
       connclose(conn, "Force HTTP/1.1 connection");
+      data->state.http_neg.wanted = CURL_HTTP_V1x;
       data->state.http_neg.allowed = CURL_HTTP_V1x;
     }
   }
@@ -2849,7 +2852,7 @@ CURLcode Curl_http(struct Curl_easy *data, bool *done)
   }
 
   if(!Curl_conn_is_ssl(conn, FIRSTSOCKET) && (httpversion < 20) &&
-     (data->state.http_neg.allowed & CURL_HTTP_V2x) &&
+     (data->state.http_neg.wanted & CURL_HTTP_V2x) &&
      data->state.http_neg.h2_upgrade) {
     /* append HTTP2 upgrade magic stuff to the HTTP request if it is not done
        over SSL */
index 61237b3bf8a33089a063906ae3083a1745cd6f6d..c1bd45747059f432a3a10533ba2145d708093585 100644 (file)
@@ -76,6 +76,7 @@ struct dynhds;
 
 struct http_negotiation {
   unsigned char rcvd_min; /* minimum version seen in responses, 09, 10, 11 */
+  http_majors wanted;  /* wanted major versions when talking to server */
   http_majors allowed; /* allowed major versions when talking to server */
   BIT(h2_upgrade);  /* Do HTTP Upgrade from 1.1 to 2 */
   BIT(h2_prior_knowledge); /* Directly do HTTP/2 without ALPN/SSL */
index cd7512871e72fdd36b9835947a42957e17852385..d809031cff0977b0af20354e50a86f3f1a711925 100644 (file)
@@ -2795,7 +2795,7 @@ out:
 bool Curl_http2_may_switch(struct Curl_easy *data)
 {
   if(Curl_conn_http_version(data, data->conn) < 20 &&
-     (data->state.http_neg.allowed & CURL_HTTP_V2x) &&
+     (data->state.http_neg.wanted & CURL_HTTP_V2x) &&
      data->state.http_neg.h2_prior_knowledge) {
 #ifndef CURL_DISABLE_PROXY
     if(data->conn->bits.httpproxy && !data->conn->bits.tunnel_proxy) {
index 449420dc86592c80beac3fcd2e383a335553a070..c232f69544a0c848029b837054819b76ea8c3413 100644 (file)
@@ -1918,6 +1918,7 @@ static CURLMcode state_performing(struct Curl_easy *data,
     if(!ret) {
       infof(data, "Downgrades to HTTP/1.1");
       streamclose(data->conn, "Disconnect HTTP/2 for HTTP/1");
+      data->state.http_neg.wanted = CURL_HTTP_V1x;
       data->state.http_neg.allowed = CURL_HTTP_V1x;
       /* clear the error message bit too as we ignore the one we got */
       data->state.errorbuf = FALSE;
index 8a87d24446248fe07d30b8e705b47cfc0bf60928..a6be0c77063efc6ab68af5c22158e34891e5b8a9 100644 (file)
@@ -600,11 +600,6 @@ static CURLcode setopt_long(struct Curl_easy *data, CURLoption option,
      */
     switch(arg) {
     case CURL_HTTP_VERSION_NONE:
-#ifdef USE_HTTP2
-      /* This seems an undesirable quirk to force a behaviour on lower
-       * implementations that they should recognize independently? */
-      arg = CURL_HTTP_VERSION_2TLS;
-#endif
       /* accepted */
       break;
     case CURL_HTTP_VERSION_1_0:
index b348834c225a1390e14e128d817bed39d49fc3b3..4efebc9563459312228819c233111374c5c30dc3 100644 (file)
--- a/lib/url.c
+++ b/lib/url.c
@@ -473,11 +473,7 @@ CURLcode Curl_init_userdefined(struct Curl_easy *data)
   set->maxage_conn = 118;
   set->maxlifetime_conn = 0;
   set->http09_allowed = FALSE;
-#ifdef USE_HTTP2
-  set->httpwant = CURL_HTTP_VERSION_2TLS
-#else
-  set->httpwant = CURL_HTTP_VERSION_1_1
-#endif
+  set->httpwant = CURL_HTTP_VERSION_NONE
     ;
 #if defined(USE_HTTP2) || defined(USE_HTTP3)
   memset(&set->priority, 0, sizeof(set->priority));
@@ -1034,9 +1030,7 @@ static bool url_match_conn(struct connectdata *conn, void *userdata)
 
 #ifndef CURL_DISABLE_HTTP
   /* If looking for HTTP and the HTTP versions allowed do not include
-   * the HTTP version of conn, continue looking.
-   * CURL_HTTP_VERSION_2TLS is default which indicates no preference,
-   * so we take any existing connection. */
+   * the HTTP version of conn, continue looking. */
   if((needle->handler->protocol & PROTO_FAMILY_HTTP)) {
     switch(Curl_conn_http_version(data, conn)) {
     case 30:
@@ -3074,44 +3068,47 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
     enum alpnid srcalpnid = ALPN_none;
     bool hit = FALSE;
     struct altsvc *as = NULL;
-    int allowed_versions = ALPN_none;
-
-    if(data->state.http_neg.allowed & CURL_HTTP_V3x)
-      allowed_versions |= ALPN_h3;
-    if(data->state.http_neg.allowed & CURL_HTTP_V2x)
-      allowed_versions |= ALPN_h2;
-    if(data->state.http_neg.allowed & CURL_HTTP_V1x)
-      allowed_versions |= ALPN_h1;
-    allowed_versions &= (int)data->asi->flags;
+    int allowed_alpns = ALPN_none;
+    struct http_negotiation *neg = &data->state.http_neg;
+
+    DEBUGF(infof(data, "Alt-svc check wanted=%x, allowed=%x",
+                 neg->wanted, neg->allowed));
+    if(neg->allowed & CURL_HTTP_V3x)
+      allowed_alpns |= ALPN_h3;
+    if(neg->allowed & CURL_HTTP_V2x)
+      allowed_alpns |= ALPN_h2;
+    if(neg->allowed & CURL_HTTP_V1x)
+      allowed_alpns |= ALPN_h1;
+    allowed_alpns &= (int)data->asi->flags;
 
     host = conn->host.rawalloc;
     DEBUGF(infof(data, "check Alt-Svc for host %s", host));
 #ifdef USE_HTTP3
-    if(!hit && (allowed_versions & ALPN_h3)) {
+    if(!hit && (neg->wanted & CURL_HTTP_V3x)) {
       srcalpnid = ALPN_h3;
       hit = Curl_altsvc_lookup(data->asi,
                                ALPN_h3, host, conn->remote_port, /* from */
                                &as /* to */,
-                               allowed_versions);
+                               allowed_alpns);
     }
  #endif
  #ifdef USE_HTTP2
-    if(!hit && (allowed_versions & ALPN_h2) &&
-       !data->state.http_neg.h2_prior_knowledge) {
+    if(!hit && (neg->wanted & CURL_HTTP_V2x) &&
+       !neg->h2_prior_knowledge) {
       srcalpnid = ALPN_h2;
       hit = Curl_altsvc_lookup(data->asi,
                                ALPN_h2, host, conn->remote_port, /* from */
                                &as /* to */,
-                               allowed_versions);
+                               allowed_alpns);
     }
  #endif
-    if(!hit && (allowed_versions & ALPN_h1) &&
-       !data->state.http_neg.only_10) {
+    if(!hit && (neg->wanted & CURL_HTTP_V1x) &&
+       !neg->only_10) {
       srcalpnid = ALPN_h1;
       hit = Curl_altsvc_lookup(data->asi,
                                ALPN_h1, host, conn->remote_port, /* from */
                                &as /* to */,
-                               allowed_versions);
+                               allowed_alpns);
     }
 
     if(hit) {
@@ -3131,15 +3128,15 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
         /* protocol version switch */
         switch(as->dst.alpnid) {
         case ALPN_h1:
-          data->state.http_neg.allowed = CURL_HTTP_V1x;
-          data->state.http_neg.only_10 = FALSE;
+          neg->wanted = neg->allowed = CURL_HTTP_V1x;
+          neg->only_10 = FALSE;
           break;
         case ALPN_h2:
-          data->state.http_neg.allowed = CURL_HTTP_V2x;
+          neg->wanted = neg->allowed = CURL_HTTP_V2x;
           break;
         case ALPN_h3:
           conn->transport = TRNSPRT_QUIC;
-          data->state.http_neg.allowed = CURL_HTTP_V3x;
+          neg->wanted = neg->allowed = CURL_HTTP_V3x;
           break;
         default: /* should not be possible */
           break;
index a6587c072eded06e37458a5b918fe6782bbfeacc..ae4afbf74d92471f0a091ecd80b3de629724654f 100644 (file)
@@ -1541,7 +1541,7 @@ static CURLcode cf_ssl_create(struct Curl_cfilter **pcf,
 
   DEBUGASSERT(data->conn);
 
-  ctx = cf_ctx_new(data, alpn_get_spec(data->state.http_neg.allowed,
+  ctx = cf_ctx_new(data, alpn_get_spec(data->state.http_neg.wanted,
                                        conn->bits.tls_enable_alpn));
   if(!ctx) {
     result = CURLE_OUT_OF_MEMORY;
index 9510622683ac72d6ce83f70f24c6da586dc090e3..2ce0b484556e7100e97d9d813f3b85d0505966eb 100644 (file)
--- a/lib/ws.c
+++ b/lib/ws.c
@@ -1310,6 +1310,7 @@ static CURLcode ws_setup_conn(struct Curl_easy *data,
   /* WebSockets is 1.1 only (for now) */
   data->state.http_neg.accept_09 = FALSE;
   data->state.http_neg.only_10 = FALSE;
+  data->state.http_neg.wanted = CURL_HTTP_V1x;
   data->state.http_neg.allowed = CURL_HTTP_V1x;
   return Curl_http_setup_conn(data, conn);
 }
index 20a8d87b1a341345f7e974e7e25a7f5c36c1c0c0..54cc47cd8386a5dc23e9632dc28877bb80cccef4 100644 (file)
@@ -1087,8 +1087,6 @@ static CURLcode config2setopts(struct GlobalConfig *global,
 
     if(config->httpversion)
       my_setopt_enum(curl, CURLOPT_HTTP_VERSION, config->httpversion);
-    else if(feature_http2)
-      my_setopt_enum(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2TLS);
 
     /* curl 7.19.1 (the 301 version existed in 7.18.2),
        303 was added in 7.26.0 */
index 468fcfbb71f0d11d429754f0e5d5fb7e47875e3d..747e31eb58a0ee6f1c8c931d710fe5f835c15658 100644 (file)
@@ -76,30 +76,24 @@ class TestReuse:
         assert r.total_connects == count
 
     @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported")
-    def test_12_03_alt_svc_h2h3(self, env: Env, httpd, nghttpx):
+    def test_12_03_as_follow_h2h3(self, env: Env, httpd, nghttpx):
+        # Without '--http*` an Alt-Svc redirection from h2 to h3 is allowed
         httpd.clear_extra_configs()
         httpd.reload()
-        count = 2
-        # write a alt-svc file the advises h3 instead of h2
+        # write a alt-svc file that advises h3 instead of h2
         asfile = os.path.join(env.gen_dir, 'alt-svc-12_03.txt')
-        ts = datetime.now() + timedelta(hours=24)
-        expires = f'{ts.year:04}{ts.month:02}{ts.day:02} {ts.hour:02}:{ts.minute:02}:{ts.second:02}'
-        with open(asfile, 'w') as fd:
-            fd.write(f'h2 {env.domain1} {env.https_port} h3 {env.domain1} {env.https_port} "{expires}" 0 0')
-        log.info(f'altscv: {open(asfile).readlines()}')
+        self.create_asfile(asfile, f'h2 {env.domain1} {env.https_port} h3 {env.domain1} {env.h3_port}')
         curl = CurlClient(env=env)
-        urln = f'https://{env.authority_for(env.domain1, "h2")}/data.json?[0-{count-1}]'
+        urln = f'https://{env.authority_for(env.domain1, "h2")}/data.json'
         r = curl.http_download(urls=[urln], with_stats=True, extra_args=[
-            '--alt-svc', f'{asfile}', '--http3',
+            '--alt-svc', f'{asfile}',
         ])
-        r.check_response(count=count, http_status=200)
-        # We expect the connection to be reused
-        assert r.total_connects == 1
-        for s in r.stats:
-            assert s['http_version'] == '3', f'{s}'
+        r.check_response(count=1, http_status=200)
+        assert r.stats[0]['http_version'] == '3', f'{r.stats}'
 
     @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported")
-    def test_12_04_alt_svc_h3h2(self, env: Env, httpd, nghttpx):
+    def test_12_04_as_follow_h3h2(self, env: Env, httpd, nghttpx):
+        # With '--http3` an Alt-Svc redirection from h3 to h2 is allowed
         httpd.clear_extra_configs()
         httpd.reload()
         count = 2
@@ -122,7 +116,8 @@ class TestReuse:
             assert s['http_version'] == '2', f'{s}'
 
     @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported")
-    def test_12_05_alt_svc_h3h1(self, env: Env, httpd, nghttpx):
+    def test_12_05_as_follow_h3h1(self, env: Env, httpd, nghttpx):
+        # With '--http3` an Alt-Svc redirection from h3 to h1 is allowed
         httpd.clear_extra_configs()
         httpd.reload()
         count = 2
@@ -145,7 +140,8 @@ class TestReuse:
             assert s['http_version'] == '1.1', f'{s}'
 
     @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported")
-    def test_12_06_alt_svc_h3h1_h3only(self, env: Env, httpd, nghttpx):
+    def test_12_06_as_ignore_h3h1(self, env: Env, httpd, nghttpx):
+        # With '--http3-only` an Alt-Svc redirection from h3 to h1 is ignored
         httpd.clear_extra_configs()
         httpd.reload()
         count = 2
@@ -166,3 +162,26 @@ class TestReuse:
         assert r.total_connects == 1
         for s in r.stats:
             assert s['http_version'] == '3', f'{s}'
+
+    @pytest.mark.skipif(condition=not Env.have_h3(), reason="h3 not supported")
+    def test_12_07_as_ignore_h2h3(self, env: Env, httpd, nghttpx):
+        # With '--http2` an Alt-Svc redirection from h2 to h3 is ignored
+        httpd.clear_extra_configs()
+        httpd.reload()
+        # write a alt-svc file that advises h3 instead of h2
+        asfile = os.path.join(env.gen_dir, 'alt-svc-12_03.txt')
+        self.create_asfile(asfile, f'h2 {env.domain1} {env.https_port} h3 {env.domain1} {env.h3_port}')
+        curl = CurlClient(env=env)
+        urln = f'https://{env.authority_for(env.domain1, "h2")}/data.json'
+        r = curl.http_download(urls=[urln], with_stats=True, extra_args=[
+            '--alt-svc', f'{asfile}', '--http2'
+        ])
+        r.check_response(count=1, http_status=200)
+        assert r.stats[0]['http_version'] == '2', f'{r.stats}'
+
+    def create_asfile(self, fpath, line):
+        ts = datetime.now() + timedelta(hours=24)
+        expires = f'{ts.year:04}{ts.month:02}{ts.day:02} {ts.hour:02}:{ts.minute:02}:{ts.second:02}'
+        with open(fpath, 'w') as fd:
+            fd.write(f'{line} "{expires}" 0 0')
+        log.info(f'altscv: {open(fpath).readlines()}')