I am writing unit tests for a python program using the unittest framework. One of the functions of the program is connecting to an external REST API using the requests library. If there is a connection error or timeout, I would like the function to retry up to 3 times before failing. As such I have written a test which uses a mock (actually httpretty) to replace the external API and raises requests.ConnectionError twice before returning something.
class APITests(unittest.TestCase):
def mock_response(self, uri, body='OK', method=httpretty.GET,
status_code=200, error=None,
error_repeats=None):
"""
Function to register HTTPretty response
"""
responses = []
if error:
def callback(request, uri, headers):
raise error
if error_repeats:
responses += [httpretty.Response(body=callback)]*error_repeats
responses += [httpretty.Response(body=body,
status=status_code)]
else:
responses += [httpretty.Response(body=callback)]
else:
responses += [httpretty.Response(body=body, status=status_code)]
httpretty.register_uri(method, uri, responses=responses)
#httpretty.activate
def test_get_connection_error_then_success_doesnt_raise(self):
self.mock_response(
'http://irrelevant.com',
error=requests.ConnectionError,
error_repeats=2
)
api._get('http://irrelevant.com')
This works fine and the test passes when an exception is not raised no the third attempt but the two exceptions that are raised (intentionally) and caught and handled by the code are printed to the console, polluting the test output. Is there a clean way to stop this happening?
Further info
Here is the method(s) I am testing
def _get(self, url, retries=3):
while retries > 0:
try:
r = requests.get(url)
try:
r.raise_for_status()
return r
except requests.HTTPError as e:
self._handle_HTTP_error(e)
except (requests.ConnectionError, requests.Timeout) as e:
retries -= 1
if not retries:
self._handle_connection_error(e)
def _handle_connection_error(self, error):
raise requests.ConnectionError('Could not connect to API')
The console output is:
Exception in thread Thread-23:
Traceback (most recent call last):
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 810, in __bootstrap_inner
self.run()
File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/threading.py", line 763, in run
self.__target(*self.__args, **self.__kwargs)
File "/Users/lukecaldwell/Documents/Repos/other/AidTrends/venv/lib/python2.7/site-packages/httpretty/core.py", line 637, in fill_filekind
headers
File "/Users/lukecaldwell/Documents/Repos/other/AidTrends/aidtrends/tests/test_OECD.py", line 131, in callback
raise error
ConnectionError
Related
I am sending multiple requests with aiohttp using tor's http proxy (with aiohttp_socks)
After some requests are done I am getting the following error:
Traceback (most recent call last):
File "main.py", line 171, in <module>
loop.run_until_complete(future)
File "/usr/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete
return future.result()
File "main.py", line 95, in get_market_pages
async with session.get(active_link, headers=headers) as response:
File "/home/mrlalatg/.local/lib/python3.8/site-packages/aiohttp/client.py", line 1012, in __aenter__
self._resp = await self._coro
File "/home/mrlalatg/.local/lib/python3.8/site-packages/aiohttp/client.py", line 504, in _request
await resp.start(conn)
File "/home/mrlalatg/.local/lib/python3.8/site-packages/aiohttp/client_reqrep.py", line 847, in start
message, payload = await self._protocol.read() # type: ignore # noqa
File "/home/mrlalatg/.local/lib/python3.8/site-packages/aiohttp/streams.py", line 591, in read
await self._waiter
aiohttp.client_exceptions.ClientOSError: [Errno 1] [SSL: APPLICATION_DATA_AFTER_CLOSE_NOTIFY] application data after close notify (_ssl.c:2745)
I can't find any information about this error, except the git discussion about a similar one - github
There I found a workaround (link) that I can modify to ignore this error, but it didn't work, the error is still there.
The modified version of a workaround:
SSL_PROTOCOLS = (asyncio.sslproto.SSLProtocol,)
try:
import uvloop.loop
except ImportError:
pass
else:
SSL_PROTOCOLS = (*SSL_PROTOCOLS, uvloop.loop.SSLProtocol)
def ignore_aiohttp_ssl_eror(loop):
"""Ignore aiohttp #3535 / cpython #13548 issue with SSL data after close
There is an issue in Python 3.7 up to 3.7.3 that over-reports a
ssl.SSLError fatal error (ssl.SSLError: [SSL: KRB5_S_INIT] application data
after close notify (_ssl.c:2609)) after we are already done with the
connection. See GitHub issues aio-libs/aiohttp#3535 and
python/cpython#13548.
Given a loop, this sets up an exception handler that ignores this specific
exception, but passes everything else on to the previous exception handler
this one replaces.
Checks for fixed Python versions, disabling itself when running on 3.7.4+
or 3.8.
"""
orig_handler = loop.get_exception_handler()
def ignore_ssl_error(loop, context):
if context.get("message") in {
"SSL error in data received",
"Fatal error on transport",
}:
# validate we have the right exception, transport and protocol
exception = context.get('exception')
protocol = context.get('protocol')
if (
isinstance(exception, ssl.SSLError)
and exception.reason == 'APPLICATION_DATA_AFTER_CLOSE_NOTIFY'
and isinstance(protocol, SSL_PROTOCOLS)
):
if loop.get_debug():
asyncio.log.logger.debug('Ignoring asyncio SSL KRB5_S_INIT error')
return
if orig_handler is not None:
orig_handler(loop, context)
else:
loop.default_exception_handler(context)
loop.set_exception_handler(ignore_ssl_error)
I'm using the Firebase Realtime Database listener to listen to changes on a database path.
My program recently crashed because of the following 503 error that seems to be raised by the underlying requests library:
Traceback (most recent call last):
File "/usr/local/lib/python3.7/threading.py", line 917, in _bootstrap_inner
self.run()
File "/usr/local/lib/python3.7/threading.py", line 865, in run
self._target(*self._args, **self._kwargs)
File "/usr/local/lib/python3.7/site-packages/firebase_admin/db.py", line 123, in _start_listen
for sse_event in self._sse:
File "/usr/local/lib/python3.7/site-packages/firebase_admin/_sseclient.py", line 128, in __next__
self._connect()
File "/usr/local/lib/python3.7/site-packages/firebase_admin/_sseclient.py", line 112, in _connect
self.resp.raise_for_status()
File "/usr/local/lib/python3.7/site-packages/requests/models.py", line 940, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 503 Server Error: Service Unavailable for url: https://database_url...
My listener initialization is wrapped in a try statement, so I'm unsure why this wasn't caught, swallowed and retried as I expected it to:
def init_listener():
try:
listener = firebase_admin.db.reference(db_path).listen(handle_change)
except Exception as e:
time.sleep(1) # Retry in one second.
init_listener()
I'd like to handle future 503 errors, but I'm not sure how to go about doing this.
Additionally, I'm using except Exception as e above for demo/debugging purposes, but I'm also not sure if requests.exceptions.HTTPError will be specific enough to catch only 500 errors (though I don't know what other errors can be raised).
From the firebase_admin reference docs:
This API is based on the event streaming support available in the
Firebase REST API. Each call to listen() starts a new HTTP connection
and a background thread. This is an experimental feature.
The key here is that this all runs in a background thread. Therefore, wrapping the call to listen() in a try/except will not catch exceptions thrown in the thread. There is no simple way to catch the exceptions happening in the background thread.
To solve your issue, you will probably need to know more about why the database is returning an HTTP 503 status. Or you will need to switch to some other firebase_admin API that will allow you to catch and ignore these exceptions.
I see the following exception sometimes when I try to hit my home page.
ERROR:root:HTTPConnectionPool(host='0.0.0.0', port=8003): Max retries exceeded with url:
/snapshot/?app=cdnstats&key=28736ba5fbe151d5ff6678015c8f6ade (Caused by <class 'socket.error'>:
[Errno 61] Connection refused)
Traceback (most recent call last):
File "/Users/rokumar/CDNStats/cdnstats/app/core/views.py", line 257, in get_snapshot_data
data = templates.render_snapshot(controllers.get_snapshot_data())
File "/Users/rokumar/CDNStats/cdnstats/util.py", line 260, in decorated
expiry, mem_args, func, args, kwargs)
File "/Users/rokumar/CDNStats/cdnstats/util.py", line 227, in get_data_from_meminstance
data = func(*args, **kwargs)
File "/Users/rokumar/CDNStats/cdnstats/app/core/controllers.py", line 255, in get_snapshot_data
return util.call_get_api(config.CDNSTATS_API_URL + 'snapshot/?', data)
File "/Users/rokumar/CDNStats/cdnstats/util.py", line 123, in call_get_api
raise ex
The following is the piece of code generating the exception.
def call_get_api(url, data):
try:
data = data.copy()
data['key'] = request.args.get('key')
data['app'] = config.APPNAME
query = soft_urlencode(data)
response = requests.get(url + query)
if response.status_code == 200:
return response.json()
else:
apiexception = APIException(response.content)
apiexception.status_code = response.status_code
raise apiexception
except UnicodeEncodeError as ex:
print ex
raise ex
except Exception as ex:
raise ex
I see the exception intermittently and my app slows down heavily. I don;t really understand the exception or what is wrong. The exception says max retries exceeded but I do not have any retry logic going on.
in urlopen, try setting retries=False or retries=1. The default is 3 so that will probably be your retry logic going on there.
I am seeing the python-requests library crash with the following traceback:
Traceback (most recent call last):
File "/usr/lib/python3.2/http/client.py", line 529, in _read_chunked
chunk_left = int(line, 16)
ValueError: invalid literal for int() with base 16: b''
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "./app.py", line 507, in getUrlContents
response = requests.get(url, headers=headers, auth=authCredentials, timeout=http_timeout_seconds)
File "/home/dotancohen/code/lib/requests/api.py", line 55, in get
return request('get', url, **kwargs)
File "/home/dotancohen/code/lib/requests/api.py", line 44, in request
return session.request(method=method, url=url, **kwargs)
File "/home/dotancohen/code/lib/requests/sessions.py", line 338, in request
resp = self.send(prep, **send_kwargs)
File "/home/dotancohen/code/lib/requests/sessions.py", line 441, in send
r = adapter.send(request, **kwargs)
File "/home/dotancohen/code/lib/requests/adapters.py", line 340, in send
r.content
File "/home/dotancohen/code/lib/requests/models.py", line 601, in content
self._content = bytes().join(self.iter_content(CONTENT_CHUNK_SIZE)) or bytes()
File "/home/dotancohen/code/lib/requests/models.py", line 542, in generate
for chunk in self.raw.stream(chunk_size, decode_content=True):
File "/home/dotancohen/code/lib/requests/packages/urllib3/response.py", line 222, in stream
data = self.read(amt=amt, decode_content=decode_content)
File "/home/dotancohen/code/lib/requests/packages/urllib3/response.py", line 173, in read
data = self._fp.read(amt)
File "/usr/lib/python3.2/http/client.py", line 489, in read
return self._read_chunked(amt)
File "/usr/lib/python3.2/http/client.py", line 534, in _read_chunked
raise IncompleteRead(b''.join(value))
http.client.IncompleteRead: IncompleteRead(0 bytes read)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/usr/lib/python3.2/threading.py", line 740, in _bootstrap_inner
self.run()
File "./app.py", line 298, in run
self.target(*self.args)
File "./app.py", line 400, in provider_query
url_contents = getUrlContents(str(providerUrl), '', authCredentials)
File "./app.py", line 523, in getUrlContents
except http.client.IncompleteRead as error:
NameError: global name 'http' is not defined
As can be seen, I've tried to catch the http.client.IncompleteRead: IncompleteRead(0 bytes read) error that requests is throwing with the line except http.client.IncompleteRead as error:. However, that is throwing a NameError due to http not being defined. So how can I catch that exception?
This is the code throwing the exception:
import requests
from requests_oauthlib import OAuth1
authCredentials = OAuth1('x', 'x', 'x', 'x')
response = requests.get(url, auth=authCredentials, timeout=20)
Note that I am not including the http library, though requests is including it. The error is very intermittent (happens perhaps once every few hours, even if I run the requests.get() command every ten seconds) so I'm not sure if added the http library to the imports has helped or not.
In any case, in the general sense, if included library A in turn includes library B, is it impossible to catch exceptions from B without including B myself?
To answer your question
In any case, in the general sense, if included library A in turn includes library B, is it impossible to catch exceptions from B without including B myself?
Yes. For example:
a.py:
import b
# do some stuff with b
c.py:
import a
# but you want to use b
a.b # gives you full access to module b which was imported by a
Although this does the job, it doesn't look so pretty, especially with long package/module/class/function names in real world.
So in your case to handle http exception, either try to figure out which package/module within requests imports http and so that you'd do raise requests.XX.http.WhateverError or rather just import it as http is a standard library.
It's hard to analyze the problem if you don't give source and just the stout,
but check this link out : http://docs.python-requests.org/en/latest/user/quickstart/#errors-and-exceptions
Basically,
try and catch the exception whereever the error is rising in your code.
Exceptions:
In the event of a network problem (e.g. DNS failure, refused connection, etc),
Requests will raise a **ConnectionError** exception.
In the event of the rare invalid HTTP response,
Requests will raise an **HTTPError** exception.
If a request times out, a **Timeout** exception is raised.
If a request exceeds the configured number of maximum redirections,
a **TooManyRedirects** exception is raised.
All exceptions that Requests explicitly raises inherit
from **requests.exceptions.RequestException.**
Hope that helped.
I have a script running that is testing a series of urls for availability.
This is one of the functions.
def checkUrl(url): # Only downloads headers, returns status code.
p = urlparse(url)
conn = httplib.HTTPConnection(p.netloc)
conn.request('HEAD', p.path)
resp = conn.getresponse()
return resp.status
Occasionally, the VPS will lose connectivity, the entire script crashes when that occurs.
File "/usr/lib/python2.6/httplib.py", line 914, in request
self._send_request(method, url, body, headers)
File "/usr/lib/python2.6/httplib.py", line 951, in _send_request
self.endheaders()
File "/usr/lib/python2.6/httplib.py", line 908, in endheaders
self._send_output()
File "/usr/lib/python2.6/httplib.py", line 780, in _send_output
self.send(msg)
File "/usr/lib/python2.6/httplib.py", line 739, in send
self.connect()
File "/usr/lib/python2.6/httplib.py", line 720, in connect
self.timeout)
File "/usr/lib/python2.6/socket.py", line 561, in create_connection
raise error, msg
socket.error: [Errno 101] Network is unreachable
I'm not at all familiar with handling errors like this in python.
What is the appropriate way to keep the script from crashing when network connectivity is temporarily lost?
Edit:
I ended up with this - feedback?
def checkUrl(url): # Only downloads headers, returns status code.
try:
p = urlparse(url)
conn = httplib.HTTPConnection(p.netloc)
conn.request('HEAD', p.path)
resp = conn.getresponse()
return resp.status
except IOError, e:
if e.errno == 101:
print "Network Error"
time.sleep(1)
checkUrl(url)
else:
raise
I'm not sure I fully understand what raise does though..
If you just want to handle this Network is unreachable 101, and let other exceptions throw an error, you can do following for example.
from errno import ENETUNREACH
try:
# tricky code goes here
except IOError as e:
# an IOError exception occurred (socket.error is a subclass)
if e.errno == ENETUNREACH:
# now we had the error code 101, network unreachable
do_some_recovery
else:
# other exceptions we reraise again
raise
Problem with your solution as it stands is you're going to run out of stack space if there are too many errors on a single URL (> 1000 by default) due to the recursion. Also, the extra stack frames could make tracebacks hard to read (500 calls to checkURL). I'd rewrite it to be iterative, like so:
def checkUrl(url): # Only downloads headers, returns status code.
while True:
try:
p = urlparse(url)
conn = httplib.HTTPConnection(p.netloc)
conn.request('HEAD', p.path)
resp = conn.getresponse()
return resp.status
except IOError as e:
if e.errno == 101:
print "Network Error"
time.sleep(1)
except:
raise
Also, you want the last clause in your try to be a bare except not an else. Your else only gets executed if control falls through the try suite, which can never happen, since the last statement of the try suite is return.
This is very easy to change to allow a limited number of retries. Just change the while True: line to for _ in xrange(5) or however many retries you wish to accept. The function will then return None if it can't connect to the site after 5 attempts. You can have it return something else or raise an exception by adding return or raise SomeException at the very end of the function (indented the same as the for or while line).
put try...except: around your code to catch exceptions.
http://docs.python.org/tutorial/errors.html