Repository: sunpy/sunpy

Issue:
RHESSI Fido client not falling back to the alternative servers
### Describe the bug

Currently the primary RHESSI sever (https://hesperia.gsfc.nasa.gov/hessidata/) is down but the RHESSI client is not falling back to any of the alternatives as it should.

Seems like adding this specific error `RemoteDisconnected` to the catch block works

```
from http.client import RemoteDisconnected

data_servers = ('https://hesperia.gsfc.nasa.gov/hessidata/',
                'http://hessi.ssl.berkeley.edu/hessidata/',
                'http://soleil.i4ds.ch/hessidata/')
def get_base_url():
    """
    Find the first mirror which is online
    """
    for server in data_servers:
        try:
            urlopen(server, timeout=1)
            return server
        except (URLError, RemoteDisconnected, socket.timeout,):
           pass
    raise OSError(f'Unable to find an online HESSI server from {data_servers}')
```

```
get_base_url()
'http://soleil.i4ds.ch/hessidata/'
```

### To Reproduce

```
from sunpy.net.dataretriever.sources import rhessi

query = Fido.search(a.Time('2015-10-09T13:00:00', '2015-10-09T15:00:00'),
                    a.Instrument.rhessi)

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/net/fido_factory.py:315, in UnifiedDownloaderFactory.search(self, *query)
    269 """
    270 Query for data in form of multiple parameters.
    271
   (...)
    312 parts individually.
    313 """
    314 query = attr.and_(*query)
--> 315 results = query_walker.create(query, self)
    317 # If we have searched the VSO but no results were returned, but another
    318 # client generated results, we drop the empty VSO results for tidiness.
    319 # This is because the VSO _can_handle_query is very broad because we
    320 # don't know the full list of supported values we can search for (yet).
    321 results = [r for r in results if not isinstance(r, vso.VSOQueryResponseTable) or len(r) > 0]

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/net/attr.py:613, in AttrWalker.create(self, *args, **kwargs)
    609 def create(self, *args, **kwargs):
    610     """
    611     Call the create function(s) matching the arguments to this method.
    612     """
--> 613     return self.createmm(self, *args, **kwargs)

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/util/functools.py:18, in seconddispatch.<locals>.wrapper(*args, **kwargs)
     17 def wrapper(*args, **kwargs):
---> 18     return dispatcher.dispatch(args[1].__class__)(*args, **kwargs)

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/net/fido_factory.py:244, in _create_and(walker, query, factory)
    242 @query_walker.add_creator(attr.AttrAnd)
    243 def _create_and(walker, query, factory):
--> 244     return factory._make_query_to_client(*query.attrs)

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/net/fido_factory.py:489, in UnifiedDownloaderFactory._make_query_to_client(self, *query)
    487     if isinstance(tmpclient, vso.VSOClient):
    488         kwargs = dict(response_format="table")
--> 489     results.append(tmpclient.search(*query, **kwargs))
    491 # This method is called by `search` and the results are fed into a
    492 # UnifiedResponse object.
    493 return results

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/net/dataretriever/sources/rhessi.py:223, in RHESSIClient.search(self, *args, **kwargs)
    221 timerange = TimeRange(matchdict['Start Time'], matchdict['End Time'])
    222 metalist = []
--> 223 for url in self.get_observing_summary_filename(timerange):
    224     exdict = parse(pattern, url).named
    225     exdict['url'] = url

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/net/dataretriever/sources/rhessi.py:168, in RHESSIClient.get_observing_summary_filename(self, time_range)
    164 # need to download and inspect the dbase file to determine the filename
    165 # for the observing summary data
    166 # the dbase files are monthly but contain the daily filenames
    167 for this_month in month_list:
--> 168     dbase_file_name, hdrs = self.get_observing_summary_dbase_file(this_month)
    169     dbase_dat = parse_observing_summary_dbase_file(dbase_file_name)
    170     this_month_obssumm_filenames = dbase_dat.get('filename')

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/net/dataretriever/sources/rhessi.py:216, in RHESSIClient.get_observing_summary_dbase_file(time)
    213 if _time < parse_time("2002/02/01"):
    214     raise ValueError("RHESSI summary files are not available before 2002-02-01")
--> 216 url = get_base_url() + f'dbase/{_time.strftime("hsi_obssumm_filedb_%Y%m.txt")}'
    217 return urlretrieve(url)

File ~/.virtualenvs/stixpy/lib/python3.11/site-packages/sunpy/net/dataretriever/sources/rhessi.py:92, in get_base_url()
     90 for server in data_servers:
     91     try:
---> 92         urlopen(server, timeout=1)
     93         return server
     94     except (URLError, socket.timeout):

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/urllib/request.py:216, in urlopen(url, data, timeout, cafile, capath, cadefault, context)
    214 else:
    215     opener = _opener
--> 216 return opener.open(url, data, timeout)

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/urllib/request.py:519, in OpenerDirector.open(self, fullurl, data, timeout)
    516     req = meth(req)
    518 sys.audit('urllib.Request', req.full_url, req.data, req.headers, req.get_method())
--> 519 response = self._open(req, data)
    521 # post-process response
    522 meth_name = protocol+"_response"

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/urllib/request.py:536, in OpenerDirector._open(self, req, data)
    533     return result
    535 protocol = req.type
--> 536 result = self._call_chain(self.handle_open, protocol, protocol +
    537                           '_open', req)
    538 if result:
    539     return result

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/urllib/request.py:496, in OpenerDirector._call_chain(self, chain, kind, meth_name, *args)
    494 for handler in handlers:
    495     func = getattr(handler, meth_name)
--> 496     result = func(*args)
    497     if result is not None:
    498         return result

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/urllib/request.py:1391, in HTTPSHandler.https_open(self, req)
   1390 def https_open(self, req):
-> 1391     return self.do_open(http.client.HTTPSConnection, req,
   1392         context=self._context, check_hostname=self._check_hostname)

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/urllib/request.py:1352, in AbstractHTTPHandler.do_open(self, http_class, req, **http_conn_args)
   1350     except OSError as err: # timeout error
   1351         raise URLError(err)
-> 1352     r = h.getresponse()
   1353 except:
   1354     h.close()

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/http/client.py:1375, in HTTPConnection.getresponse(self)
   1373 try:
   1374     try:
-> 1375         response.begin()
   1376     except ConnectionError:
   1377         self.close()

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/http/client.py:318, in HTTPResponse.begin(self)
    316 # read until we get a non-100 response
    317 while True:
--> 318     version, status, reason = self._read_status()
    319     if status != CONTINUE:
    320         break

File /opt/homebrew/Cellar/python@3.11/3.11.3/Frameworks/Python.framework/Versions/3.11/lib/python3.11/http/client.py:287, in HTTPResponse._read_status(self)
    283     print("reply:", repr(line))
    284 if not line:
    285     # Presumably, the server closed the connection before
    286     # sending a valid response.
--> 287     raise RemoteDisconnected("Remote end closed connection without"
    288                              " response")
    289 try:
    290     version, status, reason = line.split(None, 2)

RemoteDisconnected: Remote end closed connection without response
```

### Screenshots

_No response_

### System Details

_No response_

### Installation method

pip


Discussion:

Resolving patch:
diff --git a/sunpy/net/dataretriever/sources/rhessi.py b/sunpy/net/dataretriever/sources/rhessi.py
--- a/sunpy/net/dataretriever/sources/rhessi.py
+++ b/sunpy/net/dataretriever/sources/rhessi.py
@@ -4,6 +4,7 @@
 import csv
 import socket
 from datetime import datetime
+from http.client import RemoteDisconnected
 from urllib.error import URLError
 from urllib.request import urlopen, urlretrieve
 
@@ -91,7 +92,7 @@ def get_base_url():
         try:
             urlopen(server, timeout=1)
             return server
-        except (URLError, socket.timeout):
+        except (RemoteDisconnected, URLError, socket.timeout):
             pass
     raise OSError(f'Unable to find an online HESSI server from {data_servers}')
 


Test patch:
diff --git a/sunpy/net/dataretriever/sources/tests/test_rhessi.py b/sunpy/net/dataretriever/sources/tests/test_rhessi.py
--- a/sunpy/net/dataretriever/sources/tests/test_rhessi.py
+++ b/sunpy/net/dataretriever/sources/tests/test_rhessi.py
@@ -1,5 +1,6 @@
 import socket
 from unittest import mock
+from http.client import RemoteDisconnected
 from urllib.error import URLError
 
 import pytest
@@ -76,6 +77,15 @@ def test_get_base_url_on_timeout(mock_urlopen):
         rhessi.get_base_url()
 
 
+@mock.patch('sunpy.net.dataretriever.sources.rhessi.urlopen', side_effect=RemoteDisconnected(''))
+def test_get_base_url_on_remote_disconnected(mock_urlopen):
+    """
+    If all tested data servers timeout, then raise an `IOError`
+    """
+    with pytest.raises(OSError):
+        rhessi.get_base_url()
+
+
 def parsed_dbase():
     """
     The result of calling `parse_observing_summary_dbase_file(...)` on