handlign timeout with Ajax call

Viewed 474

I am not a JS guy (can write some basic stuff), I am working on integrating our Payment API with the front end, as part of the process, we want to trigger a timeout in case my back-end Payment API is not responding within a given timeframe. Here is the JS code which interacting with the Payment API.

performAuthorizePaymentRequest: function (payment) {
        return new Promise(function (resolve, reject) {
            $.ajax({
                type: 'POST',
                url: encodedContextPath + '/checkout/authorise_order',
                data: JSON.stringify(payment),
                dataType: 'json',
                contentType: 'application/json',
                success: resolve,
                error: reject,
                always: function () {
                     pageSpinner.end() 
                },
                timeout: 30000 /* call this.session.completePayment() within 30 seconds or the payment is cancelled */
            });
       });
    }

This seems to be working fine, however I have seen the following issue.

  1. In case m backed API throws any error, the code is still waiting for the timeout.
  2. Even for success cases, the js is waiting for timeout before showing success message.

Is there a way to handle this? Let me know in case I am doing something wrong here.

Note: This code will only execute of Safari, for other browser this is not available

7 Answers

You can use the abort method of the XMLHttpRequest object and then set a timeout to abort the request after 30 seconds. Like so:

var requestObj = $.ajax({
    ...
)};
setTimeout(function(){
    requestObj.abort();
}, 30 * 1000); // 30 seconds

See XMLHttpRequest.abort reference


Another approach is to have the success function cease to work after 30 seconds by setting a status flag to 'out of time'. See comments in the code below:

/* Use this variable to check whether we are within 30 minutes. This can be:
    'still in time': the original state, waiting for a response
    'out of time': we passed the 30 minutes, don't do anything with a possible success response and call the reject function
    'already resolved': resolved within 30 minutes, no need to reject after 30 minutes
*/
var timeStatus = 'still in time';
/* Time until timeout */
var timeoutTime = 30 * 1000; //30 seconds
/* Save the request */
var theRequest = $.ajax({
    ...
    /* In success we must check if we are still in time */
    success: function(data, textStatus, jqXHR){
        
        if(timeStatus === 'out of time'){
            // BAIL OUT. Do nothing since this has alreay been rejected in the setTimeout function. */
            console.log("bailing out. 30 minutes exceeded");
            return false;
        }
        else{
            /* Set to 'already resolved' so our setTimeout function doesn't reject. */
            timeStatus = 'already resolved';
            resolve(data, textStatus, jqXHR);
        }
    },
    /* In error we do the same 'out of time' check */
    error: function (jqXHR, errorCode, error){
        if(timeStatus === 'out of time'){
            // BAIL OUT. Do nothing since the request has alreay been rejected in the setTimeout function. */
            console.log("bailing out. 30 minutes exceeded");
            return false;
        }
        else{
            /* Set to 'already resolved' so our setTimeout function doesn't reject. */
            timeStatus = 'already resolved';
            reject(jqXHR, errorCode, error);
        }
    }
});
/* After 30 minutes set the timeStatus to 'out of time' so we can cut off incoming ajax responses. */
setTimeout(function(){
    if(timeStatus !== 'already resolved'){
        timeStatus = 'out of time';
        reject(theRequest, 'timeout','');
    }
}, timeoutTime); 

I would avoid a timeout in the ajax call, but would go to a with a set_time_limit(30) in the php (if your /checkout/authorise_order is a php...)

if the answer arrive before, all fine (won't wait untill 30sec) it's a time limit...

For javascript part I use my own :

function ajaxLive ( divID ) {
    var pcache = (Math.floor(Math.random() * 100000000) + 1);
    var params = "divID="+encodeURIComponent(divID);
    var xhr = new XMLHttpRequest(); 
    xhr.open("POST", "path_to_script.php?pcache="+pcache, true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xhr.onprogress = function(e) { if (xhr.readyState == 4) { $("#"+divID).html(e.currentTarget.responseText) ; } }
    xhr.send(params);
}

in this function pcache if crucial to avoid caching because the purpose of api calls is to have a different answer each time...

in the params you can add extra stuff too and it will pass it to php

If you use php curl or shell curl, there is also max time mechanisms

curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 0); // for the connection (not the answer)
curl_setopt($ch, CURLOPT_TIMEOUT, 30); //timeout in seconds

You can abort the timeout like this maybe ? the example is at error, but you can do the same at success

$.ajax({
url: "/your_ajax_method/",
type: "GET",
dataType: "json",
timeout: 3000, //Set your timeout value in milliseconds or 0 for unlimited
success: function(response) { alert(response); },
error: function(jqXHR, textStatus, errorThrown) {
    if(textStatus!=="timeout")  
          jqXHR.abort("timeout");    
}

});​

I would recommend sticking to fetch instead of ajax because it timeouts after 300s by default and does fail if the server returns an error.

You can also abort the request before the default timeout using an abort controller https://developer.mozilla.org/en-US/docs/Web/API/AbortController.

The same code in fetch would be:


const performAuthorizePaymentRequest = (payment) =>
  fetch(`${encodedContextPath}/checkout/authorise_order`, {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify(payment), 
  }).then(r=>{
   pageSpinner.end();
   return r.json();
  })

performAuthorizePaymentRequest: function (payment) {

// you can start a loading on your view in here like this
// $('.fix-loading').show('fast');
      let promises_await = [];
      promises_await.push(
                    $.ajax({
                        type: 'POST',
                        url: encodedContextPath + '/checkout/authorise_order',
                        data: JSON.stringify(payment),
                        dataType: 'json',
                        contentType: 'application/json',
                        success: resolve,
                        error: reject,
// I don't know are these lines below are necessary for your payment or not
                        //always: function () {
                        //     pageSpinner.end() 
                        //},
                        //timeout: 30000 /* call this.session.completePayment() within 30 seconds or the payment is cancelled */
                    });
             );

$.when.apply($, promises_await).then(function () {
// if all requests in ajax complete and receive answer, do something here

// also if use loading you can hide it here like this and also in catch
// $('.fix-loading').hide('fast');
}).catch(function () {
// also if use loading you can hide it here like this and also in catch
// $('.fix-loading').hide('fast');
//error handling
});
}

You can using promise.race().

The Promise.race() method returns a promise that fulfills or rejects as soon as one of the promises in an iterable fulfills or rejects, with the value or reason from that promise.

Example:

const timeout = (prom, time) =>
    Promise.race([prom, new Promise((_r, rej) => setTimeout(rej, time))]);

Use:

// resolves in 500 ms
const fn = async () => {
    await new Promise((res) => setTimeout(res, 500));
    return "p2";
}

// finishes before the timeout
const result = await timeout(fn(), 1000);
// result = p2

// timeouts in 100 ms
await timeout(fn(), 100);
// error

ref: anonystick.com

  • I will suggest you something I don't know if you will like it or will works fine for you or not but you can use setInterval() method this method will repeat your code automatically for many times you can create a condition if event happend stop this setInterval() function

Example

    var check = false;
    setInterval(function() {
        if (check === true) {
            return;
        }
        $.ajax({
            type: 'POST',
            url: encodedContextPath + '/checkout/authorise_order',
            data: JSON.stringify(payment),
            dataType: 'json',
            contentType: 'application/json',
            success: function(e) {
                if (e.status === 200) {
                    check = true;
                }
            },
            always: function() {
                pageSpinner.end()
            }
        });
    }, 10);
Related