renovate/lib/platform/github/gh-got-wrapper.js

219 lines
5.9 KiB
JavaScript
Raw Normal View History

const URL = require('url');
const ghGot = require('gh-got');
2018-04-09 04:07:05 +00:00
const delay = require('delay');
const parseLinkHeader = require('parse-link-header');
2018-07-06 05:26:36 +00:00
const endpoints = require('../../util/endpoints');
let cache = {};
let stats = {};
2018-07-06 05:26:36 +00:00
async function get(path, options, retries = 5) {
const { host } = URL.parse(path);
const opts = {
...endpoints.find({ platform: 'github', host }),
...options,
};
const method = opts.method || 'get';
const useCache = opts.useCache || true;
if (method === 'get' && useCache && cache[path]) {
logger.trace({ path }, 'Returning cached result');
return cache[path];
}
2018-09-05 12:18:31 +00:00
logger.trace(`${method.toUpperCase()} ${path}`);
2018-09-05 09:03:47 +00:00
stats.requests = (stats.requests || []).concat([
method.toUpperCase() + ' ' + path.replace(opts.endpoint, ''),
]);
try {
2018-09-07 04:28:07 +00:00
if (global.appMode) {
const appAccept = 'application/vnd.github.machine-man-preview+json';
opts.headers = Object.assign(
{},
{
accept: appAccept,
2018-08-28 15:07:00 +00:00
'user-agent':
process.env.RENOVATE_USER_AGENT ||
'https://github.com/renovatebot/renovate',
},
opts.headers
);
if (opts.headers.accept !== appAccept) {
opts.headers.accept = `${appAccept}, ${opts.headers.accept}`;
}
}
const res = await ghGot(path, opts);
if (res && res.headers) {
stats.rateLimit = res.headers['x-ratelimit-limit'];
stats.rateLimitRemaining = res.headers['x-ratelimit-remaining'];
}
2018-04-09 04:07:05 +00:00
if (opts.paginate) {
// Check if result is paginated
2018-07-03 09:53:09 +00:00
const pageLimit = opts.pageLimit || 10;
const linkHeader = parseLinkHeader(res.headers.link);
if (linkHeader && linkHeader.next && linkHeader.last) {
let lastPage = +linkHeader.last.page;
if (!process.env.RENOVATE_PAGINATE_ALL) {
2018-07-03 09:53:09 +00:00
lastPage = Math.min(pageLimit, lastPage);
}
const pageNumbers = Array.from(
new Array(lastPage),
(x, i) => i + 1
).slice(1);
const pages = await Promise.all(
pageNumbers.map(page => {
const url = URL.parse(linkHeader.next.url, true);
delete url.search;
url.query.page = page;
return get(URL.format(url), { ...opts, paginate: false }, retries);
})
);
res.body = res.body.concat(
...pages.filter(Boolean).map(page => page.body)
);
}
}
if (
method === 'get' &&
(path.startsWith('repos/') ||
path.startsWith('https://api.github.com/repos/'))
) {
cache[path] = res;
}
// istanbul ignore if
if (method === 'POST' && path === 'graphql') {
if (res.errors && retries > 0) {
logger.info('Retrying graphql query');
return get(path, opts, 0);
}
if (res.data && retries === 0) {
logger.info('Recovered graphql query');
}
}
return res;
} catch (err) {
if (
err.name === 'RequestError' &&
(err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')
) {
throw new Error('platform-failure');
}
if (err.statusCode >= 500 && err.statusCode < 600) {
if (retries > 0) {
logger.info(
{ statusCode: err.statusCode, message: err.message },
`Retrying request`
);
2018-04-09 04:07:05 +00:00
await delay(5000 / retries);
2018-04-09 04:07:05 +00:00
return get(path, opts, retries - 1);
}
throw new Error('platform-failure');
2018-07-09 09:28:33 +00:00
}
if (
2017-08-27 13:10:19 +00:00
retries > 0 &&
err.statusCode === 403 &&
err.message &&
2018-06-04 18:44:32 +00:00
err.message.startsWith('You have triggered an abuse detection mechanism')
2017-08-27 13:10:19 +00:00
) {
logger.info(
{
headers: err.headers,
path,
statusCode: err.statusCode,
message: err.message,
},
`Retrying request`
);
2018-04-09 04:07:05 +00:00
await delay(180000 / (retries * retries));
return get(path, opts, retries - 1);
2018-07-09 09:28:33 +00:00
}
if (
err.statusCode === 403 &&
err.message &&
err.message.includes('rate limit exceeded')
) {
logger.info({ err, headers: err.headers }, 'Rate limit exceeded');
throw new Error('rate-limit-exceeded');
} else if (
err.statusCode === 403 &&
err.message &&
err.message.includes('blobs up to 1 MB in size')
) {
throw err;
} else if (err.statusCode === 403) {
if (retries > 0) {
logger.info(
{ statusCode: err.statusCode, message: err.message },
`Retrying request`
);
2018-04-09 04:07:05 +00:00
await delay(60000 / (retries * retries));
return get(path, opts, retries - 1);
}
} else if (
err.statusCode === 401 &&
err.message &&
err.message.includes('Bad credentials')
) {
const rateLimit = err.headers ? err.headers['x-ratelimit-limit'] : -1;
logger.info(
{
token: maskToken(opts.token),
err,
},
'Bad credentials'
);
if (rateLimit === '60') {
throw new Error('platform-failure');
}
throw new Error('bad-credentials');
}
throw err;
}
}
// istanbul ignore next
function maskToken(token) {
if (!token) {
return token;
}
return `${token.substring(0, 2)}${new Array(token.length - 3).join(
'*'
)}${token.slice(-2)}`;
}
const helpers = ['get', 'post', 'put', 'patch', 'head', 'delete'];
for (const x of helpers) {
get[x] = (url, opts) =>
get(url, Object.assign({}, opts, { method: x.toUpperCase() }));
}
2018-09-07 04:28:07 +00:00
get.setAppMode = function setAppMode() {
// no-op
};
get.reset = function reset() {
cache = null;
cache = {};
// istanbul ignore if
if (stats.requests && stats.requests.length > 1) {
2018-09-05 09:03:47 +00:00
logger.info(
{
rateLimit: parseInt(stats.rateLimit, 10),
2018-09-05 09:03:47 +00:00
requestCount: stats.requests.length,
rateLimitRemaining: parseInt(stats.rateLimitRemaining, 10),
2018-09-05 09:03:47 +00:00
},
'Request stats'
);
stats.requests.sort();
logger.debug({ requests: stats.requests }, 'All requests');
stats = {};
}
};
module.exports = get;