oras-py icon indicating copy to clipboard operation
oras-py copied to clipboard

BasicAuth credentials not included in initial request, causing failures with large file uploads

Open ghost opened this issue 2 months ago • 0 comments

Environment:

  • oras-py version: 0.2.37
  • Python version: 3.11.12
  • Registry: Zot (LDAP-backed with BasicAuth)

Description:

When using BasicAuth to upload large artifacts (>2MB), uploads fail with SSLEOFError: EOF occurred in violation of protocol. Small files (<100 bytes) upload successfully to the same registry with the same credentials.

Root Cause:

In oras/provider.py, the do_request() method only adds auth headers for TokenAuth, not BasicAuth (lines 994-996):

  # Make the request and return to calling function, but attempt to use auth token if previously obtained
  if isinstance(self.auth, oras.auth.TokenAuth) and self.auth.token is not None:
      headers.update(self.auth.get_auth_header())

This forces all BasicAuth requests to go through the 401 retry mechanism:

  1. First request sent without auth headers → 401 Unauthorized
  2. Server responds with 401
  3. authenticate_request() adds auth headers and retries

For small files, this retry completes successfully. For large files (especially with methods like PUT that include the file data), the connection breaks during the initial failed upload attempt before the retry can complete, resulting in SSL EOF errors.

Evidence:

Registry logs show the no-auth → 401 → with-auth → success pattern for all BasicAuth requests:

  # Small file (succeeds after retry):
  PUT /v2/test/blobs/uploads/<uuid>?digest=sha256:... - 401 (no auth header)
  PUT /v2/test/blobs/uploads/<uuid>?digest=sha256:... - 201 (with auth header)

  # Large file (connection breaks before retry):
  PUT /v2/test/blobs/uploads/<uuid>?digest=sha256:... - SSL EOF error

Expected Behavior:

BasicAuth credentials should be included in the initial request, similar to how TokenAuth works, avoiding unnecessary 401 retries.

Proposed Fix:

Modify do_request() in oras/provider.py to include BasicAuth headers on the first attempt:

  # Make the request and return to calling function, but attempt to use auth token if previously obtained
  if isinstance(self.auth, oras.auth.TokenAuth) and self.auth.token is not None:
      headers.update(self.auth.get_auth_header())
  elif hasattr(self.auth, '_basic_auth') and self.auth._basic_auth:
      headers.update(self.auth.get_auth_header())

Workaround:

Override do_request() in a custom Registry subclass to add BasicAuth headers proactively:

  class ArtifactRegistry(oras.provider.Registry):
      def do_request(self, url, method="GET", data=None, headers=None, json=None, stream=False):
          if headers is None:
              headers = {}

          # Include BasicAuth header on first attempt
          if self.auth and hasattr(self.auth, '_basic_auth') and self.auth._basic_auth:
              try:
                  auth_header = self.auth.get_auth_header()
                  if auth_header:
                      headers.update(auth_header)
              except Exception:
                  pass

          return super().do_request(url, method, data, headers, json, stream)

Related Code:

  The chunked upload path has a TODO comment acknowledging this issue (line 648):
  # Important to update with auth token if acquired
  # TODO call to auth here

This suggests the issue is known but hasn't been addressed for BasicAuth.

ghost avatar Oct 02 '25 22:10 ghost