Search code examples
typescripterror-handlingaxiosunhandled-exceptionhomebridge

Why does an axios request error result in an unhandled exception even though all calls are prefaced by await and wrapped in try/catch blocks?


I'm developing a homebridge plugin in TypeScript and I'm using axios for my network requests (GitHub link). The plugin logs in to a server in the local network once and then polls the server in fixed intervals to retrieve information about the server's state. If the session expires, the server will return a 302 error and redirect to the login page, so the plugin does not allow redirects and looks out for 30x errors, which signify a need to log in again and renew the session, before retrying the actual request. I use a helper function to make my actual network requests, and every single call to this function is wrapped in try { await... } catch { } blocks. Regardless, occasionally an error somehow skips the error handling mechanism and propagates back to the main event loop, where it crashes the plugin, as it goes unhandled.

The relevant code is as follows:

In the class constructor:

    // [...]
    this.client = axios.create({
      baseURL: this.httpPrefix + this.IPAddress,
      timeout: 10000,
      maxRedirects: 0,
      method: "post",
      headers: {
        'content-type': 'application/x-www-form-urlencoded'
      }
    });
    // [...]

In the polling function:

    [...]
    try {
      const response = await this.makeRequest('user/seq.xml', {'sess': this.sessionID});
      // [...]
    } catch (error) { throw(error); }

The actual helper function that handles requests:

  private async makeRequest(address: string, payload = {}) {
    try {
      return await this.client({
        url: address,
        data: payload
      });
    } catch (error) {
      if (axios.isAxiosError(error)) {
        const response = error.response;
        if (response == undefined) throw error;
        if (response.status >= 300 && response.status < 400) {
          await this.login();
          payload['sess'] = this.sessionID;
          const response = await this.client({
            url: address,
            data: payload
          });
          return response;
        } else throw(error);
      } else throw (error);
    }
  }

And this is the function that handles scheduling the polling on fixed intervals:

  async updateAccessories() {
    // [...]
    try {
      await this.securitySystem.poll();
      // [...]
    } catch (error) {
      this.log.error((<Error>error).message);
      await delay(retryDelayDuration);
    }

    setTimeout(this.updateAccessories.bind(this), this.pollTimer);
  }

The delay function called above is a small helper that is as follows:

export function delay(milliseconds: number) {
    return new Promise(resolve => setTimeout(resolve, milliseconds));
}

Essentially the homebridge server loads up the plugin, which, after initial login and accessories discovery, calls the updateAccessories function for the first time, and by itself will use setTimeout to reschedule itself to run again after the pollTimer interval. poll() is called, which then performs all the necessary logic to query the server, retrieve and parse all the relevant data and update the data model, and so on. The idea is that if a poll fails for any reason, the plugin should gracefully gloss over it and retry at the next polling attempt.

You see how every axios request is called with await and wrapped in a try/catch block to check for 30x errors, and the helper function itself is also called with a similar mechanism. In theory, all errors should be caught and dealt with higher up in the program's logic. However, I'm getting intermittent errors like this:

AxiosError: Request failed with status code 302
    at settle (/usr/lib/node_modules/homebridge-caddx-interlogix/node_modules/axios/lib/core/settle.js:19:12)
    at IncomingMessage.handleStreamEnd (/usr/lib/node_modules/homebridge-caddx-interlogix/node_modules/axios/lib/adapters/http.js:495:11)
    at IncomingMessage.emit (node:events:525:35)
    at endReadableNT (node:internal/streams/readable:1358:12)
    at processTicksAndRejections (node:internal/process/task_queues:83:21)

It appears as if some axios failed calls end up escaping error handling and bubble up to the main event loop, therefore crashing the program. I did some searching and made sure the setTimeout code is called outside of try/catches, but still the errors come up every so often.

Any ideas on this? Thanks in advance.


Solution

  • Update: I didn't manage to actually stop the uncaught 302 errors from occurring during program execution, but I did solve the problem in another way: I added a validateStatus option to the creation of the client object in the class constructor:

      [...],
      validateStatus: function (status) {
        return (status >= 200 && status < 400);
      }
    

    This part of code in conjunction with maxRedirects: 0 makes axios not follow 302 redirects, but not consider them as errors, giving me the chance to handle these status codes manually in the helper function, checking the response.status value. Just leaving this here for anyone that might need something like this :)