Description
I've developed an application which use axios to communicate with the PayPal API. PayPal has a NodeJS SDK, but unfortunately this doesn't support the Subscription API
, so I'm using axios
to handle the API
requests.
The code is working well, but I want improve the Access Token
logic. Essentially the PayPal
token expiry is 1 hour, and I don't want to get a new token each time I perform a request.
So I've used a mechanism that allows me to store the token in my own database and through the interceptors
of Axios
I retrieve that token. If the token is expired, I get a new one.
Code
First of all, I've declared the axios
instance, I don't want use the global instance 'cause I could reuse axios
for other API
payment gateways such as Stripe
:
const qs = require('qs');
const axios = require('axios');
const tokenUtils = require('./token-utils');
const baseURL = 'https://api.sandbox.paypal.com';
let instance = axios.create({
baseURL
})
then I defined the interceptors
which is something like a middleware
and is executed before any request:
instance.interceptors.response.use(
// Return response as no errors raised
function (response) {
return response
},
function (error) {
const errorResponse = error.response;
// Check if token is expired
if (isTokenExpiredError(errorResponse)) {
return resetTokenAndReattemptRequest(error)
}
// Error was not generated by the expired token
return Promise.reject(error);
}
)
then I have the isTokenExpiredError
which simply checks if the token is expired looking at the http
status code:
function isTokenExpiredError(errorResponse) {
return (errorResponse.status == 401) ? true : false;
}
Next, I have the resetTokenAndReattemptRequest
function which actually checks if the token is expired and eventually gets a new one:
let isAlreadyFetchingAccessToken = false;
async function resetTokenAndReattemptRequest(error) {
try {
const { response } = error;
// Get the stored token
const accessToken = await tokenUtils.getResetToken();
// Start the token generation task
// Add the failed request to the subscriber
const retryOriginalRequest = new Promise(resolve => {
addSubscriber(newToken => {
response.config.headers.Authorization = 'Bearer ' + newToken;
resolve(axios(response.config));
});
});
// There are no fetchint token request and the access token is expired (no length)
if (!isAlreadyFetchingAccessToken && accessToken === "") {
isAlreadyFetchingAccessToken = true;
const response = await axios({
method: 'POST',
url: `${baseURL}/v1/oauth2/token`,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Access-Control-Allow-Credentials': true
},
data: qs.stringify({
grant_type: 'client_credentials'
}),
auth: {
username: "your paypal client id",
password: "your paypal secret id"
}
});
// An error happened when asking for a new token
if (!response.data) {
return Promise.reject(error);
}
const newToken = response.data.access_token;
// Store generated token in db for a next usage
tokenUtils.saveToken(newToken);
isAlreadyFetchingAccessToken = false;
onAccessTokenFetched(newToken);
}
return retryOriginalRequest;
}
catch (err) {
return Promise.reject(err);
}
}
I commented the whole code, hoping to be much clear as possible, feel free to ask any question eventually.
Essentially, a failed request is added to the subscribers
which is handled by the following:
let subscribers = [];
function addSubscriber(callback) {
subscribers.push(callback);
}
each subscribed request
is execute after the token generation by:
function onAccessTokenFetched(accessToken) {
subscribers.forEach(callback => callback(accessToken));
subscribers = [];
}
So an API
request will look like this:
instance.get(
`${baseURL}/v1/billing/subscriptions/I-0SUB6KG4203B`)
.then(response => {
console.log(response.data);
return response.data;
}).catch((error) => {
console.log('error');
return null;
});
I also have a tokenUtils
which contains two function:
- getAccessToken: get the token stored from a database
- storeAccessToken: store a token to the db
I've also uploaded the full code here, you may take a look.