Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More information in error message when _get_endpoint requests fail #641

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

stvoutsin
Copy link
Contributor

@stvoutsin stvoutsin commented Jan 17, 2025

Summary

Currently when a user provides an invalid TAP URL or encounters connection issues during instantiation:
tap_service = pyvo.dal.TAPService(baseurl) the error message is:

DALServiceError: No working capabilities endpoint provided

We've found that this is slightly confusing for our users and lacks a bit of information on what went wrong.

Changes

This PR:

  • Provides more context to the error messages
  • Shows what URLs were attempted when trying to connect to the TAP service
  • Suggests possible fixes (e.g., verify URL is correct, check service is running)
  • Adds test coverage for various connection failure scenarios like:
    • Invalid/malformed URLs
    • HTTP error responses (403, 500, 502, 503)
    • Network-level connection failures

Other Changes

  • In the process I've also fixed a couple of issues with the docstrings where mostly there were missing or incorrect params.
  • Added some classes to __all__ in dal.query

Example

Example improved error message:

pyvo.dal.exceptions.DALServiceError: Cannot find TAP service at 'https://data-dev.lsst.cloud/api/setap'.

Unable to access the capabilities endpoint at:
- https://data-dev.lsst.cloud/api/setap/capabilities: 404 Not Found
- https://data-dev.lsst.cloud/api/capabilities: 404 Not Found

This could mean:
1. The service URL is incorrect
2. The service is temporarily unavailable
3. The service doesn't support this protocol

Related Issues

#125
#586

Interested to hear if these changes make sense or if you have any objections / alternative approaches you'd rather go with.

Copy link

codecov bot commented Jan 17, 2025

Codecov Report

Attention: Patch coverage is 84.61538% with 4 lines in your changes missing coverage. Please review.

Project coverage is 82.31%. Comparing base (f765128) to head (abee48e).
Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
pyvo/dal/tap.py 66.66% 2 Missing ⚠️
pyvo/dal/vosi.py 89.47% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #641      +/-   ##
==========================================
+ Coverage   82.30%   82.31%   +0.01%     
==========================================
  Files          72       72              
  Lines        7430     7446      +16     
==========================================
+ Hits         6115     6129      +14     
- Misses       1315     1317       +2     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@stvoutsin stvoutsin force-pushed the get_endpoint_error_message branch from d8b8983 to d5f5e41 Compare January 17, 2025 20:56
@bsipocz
Copy link
Member

bsipocz commented Jan 17, 2025

Thanks for the PR. I have the feeling that we may have one or a couple of issues open about this, if you're up for finding them please link them in the OP comment.

@bsipocz bsipocz added this to the v1.6.1 milestone Jan 17, 2025
@stvoutsin
Copy link
Contributor Author

Thanks for the PR. I have the feeling that we may have one or a couple of issues open about this, if you're up for finding them please link them in the OP comment.

The only related issues I found were:

Although the issue description in both is a bit different then the specific change this PR is attempting to address.

@stvoutsin stvoutsin force-pushed the get_endpoint_error_message branch from 883598e to bef7764 Compare January 24, 2025 17:01
Copy link
Member

@bsipocz bsipocz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These all look to be good improvements, thanks!

I hold off merging as I expect @andamian or @msdemlei may want to have a quick look, too.

pyvo/dal/vosi.py Outdated Show resolved Hide resolved
@stvoutsin stvoutsin marked this pull request as draft January 24, 2025 18:24
@stvoutsin stvoutsin force-pushed the get_endpoint_error_message branch from 0285fd5 to 0e0051c Compare January 24, 2025 18:28
@stvoutsin stvoutsin marked this pull request as ready for review January 24, 2025 18:31
@bsipocz
Copy link
Member

bsipocz commented Jan 24, 2025

Thanks! Test failures are unrelated upstream issues that are expected to go away once upstream deployment is done.

@msdemlei
Copy link
Contributor

msdemlei commented Jan 27, 2025 via email

@stvoutsin stvoutsin marked this pull request as draft January 27, 2025 20:49
@stvoutsin stvoutsin marked this pull request as ready for review January 27, 2025 22:14
@stvoutsin stvoutsin marked this pull request as draft January 27, 2025 22:19
@stvoutsin stvoutsin force-pushed the get_endpoint_error_message branch from 99807e5 to 468fdb6 Compare January 27, 2025 22:34
@stvoutsin stvoutsin force-pushed the get_endpoint_error_message branch from 8339b14 to abee48e Compare January 27, 2025 23:59
@stvoutsin
Copy link
Contributor Author

AttributeError would catch the case when someone passes a non-String. Personally, I probably would not bother catching and explaining these, but since these days the original traceback is preserved, that's not dramatic, and so that's fine for me, and I can see when the explanation given is actually helpful. I've tried to elicit a ValueError or a TypeError from urlparse which, I think, is the main source of exceptions here but failed. Hence, based on my philosophy that it's usually wise to let "unusual" exceptions propagate to the user I'd rather not catch them. The logic is somewhat like "If you don't know why you would see an exception here, don't try to explain it; a bad error message is better than a wrong one.", which perhaps is a corollary to then Zen of Python's "In the face of ambiguity, refuse the temptation to guess." Other than that, I think all of this is an improvement, so let's go. And let's, at the IVOA level, try to get rid of the horrible _get_endpoint_candidates in the first place. I'm still trying to get traction for http://ivoa.net/documents/caproles/...

Thanks for the comments!
I generally see your point and agree with the principle of "don't catch exceptions you don't understand".
The only case I was considering ValueError is an invalid IPv6 address:

