Allow HTTP request retry under certain conditions using Got - javascript

I’m using the Got library for HTTP requests within my application. I’m making HTTP requests to an API where I can expect an HTTP code 404 under certain conditions. I’d like to use Got's internal retry functionality for rerunning the request until the 404 error is gone (which will happen; I just don't know if it takes 1 minute or 30 minutes).
From the documentation I know that HTTP code 404 is not a supported statusCode for the built-in retry functionality and therefore I cannot perform any action within the beforeRetry hook of Got; see here.
I’m extending a Got instance to allow some presets for the API I’m calling. For now I have not found a clean way to extend the existing retry statusCodes with 404.
const gitlabOnPrem = got.extend({
prefixUrl: ".." + "..",
mutableDefaults: true,
responseType: 'json',
//retry: { statusCode: got.defaults.options.retry.statusCode.push(404) }, //does not work | not clean
https: { rejectUnauthorized: false },
headers: {
'PRIVATE-TOKEN': "..",
Accept: 'application/json',
},
hooks: {
beforeError: [
(error) => {
const { response } = error;
console.log(response);
/*
Another idea: if I cannot extend retry statusCodes then I´d like to somehow force a retry from here
if (response.statusCode === 404 && response.path.endsWith('/export/download')) {
console.log('FORCE A RETRY AS THE DOWNLOAD MIGHT NOT BE READY YET');
}
*/
if (response && response.body) {
error.name = 'GitLabOnPremError';
error.message = `${response.body.message !== undefined
? response.body.message
: response.body.error
} (${response.statusCode})`;
}
return error;
},
],
},
});
How can I extend the HTTP statusCodes that allow running a retry?
If this is not possible, see my comment in the code. Is it somehow possible to force a retry manually by just using Got?

Sometimes it's best to just write it yourself. Trying to get a library to work in a way that it isn't totally made to do can be more pain than it's worth. It's also usually very brittle in the long run.
Why not just wrap it yourself? Something like this:
async function MyGotFn() {
let retries = 100;
let statusCode = 404;
let response;
while (statusCode === 404 && --retries > 0) {
response = await got('...');
statusCode = response.statusCode;
}
if (response.statusCode === 404) throw new Error('max retries reached');
return response;
}

Related

Is it necessary to check if axios response status is 200 or not?

Is it necessary to check if axios.get() response status is 200 ?
useEffect(() => {
const fetch = async () => {
try {
const response = await axios.get('/api');
if (response.status === 200) {
setState(response.data);
}
} catch (error) {
console.error(error);
}
};
fetch();
}, []);
or I can do this
useEffect(() => {
const fetch = async () => {
try {
const { data } = await axios.get('/api');
setState(data);
} catch (error) {
console.error(error);
}
};
fetch();
}, []);
if it is, what is best practice?
Normally you don't need to check. But technically, it totally depends on how your team interpret HTTP protocol.
Even though it’s discouraged, I’ve seen teams that totally disregard the standard semantics of HTTP status code, blindly set status code to 200 in almost all cases, then encode the success/failure state within data payload, e.g. data.success == true or false. In that case checking status code means nothing.
Axios is just a library that facilitates message exchanging over HTTP. The real working horse is HTTP protocol. You can even customize axios to determine what case is deemed "error", like shown in this question. Better consult your backend colleagues to understand their server response convention and reach consensus within your team.

Axios interceptor is not returning to login screen when token is expired

