Repository: sunpy/sunpy

Issue:
Outdated SSL certs on helio-vo.eu cause Fido.seach to crash
### Describe the bug

Executing a Fido.search crashes raising an error and returns no results. Ideally it would return results for other client and only no results for the Helio client.

### To Reproduce

```
from sunpy.net import Fido, attrs as aq
query = Fido.search(a.Time('2024-02-09T12:00', '2024-02-09T14:30'))

SSLError: HTTPSConnectionPool(host='www.helio-vo.eu', port=443): Max retries exceeded with url: /services/xml/instruments.xsd (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)')))
```


<details><summary>Full Error</summary>
<p>
```
---------------------------------------------------------------------------
SSLCertVerificationError                  Traceback (most recent call last)
File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/connectionpool.py:467, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    466 try:
--> 467     self._validate_conn(conn)
    468 except (SocketTimeout, BaseSSLError) as e:

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/connectionpool.py:1092, in HTTPSConnectionPool._validate_conn(self, conn)
   1091 if conn.is_closed:
-> 1092     conn.connect()
   1094 if not conn.is_verified:

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/connection.py:635, in HTTPSConnection.connect(self)
    627     warnings.warn(
    628         (
    629             f"System time is way off (before {RECENT_DATE}). This will probably "
   (...)
    632         SystemTimeWarning,
    633     )
--> 635 sock_and_verified = _ssl_wrap_socket_and_match_hostname(
    636     sock=sock,
    637     cert_reqs=self.cert_reqs,
    638     ssl_version=self.ssl_version,
    639     ssl_minimum_version=self.ssl_minimum_version,
    640     ssl_maximum_version=self.ssl_maximum_version,
    641     ca_certs=self.ca_certs,
    642     ca_cert_dir=self.ca_cert_dir,
    643     ca_cert_data=self.ca_cert_data,
    644     cert_file=self.cert_file,
    645     key_file=self.key_file,
    646     key_password=self.key_password,
    647     server_hostname=server_hostname,
    648     ssl_context=self.ssl_context,
    649     tls_in_tls=tls_in_tls,
    650     assert_hostname=self.assert_hostname,
    651     assert_fingerprint=self.assert_fingerprint,
    652 )
    653 self.sock = sock_and_verified.socket

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/connection.py:774, in _ssl_wrap_socket_and_match_hostname(sock, cert_reqs, ssl_version, ssl_minimum_version, ssl_maximum_version, cert_file, key_file, key_password, ca_certs, ca_cert_dir, ca_cert_data, assert_hostname, assert_fingerprint, server_hostname, ssl_context, tls_in_tls)
    772         server_hostname = normalized
--> 774 ssl_sock = ssl_wrap_socket(
    775     sock=sock,
    776     keyfile=key_file,
    777     certfile=cert_file,
    778     key_password=key_password,
    779     ca_certs=ca_certs,
    780     ca_cert_dir=ca_cert_dir,
    781     ca_cert_data=ca_cert_data,
    782     server_hostname=server_hostname,
    783     ssl_context=context,
    784     tls_in_tls=tls_in_tls,
    785 )
    787 try:

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/util/ssl_.py:459, in ssl_wrap_socket(sock, keyfile, certfile, cert_reqs, ca_certs, server_hostname, ssl_version, ciphers, ssl_context, ca_cert_dir, key_password, ca_cert_data, tls_in_tls)
    457     pass
--> 459 ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls, server_hostname)
    460 return ssl_sock

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/util/ssl_.py:503, in _ssl_wrap_socket_impl(sock, ssl_context, tls_in_tls, server_hostname)
    501     return SSLTransport(sock, ssl_context, server_hostname)
--> 503 return ssl_context.wrap_socket(sock, server_hostname=server_hostname)

File /opt/homebrew/Cellar/python@3.11/3.11.7_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/ssl.py:517, in SSLContext.wrap_socket(self, sock, server_side, do_handshake_on_connect, suppress_ragged_eofs, server_hostname, session)
    511 def wrap_socket(self, sock, server_side=False,
    512                 do_handshake_on_connect=True,
    513                 suppress_ragged_eofs=True,
    514                 server_hostname=None, session=None):
    515     # SSLSocket class handles server_hostname encoding before it calls
    516     # ctx._wrap_socket()
--> 517     return self.sslsocket_class._create(
    518         sock=sock,
    519         server_side=server_side,
    520         do_handshake_on_connect=do_handshake_on_connect,
    521         suppress_ragged_eofs=suppress_ragged_eofs,
    522         server_hostname=server_hostname,
    523         context=self,
    524         session=session
    525     )

File /opt/homebrew/Cellar/python@3.11/3.11.7_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/ssl.py:1108, in SSLSocket._create(cls, sock, server_side, do_handshake_on_connect, suppress_ragged_eofs, server_hostname, context, session)
   1107             raise ValueError("do_handshake_on_connect should not be specified for non-blocking sockets")
-> 1108         self.do_handshake()
   1109 except (OSError, ValueError):

File /opt/homebrew/Cellar/python@3.11/3.11.7_1/Frameworks/Python.framework/Versions/3.11/lib/python3.11/ssl.py:1383, in SSLSocket.do_handshake(self, block)
   1382         self.settimeout(None)
-> 1383     self._sslobj.do_handshake()
   1384 finally:

SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)

During handling of the above exception, another exception occurred:

SSLError                                  Traceback (most recent call last)
File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/connectionpool.py:790, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    789 # Make the request on the HTTPConnection object
--> 790 response = self._make_request(
    791     conn,
    792     method,
    793     url,
    794     timeout=timeout_obj,
    795     body=body,
    796     headers=headers,
    797     chunked=chunked,
    798     retries=retries,
    799     response_conn=response_conn,
    800     preload_content=preload_content,
    801     decode_content=decode_content,
    802     **response_kw,
    803 )
    805 # Everything went great!

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/connectionpool.py:491, in HTTPConnectionPool._make_request(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)
    490         new_e = _wrap_proxy_error(new_e, conn.proxy.scheme)
--> 491     raise new_e
    493 # conn.request() calls http.client.*.request, not the method in
    494 # urllib3.request. It also calls makefile (recv) on the socket.

SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)

The above exception was the direct cause of the following exception:

MaxRetryError                             Traceback (most recent call last)
File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/requests/adapters.py:486, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    485 try:
--> 486     resp = conn.urlopen(
    487         method=request.method,
    488         url=url,
    489         body=request.body,
    490         headers=request.headers,
    491         redirect=False,
    492         assert_same_host=False,
    493         preload_content=False,
    494         decode_content=False,
    495         retries=self.max_retries,
    496         timeout=timeout,
    497         chunked=chunked,
    498     )
    500 except (ProtocolError, OSError) as err:

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/connectionpool.py:844, in HTTPConnectionPool.urlopen(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)
    842     new_e = ProtocolError("Connection aborted.", new_e)
--> 844 retries = retries.increment(
    845     method, url, error=new_e, _pool=self, _stacktrace=sys.exc_info()[2]
    846 )
    847 retries.sleep()

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/urllib3/util/retry.py:515, in Retry.increment(self, method, url, response, error, _pool, _stacktrace)
    514     reason = error or ResponseError(cause)
--> 515     raise MaxRetryError(_pool, url, reason) from reason  # type: ignore[arg-type]
    517 log.debug("Incremented Retry for (url='%s'): %r", url, new_retry)

MaxRetryError: HTTPSConnectionPool(host='www.helio-vo.eu', port=443): Max retries exceeded with url: /services/xml/instruments.xsd (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)')))

During handling of the above exception, another exception occurred:

SSLError                                  Traceback (most recent call last)
Cell In[2], line 2
      1 from sunpy.net import Fido, attrs as a
----> 2 query = Fido.search(a.Time('2024-02-09T12:00', '2024-02-09T14:30'))

File ~/Projects/sunpy-dev/sunpy/net/fido_factory.py:314, in UnifiedDownloaderFactory.search(self, *query)
    268 """
    269 Query for data in form of multiple parameters.
    270
   (...)
    311 parts individually.
    312 """
    313 query = attr.and_(*query)
--> 314 results = query_walker.create(query, self)
    316 # If we have searched the VSO but no results were returned, but another
    317 # client generated results, we drop the empty VSO results for tidiness.
    318 # This is because the VSO _can_handle_query is very broad because we
    319 # don't know the full list of supported values we can search for (yet).
    320 results = [r for r in results if not isinstance(r, vso.VSOQueryResponseTable) or len(r) > 0]

File ~/Projects/sunpy-dev/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 ~/Projects/sunpy-dev/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 ~/Projects/sunpy-dev/sunpy/net/fido_factory.py:238, in _create_data(walker, query, factory)
    236 @query_walker.add_creator(attr.DataAttr)
    237 def _create_data(walker, query, factory):
--> 238     return factory._make_query_to_client(query)

File ~/Projects/sunpy-dev/sunpy/net/fido_factory.py:480, in UnifiedDownloaderFactory._make_query_to_client(self, *query)
    478 results = []
    479 for client in candidate_widget_types:
--> 480     tmpclient = client()
    481     kwargs = dict()
    482     # Handle the change in response format in the VSO

File ~/Projects/sunpy-dev/sunpy/net/helio/hec.py:90, in HECClient.__init__(self, link)
     88 session.verify = not (bool(os.environ.get("NO_VERIFY_HELIO_SSL", 0)))
     89 transport = Transport(session=session)
---> 90 self.hec_client = Client(link, transport=transport)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/client.py:76, in Client.__init__(self, wsdl, wsse, transport, service_name, port_name, plugins, settings)
     74     self.wsdl = wsdl
     75 else:
---> 76     self.wsdl = Document(wsdl, self.transport, settings=self.settings)
     77 self.wsse = wsse
     78 self.plugins = plugins if plugins is not None else []

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/wsdl/wsdl.py:92, in Document.__init__(self, location, transport, base, settings)
     83 self._definitions = (
     84     {}
     85 )  # type: typing.Dict[typing.Tuple[str, str], "Definition"]
     86 self.types = Schema(
     87     node=None,
     88     transport=self.transport,
     89     location=self.location,
     90     settings=self.settings,
     91 )
---> 92 self.load(location)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/wsdl/wsdl.py:97, in Document.load(self, location)
     94 def load(self, location):
     95     document = self._get_xml_document(location)
---> 97     root_definitions = Definition(self, document, self.location)
     98     root_definitions.resolve_imports()
    100     # Make the wsdl definitions public

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/wsdl/wsdl.py:193, in Definition.__init__(self, wsdl, doc, location)
    191 self.wsdl._add_definition(self)
    192 self.nsmap = doc.nsmap
--> 193 self._load(doc)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/wsdl/wsdl.py:198, in Definition._load(self, doc)
    195 def _load(self, doc):
    196     self.parse_imports(doc)
--> 198     self.parse_types(doc)
    199     self.messages = self.parse_messages(doc)
    200     self.port_types = self.parse_ports(doc)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/wsdl/wsdl.py:330, in Definition.parse_types(self, doc)
    328 # Find xsd:schema elements (wsdl:types/xsd:schema)
    329 schema_nodes = findall_multiple_ns(doc, "wsdl:types/xsd:schema", namespace_sets)
--> 330 self.types.add_documents(schema_nodes, self.location)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/xsd/schema.py:111, in Schema.add_documents(self, schema_nodes, location)
    109 resolve_queue = []
    110 for node in schema_nodes:
--> 111     document = self.create_new_document(node, location)
    112     resolve_queue.append(document)
    114 for document in resolve_queue:

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/xsd/schema.py:195, in Schema.create_new_document(self, node, url, base_url, target_namespace)
    193 schema = SchemaDocument(namespace, url, base_url)
    194 self.documents.add(schema)
--> 195 schema.load(self, node)
    196 return schema

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/xsd/schema.py:421, in SchemaDocument.load(self, schema, node)
    417 # Disable XML schema validation for now
    418 # if len(node) > 0:
    419 #     self.xml_schema = etree.XMLSchema(node)
    420 visitor = SchemaVisitor(schema, self)
--> 421 visitor.visit_schema(node)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/xsd/visitor.py:165, in SchemaVisitor.visit_schema(self, node)
    162 self.document._attribute_form = node.get("attributeFormDefault", "unqualified")
    164 for child in node:
--> 165     self.process(child, parent=node)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/xsd/visitor.py:93, in SchemaVisitor.process(self, node, parent)
     91 if not visit_func:
     92     raise ValueError("No visitor defined for %r" % node.tag)
---> 93 result = visit_func(self, node, parent)
     94 return result

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/xsd/visitor.py:238, in SchemaVisitor.visit_import(self, node, parent)
    235     return
    237 # Load the XML
--> 238 schema_node = self._retrieve_data(location, base_url=self.document._location)
    240 # Check if the xsd:import namespace matches the targetNamespace. If
    241 # the xsd:import statement didn't specify a namespace then make sure
    242 # that the targetNamespace wasn't declared by another schema yet.
    243 schema_tns = schema_node.get("targetNamespace")

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/xsd/visitor.py:1198, in SchemaVisitor._retrieve_data(self, url, base_url)
   1197 def _retrieve_data(self, url: typing.IO, base_url=None):
-> 1198     return load_external(
   1199         url, self.schema._transport, base_url, settings=self.schema.settings
   1200     )

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/loader.py:89, in load_external(url, transport, base_url, settings)
     87     if base_url:
     88         url = absolute_location(url, base_url)
---> 89     content = transport.load(url)
     90 return parse_xml(content, transport, base_url, settings=settings)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/transports.py:123, in Transport.load(self, url)
    120     if response:
    121         return bytes(response)
--> 123 content = self._load_remote_data(url)
    125 if self.cache:
    126     self.cache.add(url, content)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/zeep/transports.py:135, in Transport._load_remote_data(self, url)
    133 def _load_remote_data(self, url):
    134     self.logger.debug("Loading remote data from: %s", url)
--> 135     response = self.session.get(url, timeout=self.load_timeout)
    136     response.raise_for_status()
    137     return response.content

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/requests/sessions.py:602, in Session.get(self, url, **kwargs)
    594 r"""Sends a GET request. Returns :class:`Response` object.
    595
    596 :param url: URL for the new :class:`Request` object.
    597 :param \*\*kwargs: Optional arguments that ``request`` takes.
    598 :rtype: requests.Response
    599 """
    601 kwargs.setdefault("allow_redirects", True)
--> 602 return self.request("GET", url, **kwargs)

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/requests/sessions.py:589, in Session.request(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)
    584 send_kwargs = {
    585     "timeout": timeout,
    586     "allow_redirects": allow_redirects,
    587 }
    588 send_kwargs.update(settings)
--> 589 resp = self.send(prep, **send_kwargs)
    591 return resp

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/requests/sessions.py:725, in Session.send(self, request, **kwargs)
    722 if allow_redirects:
    723     # Redirect resolving generator.
    724     gen = self.resolve_redirects(r, request, **kwargs)
--> 725     history = [resp for resp in gen]
    726 else:
    727     history = []

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/requests/sessions.py:725, in <listcomp>(.0)
    722 if allow_redirects:
    723     # Redirect resolving generator.
    724     gen = self.resolve_redirects(r, request, **kwargs)
--> 725     history = [resp for resp in gen]
    726 else:
    727     history = []

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/requests/sessions.py:266, in SessionRedirectMixin.resolve_redirects(self, resp, req, stream, timeout, verify, cert, proxies, yield_requests, **adapter_kwargs)
    263     yield req
    264 else:
--> 266     resp = self.send(
    267         req,
    268         stream=stream,
    269         timeout=timeout,
    270         verify=verify,
    271         cert=cert,
    272         proxies=proxies,
    273         allow_redirects=False,
    274         **adapter_kwargs,
    275     )
    277     extract_cookies_to_jar(self.cookies, prepared_request, resp.raw)
    279     # extract redirect url, if any, for the next loop

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/requests/sessions.py:703, in Session.send(self, request, **kwargs)
    700 start = preferred_clock()
    702 # Send the request
--> 703 r = adapter.send(request, **kwargs)
    705 # Total elapsed time of the request (approximately)
    706 elapsed = preferred_clock() - start

File ~/.virtualenvs/sunpy-dev/lib/python3.11/site-packages/requests/adapters.py:517, in HTTPAdapter.send(self, request, stream, timeout, verify, cert, proxies)
    513         raise ProxyError(e, request=request)
    515     if isinstance(e.reason, _SSLError):
    516         # This branch is for urllib3 v1.22 and later.
--> 517         raise SSLError(e, request=request)
    519     raise ConnectionError(e, request=request)
    521 except ClosedPoolError as e:

SSLError: HTTPSConnectionPool(host='www.helio-vo.eu', port=443): Max retries exceeded with url: /services/xml/instruments.xsd (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)')))
```
</p>
</details> 

