Skip to content

Commit

Permalink
Include HTTP response's reason/message if present (conda#11440)
Browse files Browse the repository at this point in the history
* Use HTTP response's reason/message if present

* Augment UnavailableInvalidChannel with response

* fixing a bug causing an empty message for response_details; moving imports to their correct spot

* Smarter stringify

* Remove intermediary variables

* Add news

Co-authored-by: Travis Hathaway <[email protected]>
  • Loading branch information
kenodegard and travishathaway authored Apr 29, 2022
1 parent f4b140c commit fa34108
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 48 deletions.
5 changes: 4 additions & 1 deletion conda/auxlib/logz.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ def jsondumps(obj):


def fullname(obj):
return obj.__module__ + "." + obj.__class__.__name__
try:
return obj.__module__ + "." + obj.__class__.__name__
except AttributeError:
return obj.__class__.__name__


request_header_sort_dict = {
Expand Down
6 changes: 5 additions & 1 deletion conda/core/subdir_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,11 @@ def fetch_repodata_remote_request(url, etag, mod_stamp, repodata_fn=REPODATA_FN)
status_code, url + '/' + repodata_fn)
return None
else:
raise UnavailableInvalidChannel(Channel(dirname(url)), status_code)
raise UnavailableInvalidChannel(
Channel(dirname(url)),
status_code,
response=e.response,
)

elif status_code == 401:
channel = Channel(url)
Expand Down
145 changes: 99 additions & 46 deletions conda/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from errno import ENOSPC
from functools import partial
import json
from json.decoder import JSONDecodeError
from logging import getLogger
import os
from os.path import join
Expand All @@ -15,13 +16,16 @@
from traceback import format_exception, format_exception_only
import getpass

from .models.channel import Channel
from .common.url import join_url, maybe_unquote
from . import CondaError, CondaExitZero, CondaMultiError, text_type
from .auxlib.entity import EntityEncoder
from .auxlib.ish import dals
from .auxlib.logz import stringify
from .auxlib.type_coercion import boolify
from ._vendor.toolz import groupby
from .base.constants import COMPATIBLE_SHELLS, PathConflict, SafetyChecks
from .common.compat import PY2, ensure_text_type, input, iteritems, iterkeys, on_win, string_types
from .common.compat import PY2, ensure_text_type, input, iteritems, iterkeys, on_win
from .common.io import dashlist, timeout
from .common.signals import get_signal_name

Expand Down Expand Up @@ -408,8 +412,6 @@ class ChannelError(CondaError):

class ChannelNotAllowed(ChannelError):
def __init__(self, channel):
from .models.channel import Channel
from .common.url import maybe_unquote
channel = Channel(channel)
channel_name = channel.name
channel_url = maybe_unquote(channel.base_url)
Expand All @@ -424,34 +426,66 @@ def __init__(self, channel):

class UnavailableInvalidChannel(ChannelError):

def __init__(self, channel, error_code):
from .models.channel import Channel
from .common.url import join_url, maybe_unquote
def __init__(self, channel, status_code, response=None):

# parse channel
channel = Channel(channel)
channel_name = channel.name
channel_url = maybe_unquote(channel.base_url)
message = dals("""
The channel is not accessible or is invalid.
channel name: %(channel_name)s
channel url: %(channel_url)s
error code: %(error_code)d

You will need to adjust your conda configuration to proceed.
Use `conda config --show channels` to view your configuration's current state,
and use `conda config --show-sources` to view config file locations.
""")
# define hardcoded/default reason/message
reason = getattr(response, "reason", None)
message = dals(
"""
The channel is not accessible or is invalid.
if channel.scheme == 'file':
message += dedent("""
As of conda 4.3, a valid channel must contain a `noarch/repodata.json` and
associated `noarch/repodata.json.bz2` file, even if `noarch/repodata.json` is
empty. Use `conda index %s`, or create `noarch/repodata.json`
and associated `noarch/repodata.json.bz2`.
""") % join_url(channel.location, channel.name)
You will need to adjust your conda configuration to proceed.
Use `conda config --show channels` to view your configuration's current state,
and use `conda config --show-sources` to view config file locations.
"""
)
if channel.scheme == "file":
url = join_url(channel.location, channel.name)
message += dedent(
f"""
As of conda 4.3, a valid channel must contain a `noarch/repodata.json` and
associated `noarch/repodata.json.bz2` file, even if `noarch/repodata.json` is
empty. Use `conda index {url}`, or create `noarch/repodata.json`
and associated `noarch/repodata.json.bz2`.
"""
)