I'm trying to write a response interceptor for my React project but I am having some issues.
When a user gets a 401 from their original request I want to try and refresh the token and continue, but if the user gets a 401 from their original request and when trying to refresh the token it fails then redirect them to the login page.
What I have does the first bit just fine, it refreshes the token and continues with the original request, but the issue i am having is that if the refresh fails, its not redirecting the user to the login page.
I would love some input on what I am doing wrong
import axios from 'axios';
import { useRouter } from 'next/router'
const router = useRouter();
const apiInstance = axios.create({
baseURL: process.env.API_URL
});
apiInstance.interceptors.response.use((response) => {
return response;
}, async function (error) {
const originalRequest = error.config;
if (error.response.status === 401 && originalRequest.url === '/oauth/token') {
router.push('/');
return Promise.reject(error);
}
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
return axios.post(process.env.OAUTH_BASE_URL + '/oauth/token', {
'grant_type': 'refresh_token',
'refresh_token': localStorage.getItem('refresh_token'),
'client_id': process.env.CLIENT_ID,
})
.then(res => {
if (res.status === 200) {
localStorage.setItem('access_token', res.access_token);
localStorage.setItem('refresh_token', res.refresh_token);
localStorage.setItem('expires_in', res.expires_in);
axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('access_token');
return apiInstance(originalRequest);
}
})
}
return Promise.reject(error);
});
export default apiInstance;
There's a couple of errors here. First, url property is equal to the whole value of url param of axios call, so this...
originalRequest.url === '/oauth/token'
... is only true if process.env.OAUTH_BASE_URL is an empty string (and most likely it's not). In general, it's better to avoid checking against URLs and use flags/custom properties set on request objects (as with _retry flag).
Also, note that while apiInstance is used for regular API call, the particular call for refresh token actually avoids it:
return axios.post(process.env.OAUTH_BASE_URL + '/oauth/token', { //
^^^^^^^^^^
... which means interceptors for this call are not even fired.
Here's one possible approach to solve this. apiInstance here is the exported axios instance, and setTokens/getAccessToken/getRefreshToken are simple abstractions over mechanisms of storing/retrieving particular tokens.
apiInstance.interceptors.request.use(request => {
if (!request._refreshToken) {
request.headers.Authorization = 'Bearer ' + getAccessToken();
}
// console.log('REQUEST', request.method + ' ' + request.url);
return request;
});
apiInstance.interceptors.response.use(
void 0, // better skip this argument altogether
error => {
const originalRequest = error.config;
if (originalRequest._refreshToken) {
console.log('REFRESH TOKEN FAILED');
// ... and all the things you need to do when refreshing token failed,
// like resettting access token, and rerouting users to /login page,
// or just sending an event for Router to process
return Promise.reject(error);
}
const errorResponse = error.response;
if (errorResponse.status !== 401) {
return Promise.reject(error);
}
return apiInstance.post('/oauth/token', {
grant_type: 'refresh_token',
refresh_token: getRefreshToken(),
client_id: process.env.CLIENT_ID,
}, {
_refreshToken: true // custom parameter
}).then((resp) => {
setTokens(resp.data);
return apiInstance(originalRequest);
});
}
);
There are two ideas behind this (easily testable with unit tests): first, failed refresh token requests always stop the interceptor chain (as they throw immediately), second, if 'business-level' API request fails, it's always preceded with refresh-token one.
Note that this code is just a prototype to illustrate the concept here. If you expect your code to be able to issue multiple API calls at once, token refresh should actually be wrapped into a function returning single promise (to avoid subsequent refresh-token calls). If you're going to use this in production, I strongly suggest at least considering using axios-auth-refresh instead of writing your own implementation for that.

Axios returns ECONNREFUSED very often while CURL works fine

I've read many topics here, but nothing same was helpful. My problem is that I often get ECONNREFUSED error while using axios.all .. get on nodejs (in 50% of get requests). In the same time, curl works great.
This in my js code:
const axios = require('axios');
async function makeGetRequest () {
let config = {
headers: {"User-Agent": 'curl/7.64.1'}
};
try {
const [BYN, RUR] = await axios.all([
axios.get('https://www.nbrb.by/api/exrates/rates/145', config),
axios.get('https://www.nbrb.by/api/exrates/rates/298', config),
]);
return [BYN.data, RUR.data];
} catch(error) {
return error
}
}
makeGetRequest().then((value) => {
console.log("VAL: ", value)
})
As you can see, I tried to manipulate headers in order to imitate curl's but this doesn't work.
The command:
curl https://www.nbrb.by/api/exrates/rates/145
works fine. But I need SSR response for my gatsby site.
The error:
ECONNREFUSED
is the underlying HTTP protocol connection error which means the the HTTP server didn't accept your connection.
Probably it's not a problem with your code at all, you just need to ensure rates and agents allowed by the web server your trying to reach to call that API.
As selfagency commented, you may be reaching the max rate of requests per seconds, and IMO it's likely to be your case because you are dispatching 2 concurrents requests.
So, try the following:
const axios = require("axios");
async function makeGetRequest() {
let config = {
headers: { "User-Agent": "curl/7.64.1" }
};
try {
const BYN = await axios.get(
"https://www.nbrb.by/api/exrates/rates/145",
config
);
const RUR = await axios.get("https://www.nbrb.by/api/exrates/rates/298", config);
return [BYN.data, RUR.data];
} catch (error) {
return error;
}
}
makeGetRequest().then(value => {
console.log("VAL: ", value);
});
This way you will be doing only 1 request at a time, so maybe you bypass the rate limit. BTW this is very common for free mode APIs that want you to pay to leverage your usage.

Get Response headers in Meteor.js

Situation
From my Meteor.js website I'm calling my own REST service. Here's a code sample from my server side
function (question) {
var r = Async.runSync(function (done) {
HTTP.get(URL, {
params: {q: question}, headers: {
"Accept": "application/json",
}
}, function (err, result) {
done(err, result);
});
});
if (r.err) {
console.log("Failed to smartSearch ... ", r.err);
return null;
} else if (r.result.content) {
console.log("Success ... ");
return JSON.parse(r.result.content);
}
}
This works great but there is also some crucial information in the response headers which I'm unable to find.
What I've tried so far
I viewed everything within r.result.content, but this only contains my request headers.
I've installed https://atmospherejs.com/gadicohen/headers and tried everything the site said.
But still not seeing my response headers.
Additional Information
I'm fairly new to Meteor.js so I don't really have an idea what I might be doing wrong but getting response headers doesn't see like a strange thing to me.
There is no need to wrap the request as an async call, as it already is.
You can use a try..catch block to handle both successful and failed requests.
try {
var result = HTTP.get(...);
var responseHeaders = result.headers;
} catch (e) {
// handle error
}
If the response headers indicate JSON response, it will be parsed and available as result.data. The response will be available as a string in result.content.
More details are available in the HTTP package API docs.

how to handle uncaught exception in http.get response

I have this code to pull down a news feed from a third party website using an API. Its setup to run every 5 seconds, and pull any news transactions that may occur. The problem seems to be when there is no new transactions that occur.
By adding the process.on('uncaught exception', function(error){ console.log("hmph") }) the cron job is able to continue 5 seconds later, so I was tempted to leave it as is; however, I added console.log("hmph") to the and now I'm confused.
The first time, the console will write hmph. 5 seconds later it will write
hmph
hmph
and so on. I know I must be missing something, but I'm not quite sure what it is. I've tried in the else statement to do request.end() but the error still fires.
Without the process.on('uncaught...') the error thrown is:
events.js:71
throw arguments[1]; // Unhandled 'error' event
^
Error: Parse Error
at Socket.socketOnData (http.js:1367:20)
at TCP.onread (net.js:403:27)
with the proccess.on('uncaught...') the console.log(error) is:
{ [Error: Parse Error] bytesParsed: 161, code: 'HPE_INVALID_CONSTANT' }
How do I properly handle this error?
Abbreviated code:
var job = new cronJob('*/5 * * * * *', function(){
var request = http.get({
host: 'www.example.com',
path: '/news_feed?apicode=myapicode',
port: 80,
headers: { 'accept-encoding': 'gzip' }
})
request.on('response', function(response){
if (response.statusCode == 200){
// gunzip response, save response to mongodb
}
else
{
// here is where the error is occuring
process.on('uncaughtException',function(error){
console.log(error);
console.log("hmph");
}
});
}, null, true);
Every time you make a request, you are binding a new uncaughtException handler, so when the first request is sent, you bind the first one, and when it fails it prints an error, and then at the next request, you add another handler, and when that fails both the first and second handlers will run.
Examining the error, and a conversation about this type of error here: https://github.com/joyent/node/issues/3354 It seems like the server you are connecting to is probably doing something weird. The easiest solution for you probably is to use the uncaughtException handler for now. That said, it is less than ideal and you should not do it as a general solution to future problems like this.
var job = new cronJob('*/5 * * * * *', function(){
var request = http.get({
host: 'www.example.com',
path: '/news_feed?apicode=myapicode',
port: 80,
headers: { 'accept-encoding': 'gzip' }
});
request.on('response', function(response){
if (response.statusCode == 200){
// gunzip response, save response to mongodb
}
});
}, null, true);
process.on('uncaughtException',function(error){
console.log(error);
console.log("hmph");
});

Categories