Cannot reconnect properly through http.client

Viewed 703

I have a Telegram bot that makes constant update requests to a Telegram API.

Occasionally I get an error like this:

18-12-16 12:12:37: error: Traceback (most recent call last):
  File "/home/pi/MuseBot/main.py", line 157, in <module>
    main()
  File "/home/pi/MuseBot/main.py", line 34, in main
    updates = HANDLER.makeRequest("getUpdates", {"timeout": REQUEST_DELAY, "offset": lastOffset})
  File "/home/pi/MuseBot/functions.py", line 42, in makeRequest
    response = self.con.getresponse()
  File "/usr/lib/python3.5/http/client.py", line 1198, in getresponse
    response.begin()
  File "/usr/lib/python3.5/http/client.py", line 297, in begin
    version, status, reason = self._read_status()
  File "/usr/lib/python3.5/http/client.py", line 258, in _read_status
    line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
  File "/usr/lib/python3.5/socket.py", line 576, in readinto
    return self._sock.recv_into(b)
  File "/usr/lib/python3.5/ssl.py", line 937, in recv_into
    return self.read(nbytes, buffer)
  File "/usr/lib/python3.5/ssl.py", line 799, in read
    return self._sslobj.read(len, buffer)
  File "/usr/lib/python3.5/ssl.py", line 583, in read
    v = self._sslobj.read(len, buffer)
OSError: [Errno 113] No route to host

After getting this a few times, I implemented automatic restarts for the bot - I figured that this error isn't something I can stop, it's on the server side.

However I've had trouble with that.

I have an API handler class to make it easier for me to manage API requests. This is instantiated once only, when the script is run, not after each restart.

It creates a connection like so:

class ApiHandler:
    def __init__(self):
        self.con = http.client.HTTPSConnection(URL, 443)

And when something goes wrong, I call this function on it:

    def reconnect(self):
        self.con.close()
        self.con = http.client.HTTPSConnection(URL, 443)

I then make requests with this handler using:

    def makeRequest(self, cmd, data={}):
        jsonData = json.dumps(data)
        try:
            self.con.request("POST", REQUEST_URL+cmd, jsonData, HEADERS)
        except:
            debug("An error occurred while carrying out the API request", 1)

        response = self.con.getresponse()
        decodedResponse = json.loads(response.read().decode())
        if not decodedResponse["ok"]:
            debug("reponse: {}".format(decodedResponse), 3)
            raise ApiError(decodedResponse["error_code"])
            return False

        return decodedResponse["result"]

(ApiError is just a class constructed from Exception which I use to distinguish it from other errors when handling crashes.) Any time I get the above error I automatically reconnect. But every subsequent makeRequest call produces this error:

18-12-16 12:13:02: notice: An error occurred while carrying out the API request
18-12-16 12:13:02: error: Traceback (most recent call last):
  File "/home/pi/MuseBot/main.py", line 157, in <module>
    main()
  File "/home/pi/MuseBot/main.py", line 23, in main
    metaData = HANDLER.makeRequest("getMe")
  File "/home/pi/MuseBot/functions.py", line 42, in makeRequest
    response = self.con.getresponse()
  File "/usr/lib/python3.5/http/client.py", line 1194, in getresponse
    response = self.response_class(self.sock, method=self._method)
  File "/usr/lib/python3.5/http/client.py", line 235, in __init__
    self.fp = sock.makefile("rb")
AttributeError: 'NoneType' object has no attribute 'makefile'

I then reconnect automatically and try again, but the error happens again and the bot enters a hellish error loop, which left unchecked creates a massive log file and crashes my raspberry pi. The only solution is often to restart my raspberry pi.

I would have thought that creating a new connection in reconnect with:

self.con = http.client.HTTPSConnection(URL, 443)

would fix it, but apparently not.

I'm at a loss for how to automatically restart my bot without creating an error loop. Any help is very very much appreciated.

1 Answers

Dear @JThistle do not take this personally but you have a few bugs in makeRequest, pleas read my comments in your code and then an example that should do what you expect.

Your code:

def makeRequest(self, cmd, data={}):
    jsonData = json.dumps(data)
    try:
        self.con.request("POST", REQUEST_URL+ cmd, jsonData, HEADERS)
    except:
        # this is you main bug you do not rerise the exception so you
        # never actually reconnect
        debug("An error occurred while carrying out the API request", 1)

    # so your program follows to this step and the socket is closed when you try
    # to read from it, hence the second exception
    response = self.con.getresponse()

    decodedResponse = json.loads(response.read().decode())
    if not decodedResponse["ok"]:
        debug("reponse: {}".format(decodedResponse), 3)
        raise ApiError(decodedResponse["error_code"])
        # this is wrong you can not reach this return, raise exception ends execution
        # and goes up the stack until it will be caught or will crash the program
        return False

    return decodedResponse["result"]

This should rerise the exception so your reconnect can actually happen:

# python uses snake case not camel case, it might seem silly but pytonistas are
# easily annoyed bunch
def make_request(self, cmd, data=None):
    # it is risky to use a reference type as default argument
    # https://docs.python-guide.org/writing/gotchas/
    data = data or {}
    json_data = json.dumps(data)

    try:
        self.con.request("POST", REQUEST_URL + cmd, json_data, HEADERS)
    except:
        debug("An error occurred while carrying out the API request", 1)
        raise  # important

    response = self.con.getresponse()

    decoded_response = json.loads(response.read().decode())
    # if the 'ok' is not present in the response string you will get 'KeyError'
    # so use .get("ok") to get None instead
    if not decoded_response.get("ok"):
        debug("reponse: {}".format(decoded_response), 3)
        error = decoded_response.get("error_code")
        # what if the "error_code" not in the response, response could be empty ?
        raise ApiError(error)

    # again response could be malformed and you will get "KeyError" if
    # "result" is not present
    return decoded_response["result"]
Related