super(UnavailableInvalidChannel, self).__init__(message, channel_url=channel_url,
channel_name=channel_name,
error_code=error_code)
# if response includes a valid json body we prefer the reason/message defined there
try:
body = response.json()
except (AttributeError, JSONDecodeError):
body = {}
else:
reason = body.get("reason", None) or reason
message = body.get("message", None) or message

# standardize arguments
status_code = status_code or "000"
reason = reason or "UNAVAILABLE OR INVALID"
if isinstance(reason, str):
reason = reason.upper()

super().__init__(
dals(
f"""
HTTP {status_code} {reason} for channel {channel_name} <{channel_url}>
"""
)
# since message may include newlines don't include in f-string/dals above
+ message,
channel_name=channel_name,
channel_url=channel_url,
status_code=status_code,
reason=reason,
response_details=stringify(response, content_max_len=1024) or "",
json=body,
)


class OperationNotAllowed(CondaError):
Expand Down Expand Up @@ -487,7 +521,6 @@ def __init__(self, url, target_full_path, checksum_type, expected_checksum, actu
expected %(checksum_type)s: %(expected_checksum)s
actual %(checksum_type)s: %(actual_checksum)s
""")
from .common.url import maybe_unquote
url = maybe_unquote(url)
super(ChecksumMismatchError, self).__init__(
message, url=url, target_full_path=target_full_path, checksum_type=checksum_type,
Expand All @@ -510,31 +543,51 @@ def __init__(self, prefix, package_name):
class CondaHTTPError(CondaError):
def __init__(self, message, url, status_code, reason, elapsed_time, response=None,
caused_by=None):
from .common.url import maybe_unquote
_message = dals("""
HTTP %(status_code)s %(reason)s for url <%(url)s>
Elapsed: %(elapsed_time)s
""")
cf_ray = getattr(response, 'headers', {}).get('CF-RAY')
_message += "CF-RAY: %s\n\n" % cf_ray if cf_ray else "\n"
message = _message + message
# if response includes a valid json body we prefer the reason/message defined there
try:
body = response.json()
except (AttributeError, JSONDecodeError):
body = {}
else:
reason = body.get("reason", None) or reason
message = body.get("message", None) or message

# standardize arguments
url = maybe_unquote(url)
status_code = status_code or '000'
reason = reason or 'CONNECTION FAILED'
if isinstance(reason, str):
reason = reason.upper()
elapsed_time = elapsed_time or '-'

from .auxlib.logz import stringify
response_details = (stringify(response, content_max_len=1024) or '') if response else ''

url = maybe_unquote(url)
if isinstance(elapsed_time, timedelta):
elapsed_time = text_type(elapsed_time).split(':', 1)[-1]
if isinstance(reason, string_types):
reason = reason.upper()
super(CondaHTTPError, self).__init__(message, url=url, status_code=status_code,
reason=reason, elapsed_time=elapsed_time,
response_details=response_details,
caused_by=caused_by)
elapsed_time = str(elapsed_time).split(":", 1)[-1]

# extract CF-RAY
try:
cf_ray = response.headers["CF-RAY"]
except (AttributeError, KeyError):
cf_ray = ""
else:
cf_ray = f"CF-RAY: {cf_ray}\n"

super().__init__(
dals(
f"""
HTTP {status_code} {reason} for url <{url}>
Elapsed: {elapsed_time}
{cf_ray}
"""
)
# since message may include newlines don't include in f-string/dals above
+ message,
url=url,
status_code=status_code,
reason=reason,
elapsed_time=elapsed_time,
response_details=stringify(response, content_max_len=1024) or "",
json=body,
caused_by=caused_by,
)


class AuthenticationError(CondaError):
Expand Down
19 changes: 19 additions & 0 deletions news/11440-improve-http-error-message
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
### Enhancements

* Attempt parsing HTTP errors as a JSON and extract error details. If present, prefer these details instead of those hard-coded. (#11440)

### Bug fixes

* <news item>

### Deprecations

* <news item>

### Docs

* <news item>

### Other

* <news item>

0 comments on commit fa34108

Please sign in to comment.