### Screenshots

_No response_

### System Details

```
==============================
sunpy Installation Information
==============================

General
#######
OS: Mac OS 14.3.1
Arch: 64bit, (arm)
sunpy: 6.0.dev201+g7618f05da
Installation path: sunpy.egg-info

Required Dependencies
#####################
astropy: 5.3
numpy: 1.24.3
packaging: 23.1
parfive: 2.0.2
pyerfa: 2.0.0.3
requests: 2.31.0

Optional Dependencies
#####################
sunpy: 6.0.dev201+g7618f05da
```

### Installation method

_No response_

Discussion:
samaloney: This is what happens with this PR, not sure why the log message is showing up twice?

```
from sunpy.net import Fido, attrs as a
query = Fido.search(a.Time('2024-02-09T12:00', '2024-02-09T14:30'))

WARNING: SSL verification error for HEC client.
You can set the 'NO_VERIFY_HELIO_SSL' environment variable disable SSL verification for Helio. [sunpy.net.helio.hec]
2024-02-12 11:20:14 - sunpy - WARNING: SSL verification error for HEC client.
You can set the 'NO_VERIFY_HELIO_SSL' environment variable disable SSL verification for Helio.
```

Patch:
diff --git a/changelog/7446.bugfix.rst b/changelog/7446.bugfix.rst
new file mode 100644
index 000000000..d32226681
--- /dev/null
+++ b/changelog/7446.bugfix.rst
@@ -0,0 +1,1 @@
+Fix a bug which caused ``Fido.search`` to crash due to SSL certificate verification error for the `~sunpy.net.helio.HECClient` now returns no results and logs a warning in this case.
diff --git a/sunpy/net/helio/hec.py b/sunpy/net/helio/hec.py
index 432137118..d879f38af 100644
--- a/sunpy/net/helio/hec.py
+++ b/sunpy/net/helio/hec.py
@@ -6,11 +6,13 @@ import os
 
 from lxml import etree
 from requests import Session
