From dd85f1d1d786501fd67a8f5d23c7af251a3cee2f Mon Sep 17 00:00:00 2001
From: Ondřej Nosek <onosek@redhat.com>
Date: Apr 29 2026 03:13:39 +0000
Subject: Add configurable HTTP version for lookaside transfers


Introduce lookaside_http_version config option to control HTTP protocol
version used for source file downloads and uploads. Valid values:
- 'auto' (default): Let curl negotiate (may use HTTP/2)
- '1.1': Force HTTP/1.1 (better for high-latency connections)
- '2': Force HTTP/2 (better for low-latency, multiplexing)

Also include UPLOAD_BUFFERSIZE optimization (1MB buffer) with hasattr
check for compatibility with older libcurl versions (RHEL 7).

This allows users to optimize transfer performance based on their
network characteristics while maintaining backward compatibility.

Configuration example:
  [fedpkg]
  lookaside_http_version = 1.1

Inspired-by: https://pagure.io/rpkg/pull-request/772
Assisted-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Signed-off-by: Ondřej Nosek <onosek@redhat.com>

---

diff --git a/pyrpkg/__init__.py b/pyrpkg/__init__.py
index 2e49132..46ca599 100644
--- a/pyrpkg/__init__.py
+++ b/pyrpkg/__init__.py
@@ -122,7 +122,8 @@ class Commands(object):
                  dist=None, target=None, quiet=False,
                  distgit_namespaced=False, realms=None, lookaside_namespaced=False,
                  git_excludes=None, results_dir='root', allow_pre_generated_srpm=False,
-                 lookaside_attempts=None, lookaside_delay=None, koji_offline=False):
+                 lookaside_attempts=None, lookaside_delay=None, lookaside_http_version='auto',
+                 koji_offline=False):
         """Init the object and some configuration details."""
 
         # Path to operate on, most often pwd
@@ -258,6 +259,8 @@ class Commands(object):
         self.lookaside_attempts = lookaside_attempts
         # initial delay between network operation attempts. In seconds.
         self.lookaside_delay = lookaside_delay
+        # HTTP protocol version for lookaside transfers
+        self.lookaside_http_version = lookaside_http_version
         # prevent connecting to Koji
         self.koji_offline = koji_offline
 
@@ -281,7 +284,8 @@ class Commands(object):
         return CGILookasideCache(
             self.lookasidehash, self.lookaside, self.lookaside_cgi,
             client_cert=self.cert_file, ca_cert=self.ca_cert,
-            attempts=self.lookaside_attempts, delay=self.lookaside_delay)
+            attempts=self.lookaside_attempts, delay=self.lookaside_delay,
+            http_version=self.lookaside_http_version)
 
     @property
     def path(self):
diff --git a/pyrpkg/cli.py b/pyrpkg/cli.py
index 6e071d3..a6f7f0c 100644
--- a/pyrpkg/cli.py
+++ b/pyrpkg/cli.py
@@ -269,6 +269,7 @@ class cliClient(object):
                                        results_dir=results_dir,
                                        lookaside_attempts=self.lookaside_attempts,
                                        lookaside_delay=self.lookaside_delay,
+                                       lookaside_http_version=self.lookaside_http_version,
                                        koji_offline=koji_offline
                                        )
 
@@ -3210,6 +3211,21 @@ class cliClient(object):
                 val = None
         return val
 
+    @property
+    def lookaside_http_version(self):
+        """loads parameter 'lookaside_http_version' from the config file
+
+        Valid values: 'auto', '1.1', '2'
+        Default: 'auto' (let curl decide)
+        """
+        if self.config.has_option(self.name, 'lookaside_http_version'):
+            val = self.config.get(self.name, 'lookaside_http_version')
+            if val in ('auto', '1.1', '2'):
+                return val
+            self.log.error("Error: The config value 'lookaside_http_version' "
+                           "must be 'auto', '1.1', or '2'.")
+        return 'auto'
+
     # this method has to be specified in a derived class
     def _check_token(self, token, token_type):
         raise rpkgError('Undefined method for checking the token.')
diff --git a/pyrpkg/lookaside.py b/pyrpkg/lookaside.py
index 689b8b6..1e88352 100644
--- a/pyrpkg/lookaside.py
+++ b/pyrpkg/lookaside.py
@@ -32,7 +32,8 @@ from .errors import AlreadyUploadedError, DownloadError, InvalidHashType, Upload
 class CGILookasideCache(object):
     """A class to interact with a CGI-based lookaside cache"""
     def __init__(self, hashtype, download_url, upload_url,
-                 client_cert=None, ca_cert=None, attempts=None, delay=None):
+                 client_cert=None, ca_cert=None, attempts=None, delay=None,
+                 http_version='auto'):
         """Constructor
 
         :param str hashtype: The hash algorithm to use for uploads. (e.g 'md5')
@@ -50,6 +51,9 @@ class CGILookasideCache(object):
             says how many tries to do. None = single attempt / no-retrying
         :param int delay: Initial delay between network operation attempts.
             Each attempt doubles the previous delay value. In seconds.
+        :param str http_version: HTTP protocol version to use. Valid values:
+            'auto' (let curl decide), '1.1' (force HTTP/1.1), '2' (force HTTP/2).
+            Defaults to 'auto'.
         """
         self.hashtype = hashtype
         self.download_url = download_url
@@ -58,6 +62,7 @@ class CGILookasideCache(object):
         self.ca_cert = ca_cert
         self.attempts = attempts if attempts is not None and attempts > 1 else 1
         self.delay_between_attempts = delay if delay is not None and delay >= 0 else 15
+        self.http_version = http_version
 
         self.log = logging.getLogger(__name__)
 
@@ -191,6 +196,10 @@ class CGILookasideCache(object):
         c.setopt(pycurl.LOW_SPEED_LIMIT, 1000)
         c.setopt(pycurl.LOW_SPEED_TIME, 60)
         c.setopt(pycurl.FOLLOWLOCATION, 1)
+        if self.http_version == '1.1':
+            c.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_1_1)
+        elif self.http_version == '2' and hasattr(pycurl, 'CURL_HTTP_VERSION_2_0'):
+            c.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_2_0)
 
         # call retry method directly instead of @retry decorator - this approach allows passing
         # object's internal variables into the retry method
@@ -355,6 +364,12 @@ class CGILookasideCache(object):
             c.setopt(pycurl.NOPROGRESS, True)
         c.setopt(pycurl.HTTPPOST, post_data)
         c.setopt(pycurl.FOLLOWLOCATION, 1)
+        if hasattr(pycurl, 'UPLOAD_BUFFERSIZE'):
+            c.setopt(pycurl.UPLOAD_BUFFERSIZE, 1024 * 1024)
+        if self.http_version == '1.1':
+            c.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_1_1)
+        elif self.http_version == '2' and hasattr(pycurl, 'CURL_HTTP_VERSION_2_0'):
+            c.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_2_0)
 
         if self.client_cert is not None:
             if os.path.exists(self.client_cert):

