How to perform async operation before performing query in custom data adapter?

Hi there,

I have a data grid in my application that uses a custom data adapter based off UrlAdapter. It's working great but now I have a requirement to validate an auth refresh token (and possibly refresh it) before the data query is performed. I've added the logic into the beforeSend handler of the data adapter but it's like the query is being executed before the token refresh can occur (query processing isn't waiting for beforeSend to complete). Here's the code for my custom adapter:

import { UrlAdaptor } from "@syncfusion/ej2-data";
import store from '@/store';
import router from '@/router';

export default class CustomUrlAdaptor extends UrlAdaptor {
  constructor(options) {
    super(options);
  }

  beforeSend(dm, request) {
    // Make sure the auth token hasn't expired
    const isExpired = store.getters['authentication/isExpired'](); // isExpired is a function getter
    if (isExpired) {
      // Perform a refresh of the auth token
      store.dispatch('authentication/refreshToken', 'fetch')
        .then(() => {
          // Add the auth header
          request.setRequestHeader('Authorization', `Bearer ${store.state.authentication.authToken}`);  // <=== This is giving error as the request is no longer in OPENED state
        })
        .catch(() => {
          store.commit('authentication/logout');
          return router.push({ name: 'login' });
        });
    } else {
      // Add the auth header
      request.setRequestHeader('Authorization', `Bearer ${store.state.authentication.authToken}`);
    }
  }
}

When the code reaches the indicated line above, I get the following error:

failed: DOMException: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': The object's state must be OPENED.

Is there a better way to accomplish this?

Thanks,
Jason

10 Replies 1 reply marked as answer

RR Rajapandi Ravi Syncfusion Team January 4, 2021 12:05 PM UTC

Hi Jason, 

Greetings from syncfusion support 

We have analyzed your query and we could see that you are using Custom adaptor and facing the problem with headers. Based on your requirement we have prepared a sample and tried to reproduce the issue. But its working fine from our end. Please refer the below code example, sample and documentation for more information. 

Fetchdata.ts 

import Vue from "vue"; 
import axios from 'axios'; 
import VueAxios from 'vue-axios'; 
 
import { GridPlugin, Page, Toolbar, Edit } from "@syncfusion/ej2-vue-grids"; 
import { DataManager, RemoteSaveAdaptor, UrlAdaptor } from "@syncfusion/ej2-vue-grids/node_modules/@syncfusion/ej2-data"; 
import Component from "vue-class-component"; 
 
 
Vue.use(GridPlugin); 
 
class custom extends UrlAdaptor { 
    beforeSend(dm: DataManager, request: XMLHttpRequest): void { 
        request.setRequestHeader("Authorization", "bearer"); 
    } 
} 
 
export default Vue.extend({ 
    data: () => { 
 
        return { 
            toolbar: ['Add', 'Edit','Delete','Update','Cancel'], 
            editSettings: { allowAdding: true, allowEditing:true, allowDeleting:true }, 
            pageSettings: { pageCount: 3 }, 
        }; 
    }, 
    methods: { 
       
        created() { 
             
        }, 
        load() { 
            var localData = new DataManager({ 
                url: "Home/UrlDatasource", 
                insertUrl: "Home/Insert", 
                updateUrl: "Home/Update", 
                removeUrl: "Home/Delete", 
                adaptor: new custom 
            }); 
            var grid = (document.getElementsByClassName('e-grid')[0] as any).ej2_instances[0]; 
            grid.dataSource = localData; 
        } 
    }, 
    provide: { 
        grid: [Page, Toolbar,Edit] 
    } 
}); 





Screenshot: 

 


Regards,
Rajapandi R 


Marked as answer

JA Jason January 4, 2021 02:51 PM UTC

That's not really the scenario at all. The problem isn't the setting of the header, the problem is that I need to set the header after an asynchronous call to refresh the auth token has occurred. By the time the async call has completed, it appears the request is no longer open so the header can no longer be set. This implies that the request is not waiting for beforeSend to complete before continuing on. Your example would likely have the same issue if you delayed the setting of the header by a second or two.

Jason


RR Rajapandi Ravi Syncfusion Team January 5, 2021 11:54 AM UTC

Hi Jason, 

Thanks for the update 

In our Grid, the beforeSend method is not asynchronous. It is a synchronous method. Before start providing a solution to your query we need more information for our clarification, Please share the below details that would be helpful for us to provide a better solution. 

