From fae1d475190a5e412f828f9f5d973916b368bb3c Mon Sep 17 00:00:00 2001 From: Vince Buffalo Date: Fri, 26 Sep 2025 18:05:01 -0700 Subject: [PATCH 1/2] fix: use duck typing for auth header application Changes isinstance check to hasattr for more flexible auth backend support. This allows authentication headers to be properly applied for all auth backends that implement get_auth_header(), not just TokenAuth instances. Fixes authentication issues with OAuth2 tokens and custom auth backends. --- oras/provider.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/oras/provider.py b/oras/provider.py index 1d91633..35bed6e 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -992,8 +992,10 @@ def do_request( headers = {} # 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()) + if self.auth and hasattr(self.auth, 'get_auth_header'): + auth_headers = self.auth.get_auth_header() + if auth_headers: # Only update if we got actual headers + headers.update(auth_headers) response = self.session.request( method, url, From 6e3f8158e874209349ec1f2bc6b2c212ffa93b8d Mon Sep 17 00:00:00 2001 From: Vince Buffalo Date: Fri, 26 Sep 2025 20:28:27 -0700 Subject: [PATCH 2/2] fix: improve error handling for non-JSON registry responses When registry returns non-JSON responses (e.g., HTML error pages), attempting to parse with r.json() causes JSONDecodeError. This commonly occurs with authentication failures or when hitting web portals instead of registry APIs. Changes: - Replace r.json() calls with descriptive HTTP status messages - Add fallback for capitalized Location header (some registries use "Location") - Provide actionable error messages for debugging authentication issues This prevents confusing "Expecting value: line 1 column 1 (char 0)" errors and helps users understand the actual problem (auth, permissions, connectivity). --- oras/provider.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/oras/provider.py b/oras/provider.py index 35bed6e..b6d01aa 100644 --- a/oras/provider.py +++ b/oras/provider.py @@ -541,7 +541,7 @@ def put_upload( # Location should be in the header session_url = self._get_location(r, container) if not session_url: - raise ValueError(f"Issue retrieving session url: {r.json()}") + raise ValueError(f"Issue retrieving session url (HTTP {r.status_code}): Check auth credentials and registry permissions") # PUT to upload blob url headers = { @@ -587,7 +587,8 @@ def _get_location( :param container: parsed container URI :type container: oras.container.Container or str """ - session_url = r.headers.get("location", "") + # Try both lowercase and capitalized Location header + session_url = r.headers.get("location") or r.headers.get("Location") or "" if not session_url: return session_url @@ -629,7 +630,7 @@ def chunked_upload( # Location should be in the header session_url = self._get_location(r, container) if not session_url: - raise ValueError(f"Issue retrieving session url: {r.json()}") + raise ValueError(f"Issue retrieving session url (HTTP {r.status_code}): Check auth credentials and registry permissions") # Read the blob in chunks, for each do a patch start = 0 @@ -654,7 +655,7 @@ def chunked_upload( ) session_url = self._get_location(r, container) if not session_url: - raise ValueError(f"Issue retrieving session url: {r.json()}") + raise ValueError(f"Issue retrieving session url (HTTP {r.status_code}): Check auth credentials and registry permissions") # Finally, issue a PUT request to close blob session_url = oras.utils.append_url_params(