Retry nodejs http.request (post,put,delete)

2019-08-28 00:41发布

问题:

What is the correct way to implement a retry on error/condition without using any third party modules in nodejs, please?

I'm not sure how to call the same function on the error and how to then pass the original callback/data to the newly called function?

Do I need to destroy/end the sockets?

I've tried looking for examples but have only found reference to third party modules and http.get samples which don't seem to work. How does one test this? I have attempted the below without success:

      async pingApi(cb) {
        let options = {
        "method":"post",
         "path": `/API/pingAPI?${this.auth}`, /ect do I reference this path?
          }
        };
        let request = await http.request(options, (response) => {
          let body = new Buffer(0);
          response.on('data', (chunk) => {
            body = Buffer.concat([body, chunk]);
          });
          response.on('end', function () {
            if (this.complete) {
              let decoded = new Buffer(body, 'base64').toString('utf8')
              let json = JSON.parse(decoded);
              if (json.result != 'OK') {
                setTimeout(pingApi, 1000); //cant pass callback
              } else {
                cb(null, json.result) //works
              }
            }
          });
        })
        request.end(); //does the socket need to be closed if retry is this function?
      }

Any help, pointing in the right direction or criticism will be greatly appreciated as I think this is a very important learning curve for me.

Thank you in advance,

回答1:

I'm not sure how to call the same function on the error and how to then pass the original callback/data to the newly called function?

I don't know for sure that everything else in your function is correct, but you can fix the recursion that you're asking about by changing this:

setTimeout(pingApi, 1000); //cant pass callback

to this:

setTimeout(() => {
    this.pingApi(cb);
}, 1000);

You aren't showing the whole context here, but if pingApi() is a method, then you also need to keep track of the this value to you can call this.pingApi(db). You can preserve the value of this by using arrow function callbacks like this:

response.on('end', () => { ... });

Other things I notice that look off here:

  1. There's no reason to use await http.request(). http.request() does not return a promise so using await with it does not do anything useful.
  2. Without the await, there's then no reason for your function to be declared async since nobody is using a returned promise from it.
  3. It's not clear what if (this.complete) is meant to do. Since this is inside a regular function callback, the value of this won't be your pingApi object. You should either save this higher in the scope typically with const self = this or all callbacks internally need to be arrow functions so the value of this is preserved.
  4. You should probably put try/catch around JSON.parse() because it can throw if the input is not perfect JSON.
  5. You should probably not retry forever. Servers really hate clients that retry forever because if something goes wrong, the client may just be bashing the server every second indefinitely. I'd suggest a certain number of max retries and then give up with an error.

Do I need to destroy/end the sockets?

No, that will happen automatically after the request ends.

How does one test this?

You have to create a test route in your server that returns the error condition for the first few requests and then returns a successful response and see if your code works with that.


Here's an attempt at a code fixup (untested):

const maxRetries = 10;

pingApi(cb, cnt = 0) {
    let options = {
    "method":"post",
     "path": `/API/pingAPI?${this.auth}`, // ect do I reference this path?
    };
    let request = http.request(options, (response) => {
      let body = new Buffer(0);
      response.on('data', (chunk) => {
        body = Buffer.concat([body, chunk]);
      });
      response.on('end', () => {
        if (this.complete) {
          let decoded = new Buffer(body, 'base64').toString('utf8')
          try {
            let json = JSON.parse(decoded);
            if (json.result != 'OK') {
                if (cnt < maxRetries)
                    setTimeout(() => {
                        this.pingApi(cb, ++cnt);
                    }, 1000);
                 } else {
                    cb(new Error("Exceeded maxRetries with error on pingApi()"));
                 }
            } else {
                cb(null, json.result) //works
            }
          } catch(e) {
              // illegal JSON encountered
              cb(e);
          }
        }
      });
    })
    request.end();
 }

Remaining open questions about this code:

  1. What is this.complete doing and what this should it be referencing?
  2. Why is there no request.write() to send the body of the POST request?

I know you ask for no external modules, but my preferred way of doing this would be to use promises and to use the request-promise wrapper around http.request() because it handles a lot of this code for you (checks response.status for you, parses JSON for you, uses promise interface, etc...). You can see how much cleaner the code is:

const rp = require('request-promise');
const maxRetries = 5;

pingApi(cnt = 0) {
    let options = {
        method: "post",
        url: `http://somedomain.com/API/pingAPI?${this.auth}`, 
        json: true
    };
    return rp(options).then(result => {
        if (result.result === "OK") {
            return result;
        } else {
            throw "try again";  // advance to .catch handler
        }
    }).catch(err => {
        if (cnt < maxRetries) {
            return pingApi(++cnt);
        } else {
            throw new Error("pingApi failed after maxRetries")
        }
    });
}

And, then sample usage:

pingApi().then(result => {
    console.log(result);
}).catch(err => {
    console.log(err);
})


回答2:

your use of async/await with core node server intrigued me and I've tried to use much as possible of this new async features.

This is what I end up with: https://runkit.com/marzelin/pified-ping

const pify = require("util").promisify;
const http = require("http");

const hostname = "jsonplaceholder.typicode.com";
const failEndpoint = "/todos/2";
const goodEndpoint = "/todos/4";

let options = {
  method: "get",
  path: `${failEndpoint}`,
  hostname
};


async function ping(tries = 0) {
  return new Promise((res) => {
    const req = http.request(options, async (response) => {
      let body = new Buffer(0);
      response.on("data", (chunk) => {
        body = Buffer.concat([body, chunk]);
      })

      const on = pify(response.on.bind(response));

      await on("end");
      let decoded = new Buffer(body, 'base64').toString('utf8')
      let json = JSON.parse(decoded);
      if (json.completed) {
        return res("all good");
      }
      if (tries < 3) {
        console.log(`retrying ${tries + 1} time`);
        return res(ping(tries + 1));
      }
      return res("failed");
    })

    req.on('error', (e) => {
      console.error(`problem with request: ${e.message}`);
    });

    // write data to request body
    req.end();
  })
}

const status = await ping();
"status: " + status