+from requests.exceptions import SSLError
 from zeep import Client
 from zeep.transports import Transport
 
 from astropy.io.votable.table import parse_single_table
 
+from sunpy import log
 from sunpy.net import attrs as a
 from sunpy.net.base_client import BaseClient, QueryResponseTable
 from sunpy.net.helio import attrs as ha
@@ -87,7 +89,12 @@ class HECClient(BaseClient):
         # This is for use in our test suite.
         session.verify = not (bool(os.environ.get("NO_VERIFY_HELIO_SSL", 0)))
         transport = Transport(session=session)
-        self.hec_client = Client(link, transport=transport)
+        try:
+            self.hec_client = Client(link, transport=transport)
+        except SSLError:
+            log.warning('SSL verification error for HEC client.\n'
+                     'Set the \'NO_VERIFY_HELIO_SSL\' environment variable disable SSL verification for Helio.')
+            self.hec_client = None
 
     @classmethod
     def _can_handle_query(cls, *query):
@@ -137,6 +144,9 @@ class HECClient(BaseClient):
         <BLANKLINE>
         <BLANKLINE>
         """
+        if self.hec_client is None:
+            table = HECResponse([], client=self)
+            return table
         qrdict = {}
         for elem in args:
             if isinstance(elem, a.Time):


Test patch:
diff --git a/sunpy/net/helio/tests/test_helio.py b/sunpy/net/helio/tests/test_helio.py
index b79b01ef7..30632d0de 100644
--- a/sunpy/net/helio/tests/test_helio.py
+++ b/sunpy/net/helio/tests/test_helio.py
@@ -2,6 +2,7 @@ import urllib
 from unittest import mock
 
 import pytest
+from requests.exceptions import SSLError
 
 from sunpy.net import attrs as a
 from sunpy.net.helio.hec import HECClient
@@ -247,6 +248,16 @@ def test_link_test_on_urlerror(mock_link_test):
     link_test('') is None
 
 
+@mock.patch('sunpy.net.helio.parser.webservice_parser', return_value=wsdl_urls())
+@mock.patch('sunpy.net.helio.parser.taverna_parser', return_value=some_taverna_urls())
+@mock.patch('sunpy.net.helio.parser.link_test', return_value='some text read')
+@mock.patch('sunpy.net.helio.hec.Client', side_effect=SSLError('SSL error'))
+def test_ssl_verify_error(mock_webservice, mock_taverna, mock_link, mock_zeep, caplog):
+    client = HECClient()
+    query = client.search(a.Time('2023/02/03', '2023/02/03'))
+    assert len(query) == 0
+    assert "Set the 'NO_VERIFY_HELIO_SSL' environment variable disable SSL verification for Helio." in caplog.text
+
 @pytest.fixture(scope="session")
 def client():
     try: