HTTP Request, Interceptors and Token refreshment in Java-script

moustafa ahmed khalil
6 min readJun 11, 2020

The HTTP request in Java-script is a mandatory way to communicate between Client Side and Server Side.

in Java-script we can do HTTP request using redirection script and we can fire a request in the background without any refreshment

to do it without refreshment we need to fire XHR request or Ajax like most of the frameworks calling

there is two Java-script APIs to fire a request XMLHttpRequest and FETCH methods

XMLHttpRequest is the oldest way to fire Javascript HTTP request,
it is a class with many methods giving callbacks to handle the request, response, headers

FETCH is a function that allows us from firing HTTP requests as an asynchronous request depending on Java-script promises and return data in then and catch callbacks

to be honest, Fetch is much easier but XMLHttpRequest is more powerful and compatible with old browsers too, that’s why all Java-script frameworks and libraries build their own HTTP layers depending on XMLHttpRequest only like Jquery Ajax Methods, Angular HTTP Client providers, Axios, and others.

in this article, I will write a full Java-script es6 layer to have XMLHttpRequest

  1. I will create a Java-script Class called Query, this class constructor will init three main properties,
    the xhr to gold new instance from XMLHttpRequest.
    the method will be null at the initialization and will hold the request method type in the request firing.
    the data will be null at the initialization and will hold the request data that will be sent to the API.
class Query {
constructor() {
this.xhr = new XMLHttpRequest();
this.method = null;
this.data = null;
}
}

2. I will add now the main rest Methods post, put, get and delete

post = (data) => {}
put = (data) => {}
delete = () => {}
get = () => {}

3. now we need to define the full endpoint name that will be changed each time we execute a new HTTP request, so I will fix the base URL and make a changeable parameter refer to the needed endpoint,

in this example, I will fix the base URL but in the real projects, it must be written in a config or env file as a constant and create a new Method take the endpoint as a parameter and merge the base URL with it in a new property called uri

setUrl = (uri) => {
this.uri = `${baseURL}/${uri}`;
}

4. now the Query Class needs a new method to set custom HTTP Headers like Accept-language, Content-type, and the others …
so I will create a method to store the needed headers in a new property called headers,
and another Method to apply the passed Headers before firing the HTTP request

setHeaders = (headers) => {
this.headers = headers;
}
applyHeaders = () => {
if (!this.headers) {
this.headers = {
'Content-Type': 'application/json'
};
}
//TODO add the next header in case Authorization Token Header
in case needed
if (localStorage.getItem('token')){
this.headers['Authorization'] =
localStorage.getItem('token');
}
if (this.headers) {
Object.keys(this.headers).forEach(key => {
this.xhr.setRequestHeader(key, this.headers[key]);
});
}
}

5. now the Query Class is so near from being completed so I will create a new method to execute the request and call it inside the request Methods functions that we created before

this execute Method with run as an async Method using Java-script promises or async Await, but in this example, I am using promises to user reject callbacks

this execute Method will do the following :
1) add values to a method and data properties.
2) create a new Promise.
3) open the xhr request.
4) call applyHeaders method that we created before.
5) listen to onloadend and onerror events to return promise rejections in the case failed requests, resolve the response in case success response, and handle general errors like 401, 403, 404, 500, and the others …
6) send the request to the API

post = (data) => {return this.excute('POST', data);}
put = (data) => {return this.excute('PUT', data);}
delete = () => {return this.excute('DELETE');}
get = () => {return this.excute('GET');}
execute = (method, data) => {
this.method = method;
this.data = data;
return new Promise((resolve, reject) => {
this.xhr.open(method, this.uri, true);
this.applyHeaders();

this.xhr.onloadend = () => {
if (this.xhr.status >= 200 && this.xhr.status < 300) {
const response = JSON.parse(this.xhr.response);
return resolve(response);
} else {
if (this.xhr.status === 403) {
// TODO call rfresh token method
} else {
return reject(this.xhr);
}
}
}
this.xhr.onerror = () => {
return reject(this.xhr);
}
this.xhr.send(data ? JSON.stringify(data) : null);
});
}

6. now Query CLass will act as an HTTP provider and Interceptor at the same time, to reach this goal we need to add final two methods refreshToken Method and create a Que if failed requests to run after token refreshment

each time we create a new instance of Query CLass we actually create a new instance of XmlHttpRequest that’s mean in case we have a failed request we can rerun the last instance we had so I will add a new Method to create a Que of requests in case user’s token expired and the server returned 401 status code and refreshToken Method to request a new user Token from the server

the most important thing is to pass the failed instance to the refresh token Method to take the last expired token and return the new token and the failed instance to the Que Method to rerun it again once Token refreshed

the Que Method will update the HTTP Headers with the new Token and execute the instance of the failed request

refreshToken = (q) => {
return new Promise((resolve, reject) => {
const query = new Query();
query.setUrl(
`token/refresh/${localStorage.getItem('refresh')}`
);
query.put().then(res => {
if (res) {
localStorage.setItem('token', res.content.token);
localStorage.setItem(
'refresh',
res.content.refreshToken
);
return resolve({
q: q,
token: res.content.token
});
}
});
});
}
excuteQueue = () => {
return this.refreshToken(this).then(res => {
if (res) {
res.q.headers.Authorization = res.token;
return res.q.excute(res.q.method, res.q.data);
}
});
}

the expected behavior now that once the server return 401 status code the interceptor we have created will automatically refresh the user access Token and runt he last failed request without any actions from the user, simply will do a silent refresh

6. finally we will call the Que method in the execute in case success request with 403 or 401 status code

if (this.xhr.status === 403) {
return resolve(this.excuteQueue());
}

the final completed example

class Query {
constructor() {
this.xhr = new XMLHttpRequest();
this.method = null;
this.data = null;
}
setUrl = (uri) => {
this.uri = `${baseURL}/${uri}`;
}
setHeaders = (headers) => {
this.headers = headers;
}
applyHeaders = () => {
if (!this.headers) {
this.headers = {
'Content-Type': 'application/json'
};
}
//TODO add the next header in case Authorization Token Header
in case needed
if (localStorage.getItem('token')){
this.headers['Authorization'] =
localStorage.getItem('token');
}
if (this.headers) {
Object.keys(this.headers).forEach(key => {
this.xhr.setRequestHeader(key, this.headers[key]);
});
}
}
refreshToken = (q) => {
return new Promise((resolve, reject) => {
const query = new Query();
query.setUrl(
`token/refresh/${localStorage.getItem('refresh')}`
);
query.put().then(res => {
if (res) {
localStorage.setItem('token', res.content.token);
localStorage.setItem(
'refresh',
res.content.refreshToken
);
return resolve({
q: q,
token: res.content.token
});
}
});
});
}
excuteQueue = () => {
return this.refreshToken(this).then(res => {
if (res) {
res.q.headers.Authorization = res.token;
return res.q.excute(res.q.method, res.q.data);
}
});
}
post = (data) => {return this.excute('POST', data);}
put = (data) => {return this.excute('PUT', data);}
delete = () => {return this.excute('DELETE');}
get = () => {return this.excute('GET');}
execute = (method, data) => {
this.method = method;
this.data = data;
return new Promise((resolve, reject) => {
this.xhr.open(method, this.uri, true);
this.applyHeaders();

this.xhr.onloadend = () => {
if (this.xhr.status >= 200 && this.xhr.status < 300) {
const response = JSON.parse(this.xhr.response);
return resolve(response);
} else {
if (this.xhr.status === 403) {
return resolve(this.excuteQueue());
} else {
return reject(this.xhr);
}
}
}
this.xhr.onerror = () => {
return reject(this.xhr);
}
this.xhr.send(data ? JSON.stringify(data) : null);
});
}
}

to run a new request

const req = new Query();
req.setUrl('end point name');
req.setHeaders({headers object});
req.post(data)
.then(res => {
console.log(res);
})
.catch(error => {
console.log(error);
});

--

--