1)        Please share your exact requirement scenario with a detailed description. 

2)        Please share the complete Grid rendering code. 

3)        Please confirm you are getting headers from any other API or Server and please share with us the scenario about why you should like to call the beforeSend in async call success. 

4)        Please share any issue reproducible sample or try to reproduce the issue with our above attached sample. 

Regards, 
Rajapandi R


JA Jason January 6, 2021 03:44 PM UTC

Most of the answers to your questions are in the original post but I'll try again.

Question 1:
Our app uses auth tokens to verify if a user is logged in. These tokens can expire. We also use refresh tokens to renew an expired auth token. The usual steps for handling tokens occur at the time of the request to the server. The request processing steps are:
  1. Check if auth token has expired. If not, skip to step 3
  2. Make call to server with refresh token to get a new auth token (asynchronous part)
  3. Make request as usual with the auth token in the header (this is why we need async operation, in the case of an expired auth token, we don't know what the valid auth token is until the refresh call has completed)
Question 2:
This applies to all grids in our app but here's the Vue component for a simple one:

<template>
  <div>
    <ejs-grid id="CoursePoolGrid" ref="grid" :dataSource="dataSource">
      <e-columns>
        <e-column field="name" headerText="POOL NAME" :customAttributes="{ class: 'priority-column' }"></e-column>
        <e-column field="noOfCourses" headerText="NUMBER OF COURSES"></e-column>
        <e-column field="slotsAvailablePerDay" headerText="SLOTS PER DAY"></e-column>
        <e-column field="teeTimeAvailable" headerText="SLOTS IN THE POOL"></e-column>
        <e-column field="remainingSlots" headerText="SLOTS REMAINING"></e-column>
      </e-columns>
    </ejs-grid>
  </div>
</template>

<script>
import { DataManager } from "@syncfusion/ej2-data";
import CustomUrlAdaptor from "./customUrlAdaptor";

export default {
  name: 'CoursePools',
  data: () => {
    return {
      pageSettings: { pageSize: 10, pageSizes: true },
      searchSettings: { fields: ['name'] },
      toolbar: ['Search']
    }
  },
  computed: {
    dataSource() {
      return new DataManager({
        url: `${process.env.VUE_APP_API}/coursePools/indexdatatable`,
        headers: [
          { 'Accept': 'application/json' }
        ],
        adaptor: new CustomUrlAdaptor()
      });
    }
  }
};
</script>

And the CustomUrlAdapter code:

import { UrlAdaptor } from "@syncfusion/ej2-data";
import store from '@/store';
import router from '@/router';

export default class CustomUrlAdaptor extends UrlAdaptor {
  constructor(options) {
    super(options);
  }

  beforeSend(dm, request) {
    // Make sure the auth token hasn't expired
    const isExpired = store.getters['authentication/isExpired'](); // isExpired is a function getter
    if (isExpired) {
      // Perform a refresh of the auth token
      store.dispatch('authentication/refreshToken', 'fetch')
        .then(() => {
          // Add the auth header
          request.setRequestHeader('Authorization', `Bearer ${store.state.authentication.authToken}`);  // <=== This is giving error as the request is no longer in OPENED state
        })
        .catch(() => {
          store.commit('authentication/logout');
          return router.push({ name: 'login' });
        });
    } else {
      // Add the auth header
      request.setRequestHeader('Authorization', `Bearer ${store.state.authentication.authToken}`);
    }
  }
}

Question 3:
This is pretty much answered in Question 1. We get the auth token from our backend API and add it as a header on the requests. If the current auth token has expired, we need to make an async call to get a new one BEFORE the request to the server can be completed which is why we need an asynchronous way to perform an operation before the grid requests data.

Question 4:
I'm not a Typescript person but here's what I think your example might look like (faking the get token call).

import Vue from "vue"; 
import axios from 'axios'; 
import VueAxios from 'vue-axios'; 
 
import { GridPlugin, Page, Toolbar, Edit } from "@syncfusion/ej2-vue-grids"; 
import { DataManager, RemoteSaveAdaptor, UrlAdaptor } from "@syncfusion/ej2-vue-grids/node_modules/@syncfusion/ej2-data"; 
import Component from "vue-class-component"; 
 
 
Vue.use(GridPlugin); 

const getToken = function() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('bearer');
    }, 2000);
  });
}

class custom extends UrlAdaptor { 
    beforeSend(dm: DataManager, request: XMLHttpRequest): void {
        const token = getToken()
          .then(token => {
            request.setRequestHeader("Authorization", token); 
          })
    } 
 
export default Vue.extend({ 
    data: () => { 
 
        return { 
            toolbar: ['Add', 'Edit','Delete','Update','Cancel'], 
            editSettings: { allowAdding: true, allowEditing:true, allowDeleting:true }, 
            pageSettings: { pageCount: 3 }, 
        }; 
    }, 
    methods: { 
       
        created() { 
             
        }, 
        load() { 
            var localData = new DataManager({ 
                url: "Home/UrlDatasource", 
                insertUrl: "Home/Insert", 
                updateUrl: "Home/Update", 
                removeUrl: "Home/Delete", 
                adaptor: new custom 
            }); 
            var grid = (document.getElementsByClassName('e-grid')[0] as any).ej2_instances[0]; 
            grid.dataSource = localData; 
        } 
    }, 
    provide: { 
        grid: [Page, Toolbar,Edit] 
    } 
}); 

Now from your description that beforeSend is synchronous, I understand that this likely can't be done this way. Is there some other way to asynchronously determine the headers for each request?

Thanks,
Jason


RR Rajapandi Ravi Syncfusion Team January 8, 2021 12:20 PM UTC

Hi Jason, 

Thanks for the update 

From validating your query we could see that if your current auth token has expired, you need to make an async call to get a new one before the request was sent to the server. We need some information for our clarification, Please share the below details that would be helpful for us to provide a better solution. 
 
1) Please confirm you have any custom API to validate the header token. If you are validating the token with a custom API, Please refer the below sample to achieve your requirement. 
 
In this sample, we have override our makeRequest method and make a own AJAX call. When our own AJAX call gets success, we have send the Datamanager request to the server. So you can also send the Datamanager request in your custom API call success method. Please refer the below code example and sample for more information. 

 
export default Vue.extend({ 
    data: () => { 
 
        return { 
            toolbar: ['Add', 'Edit','Delete','Update','Cancel'], 
            editSettings: { allowAdding: true, allowEditing:true, allowDeleting:true }, 
            pageSettings: { pageCount: 3 }, 
        }; 
    }, 
    methods: { 
        complete(args) { 
            debugger; 
        }, 
        created() { 
            debugger; 
        }, 
        load() { 
            (DataManager.prototype as any).makeRequest = function (url: any, deffered: Deferred, args?: any, query?: any): Object { 
                .  .  .  .  .  .  .  .  . 
                .  .  .  .  .  .  .  .  .  
                let req: Object = (this as any).extendRequest(url, fnSuccess, fnFail); 
                let ajax: Ajax = new Ajax(req); 
                let getInstance: any = this; 
                ajax.beforeSend = () => { 
                    (this as any).beforeSend(ajax.httpRequest, ajax); 
                }; 
                let customajax: Ajax = new Ajax(); 
                customajax.type = 'POST'; 
                (customajax as any).contentType = 'application/json'; 
                customajax.url = '/Home/Data'; 
                customajax.data = JSON.stringify({ gid: [{ OrderID: 1009, CustomerID: "Raja", ShipCity: "India" }] }); 
                customajax.send().then(function (value: any) { 
                    req = ajax.send(); 
                    (<Promise<Ajax>>req).catch((e: Error) => true); // to handle failure remote requests.         
                    (getInstance as any).requests.push(ajax); 
 
                });  
                .  .  .  .  .  .  .  . 
                .  .  .  .  .  .  .  . 
           } 
            var localData = new DataManager({ 
                url: "Home/UrlDatasource", 
                insertUrl: "Home/Insert", 
                updateUrl: "Home/Update", 
                removeUrl: "Home/Delete", 
                adaptor: new custom 
            }); 
            var grid = (document.getElementsByClassName('e-grid')[0] as any).ej2_instances[0]; 
            grid.dataSource = localData; 
        } 
    }, 
    provide: { 
        grid: [Page, Toolbar,Edit] 
    } 
}); 
 


2)     If you are validating the header token with Datamanager, Please confirm if once the API gets failed you like to send the Postback again or not. 

Regards, 
Rajapandi R


JA Jason January 12, 2021 08:12 PM UTC

I'm going to be honest here, I have no idea what that is doing. I sort of get the concept of overriding the makeRequest method but I've never had any experience with Vue.extend so I have no idea what that's doing or even where to do that in my code. Is there any way to do this in a regular single file component (preferably without Typescript)?


RR Rajapandi Ravi Syncfusion Team January 15, 2021 10:04 AM UTC

Hi Jason,  
  
We have created a new incident under your Direct trac account to follow up with this query. We suggest you to follow up with the incident for further updates. Please log in using the below link.   
  
  
Regards,  
Rajapandi R 



PA Paul July 22, 2021 05:00 PM UTC

This thread is old but I thought I would add our solution if anyone is interested. We had the same problem and using a custom DataManager in conjunction with a custom DataAdapter worked for us. You can asynchronously update the auth token in executeQuery() and use the result in beforeSend().


class MyDataManager extends DataManager {
    constructor(datasourcequeryadaptor) {
        super(datasourcequeryadaptor);
    }

    async executeQuery(querydonefailalways) {
        await this.adaptor.updateAuthToken();
        return super.executeQuery(querydonefailalways);
    }
}

class MyODataAdaptor extends ODataV4Adaptor {
    _api;
    _authToken;

    constructor(api) {
        super();
        this._api = api;
    }

    async updateAuthToken() {
        this._authToken = await this._api.getAuthToken();
    }

    beforeSend(dmrequest) {
        if (this._authToken && this._authToken.token) {
            request.setRequestHeader('Authorization''bearer ' + this._authToken.token);
        }
    }
}



MK Michael Kavouklis July 9, 2024 09:28 AM UTC

I have the same problem and Paul's reply seem to work! Thank you so much for posting it.

Paul, did you have any problem so far with this approach?

Is there any alternative to this that is more 'officially' supported? Is extending data manager considered a good practice? 

Thank you!

P.S. I am using Angular.



AR Aishwarya Rameshbabu Syncfusion Team July 15, 2024 12:43 PM UTC

Hi Michael Kavouklis,


The DataManager functions solely as an synchronous process, therefore, an asynchronous call cannot be utilized in the beforeSend method. However, in your specific scenario, an asynchronous process needs to be incorporated within the operations of DataManager. This customization can be implemented at the sample level to align with user requirements. A demonstration of token authentication has been developed by overriding the makeRequest method. In this approach, an asynchronous call is made to retrieve the token before sending the default request to the server. The token is then stored in the window variable and utilized in the DataManager request. The token can be accessed through the window variable in the beforeSend method. Please review the provided sample and code example for further clarification.


App.component.ts

 

export class CustomUrlAdaptor extends UrlAdaptor {

  public beforeSend(dm: any, request: any) {

    if ((window as any).token) {

      request.headers.set('Authorization', 'Bearer ' + (window as any).token);

      // additional headers sent here

    }

    super.beforeSend(dm, request);

  }

}

async function fetchToken() {

  const response = await fetch("https://services.odata.org/V4/Northwind/Northwind.svc/Orders");

  const movies = await response.json();

  return "New Token";

}

load() { //load event of Grid

  (DataManager.prototype as any).makeRequest = function(url: any, deffered: any, args: any, query: any) {

    const _this = this;

        let isSelector = !!query.subQuerySelector;

        let fnFail = function (e:any, req: any) {

            args.error = e;

            deffered.reject(args);

        };

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        let req = this.extendRequest(url, fnSuccess, fnFail);

        if (!this.isCustomDataAdaptor(this.adaptor)) {

            let fetch_1 = new Fetch(req);

            fetch_1.beforeSend = function () {

                _this.beforeSend(fetch_1.fetchRequest, fetch_1);

            };

            fetchToken().then(mov => { //once the token is returned from the async method we have stored the token to the window variable

              token = mov;

              (window as any).token = token;

              req = fetch_1.send(); //after we get and set the token to the window variable we have sent the default datamanager request to the server

              req.catch(function (e: any) { return true; }); // to handle failure remote requests.

            });

            this.requests.push(fetch_1);

        }

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        .  .  .  .  .  .  .  .  .  .

        return req;

  }

}

 


Sample: https://stackblitz.com/edit/angular-cptpzb-lyhyj3?file=src%2Fapp.component.html,src%2Fapp.component.ts,package.json


If you need any other assistance or have additional questions, please feel free to contact us.



Regards

Aishwarya R


Loader.
Up arrow icon