tap_service = pyvo.dal.TAPService(baseurl="http://[invalid-url", session=auth)

If we just let this, as well as the AttributeError caused by an invalid type get propagated by removing that catch, we end up exposing the internal implementation details and produce deep tracebacks that a user will have to decode.

In the most recent commit, I've slightly modified by removing the TypeError catch, and more properly propagate the error that was triggered.
Just to give a better idea of the user's pov:

Approach 1 (Propagate Errors)

AttributeError:

tap_service = pyvo.dal.TAPService(baseurl=1, session=auth)

tap_service = pyvo.dal.TAPService(baseurl=1,
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/tap.py", line 129, in __init__
    self._session.update_from_capabilities(self.capabilities)
                                           ^^^^^^^^^^^^^^^^^
  File "lib/python3.12/site-packages/astropy/utils/decorators.py", line 850, in __get__
    val = self.fget(obj)
          ^^^^^^^^^^^^^^
  File "pyvo/dal/vosi.py", line 116, in capabilities
    return vosi.parse_capabilities(self._capabilities().read)
                                   ^^^^^^^^^^^^^^^^^^^^
  File "pyvo/utils/decorators.py", line 9, in wrapper
    raw = func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/vosi.py", line 112, in _capabilities
    return self._get_endpoint('capabilities')
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/vosi.py", line 37, in _get_endpoint
    candidates = self._get_endpoint_candidates(endpoint)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/vosi.py", line 22, in _get_endpoint_candidates
    urlcomp = urlparse(self.baseurl)
              ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/urllib/parse.py", line 394, in urlparse
    url, scheme, _coerce_result = _coerce_args(url, scheme)
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/urllib/parse.py", line 133, in _coerce_args
    return _decode_args(args) + (_encode_result,)
           ^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/urllib/parse.py", line 117, in _decode_args
    return tuple(x.decode(encoding, errors) if x else '' for x in args)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/urllib/parse.py", line 117, in <genexpr>
    return tuple(x.decode(encoding, errors) if x else '' for x in args)
                 ^^^^^^^^
AttributeError: 'int' object has no attribute 'decode'

Value Error

tap_service = pyvo.dal.TAPService(baseurl="http://[invalid-url", session=auth)

tap_service = pyvo.dal.TAPService(baseurl="http://[invalid-url",
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/tap.py", line 129, in __init__
    self._session.update_from_capabilities(self.capabilities)
                                           ^^^^^^^^^^^^^^^^^
  File "lib/python3.12/site-packages/astropy/utils/decorators.py", line 850, in __get__
    val = self.fget(obj)
          ^^^^^^^^^^^^^^
  File "pyvo/dal/vosi.py", line 116, in capabilities
    return vosi.parse_capabilities(self._capabilities().read)
                                   ^^^^^^^^^^^^^^^^^^^^
  File "pyvo/utils/decorators.py", line 9, in wrapper
    raw = func(*args, **kwargs)
          ^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/vosi.py", line 112, in _capabilities
    return self._get_endpoint('capabilities')
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/vosi.py", line 37, in _get_endpoint
    candidates = self._get_endpoint_candidates(endpoint)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/vosi.py", line 22, in _get_endpoint_candidates
    urlcomp = urlparse(self.baseurl)
              ^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/urllib/parse.py", line 395, in urlparse
    splitresult = urlsplit(url, scheme, allow_fragments)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/urllib/parse.py", line 497, in urlsplit
    raise ValueError("Invalid IPv6 URL")
ValueError: Invalid IPv6 URL

Approach 2 (Explicit handling of Errors):

  try:
      candidates = self._get_endpoint_candidates(endpoint)
  except (ValueError, AttributeError) as e:
      raise DALServiceError(
          f"Cannot construct endpoint URL from base '{self.baseurl}' and endpoint '{endpoint}'. "
          f"{type(e).__name__}: {str(e)}"
      ) from e

AttributeError:

tap_service = pyvo.dal.TAPService(baseurl=1, session=auth)

    tap_service = pyvo.dal.TAPService(baseurl=1,
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/tap.py", line 131, in __init__
    raise DALServiceError(f"Cannot find TAP service at '"
pyvo.dal.exceptions.DALServiceError: Cannot find TAP service at '1'.	

Cannot construct endpoint URL from base '1' and endpoint 'capabilities'. AttributeError: 'int' object has no attribute 'decode'

Value Error

tap_service = pyvo.dal.TAPService(baseurl="http://[invalid-url", session=auth)

    tap_service = pyvo.dal.TAPService(baseurl=tap_url,
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvo/dal/tap.py", line 131, in __init__
    raise DALServiceError(f"Cannot find TAP service at '"
pyvo.dal.exceptions.DALServiceError: Cannot find TAP service at 'http://[invalid-url'.

Cannot construct endpoint URL from base 'http://[invalid-url' and endpoint 'capabilities'. ValueError: Invalid IPv6 URL

Interested to hear your thoughts on this, I think either case is fine really because both are really obscure and rare cases that users should rarely encounter.
I was more interested in making sure the connection errors are clearly indicated to the user, for this I'm happy to go with whichever approach you prefer.

I'll read up on the caproles doc, but I agree the need for that _get_endpoint_candidates step is awkward so definitely something that should indeed be addressed at the IVOA level.

@stvoutsin stvoutsin marked this pull request as ready for review January 28, 2025 00:08
@msdemlei
Copy link
Contributor

msdemlei commented Jan 28, 2025 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants