Skip to content

Commit

Permalink
Merge pull request #58 from anuraghazra/fix-ratelimit
Browse files Browse the repository at this point in the history
fix: increase github rate limit with multiple PATs

- special thanks to @filiptronicek @ApurvShah007 @garvit-joshi for helping out :D
  • Loading branch information
anuraghazra authored Jul 15, 2020
2 parents 2efb399 + 429d65a commit 9328a57
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 30 deletions.
2 changes: 2 additions & 0 deletions api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ module.exports = async (req, res) => {
} = req.query;
let stats;

res.setHeader("Cache-Control", "public, max-age=1800");
res.setHeader("Content-Type", "image/svg+xml");

try {
stats = await fetchStats(username);
} catch (err) {
Expand Down
2 changes: 2 additions & 0 deletions api/pin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ module.exports = async (req, res) => {
} = req.query;

let repoData;

res.setHeader("Cache-Control", "public, max-age=1800");
res.setHeader("Content-Type", "image/svg+xml");

try {
Expand Down
29 changes: 18 additions & 11 deletions src/fetchRepo.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
const { request } = require("./utils");
const retryer = require("./retryer");

async function fetchRepo(username, reponame) {
if (!username || !reponame) {
throw new Error("Invalid username or reponame");
}

const res = await request({
query: `
const fetcher = (variables, token) => {
return request(
{
query: `
fragment RepoInfo on Repository {
name
stargazers {
Expand All @@ -33,11 +31,20 @@ async function fetchRepo(username, reponame) {
}
}
`,
variables: {
login: username,
repo: reponame,
variables,
},
});
{
Authorization: `bearer ${token}`,
}
);
};

async function fetchRepo(username, reponame) {
if (!username || !reponame) {
throw new Error("Invalid username or reponame");
}

let res = await retryer(fetcher, { login: username, repo: reponame });

const data = res.data.data;

Expand Down
40 changes: 25 additions & 15 deletions src/fetchStats.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
const { request } = require("./utils");
const retryer = require("./retryer");
const calculateRank = require("./calculateRank");
require("dotenv").config();

async function fetchStats(username) {
if (!username) throw Error("Invalid username");

const res = await request({
query: `
const fetcher = (variables, token) => {
return request(
{
query: `
query userInfo($login: String!) {
user(login: $login) {
name
login
repositoriesContributedTo(first: 100, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
contributionsCollection {
totalCommitContributions
}
pullRequests(first: 100) {
repositoriesContributedTo(first: 1, contributionTypes: [COMMIT, ISSUE, PULL_REQUEST, REPOSITORY]) {
totalCount
}
pullRequests(first: 1) {
totalCount
}
issues(first: 100) {
issues(first: 1) {
totalCount
}
followers {
Expand All @@ -36,9 +36,17 @@ async function fetchStats(username) {
}
}
}
`,
variables: { login: username },
});
`,
variables,
},
{
Authorization: `bearer ${token}`,
}
);
};

async function fetchStats(username) {
if (!username) throw Error("Invalid username");

const stats = {
name: "",
Expand All @@ -47,12 +55,14 @@ async function fetchStats(username) {
totalIssues: 0,
totalStars: 0,
contributedTo: 0,
rank: "C",
rank: { level: "C", score: 0 },
};

let res = await retryer(fetcher, { login: username });

if (res.data.errors) {
console.log(res.data.errors);
throw Error("Could not fetch user");
throw Error(res.data.errors[0].message || "Could not fetch user");
}

const user = res.data.data.user;
Expand Down
43 changes: 43 additions & 0 deletions src/retryer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
const retryer = async (fetcher, variables, retries = 0) => {
if (retries > 7) {
throw new Error("Maximum retries exceeded");
}
try {
console.log(`Trying PAT_${retries + 1}`);

// try to fetch with the first token since RETRIES is 0 index i'm adding +1
let response = await fetcher(
variables,
process.env[`PAT_${retries + 1}`],
retries
);

// prettier-ignore
const isRateExceeded = response.data.errors && response.data.errors[0].type === "RATE_LIMITED";

// if rate limit is hit increase the RETRIES and recursively call the retryer
// with username, and current RETRIES
if (isRateExceeded) {
console.log(`PAT_${retries + 1} Failed`);
retries++;
// directly return from the function
return retryer(fetcher, variables, retries);
}

// finally return the response
return response;
} catch (err) {
// prettier-ignore
// also checking for bad credentials if any tokens gets invalidated
const isBadCredential = err.response.data && err.response.data.message === "Bad credentials";

if (isBadCredential) {
console.log(`PAT_${retries + 1} Failed`);
retries++;
// directly return from the function
return retryer(fetcher, variables, retries);
}
}
};

module.exports = retryer;
12 changes: 9 additions & 3 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,13 @@ function isValidHexColor(hexColor) {
).test(hexColor);
}

function request(data) {
function request(data, headers) {
return new Promise((resolve, reject) => {
axios({
url: "https://api.github.com/graphql",
method: "post",
headers: {
Authorization: `bearer ${process.env.GITHUB_TOKEN}`,
...headers,
},
data,
})
Expand All @@ -48,4 +48,10 @@ function request(data) {
});
}

module.exports = { renderError, kFormatter, encodeHTML, isValidHexColor, request };
module.exports = {
renderError,
kFormatter,
encodeHTML,
isValidHexColor,
request,
};
2 changes: 1 addition & 1 deletion tests/fetchStats.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ describe("Test fetchStats", () => {
mock.onPost("https://api.github.com/graphql").reply(200, error);

await expect(fetchStats("anuraghazra")).rejects.toThrow(
"Could not fetch user"
"Could not resolve to a User with the login of 'noname'."
);
});
});
50 changes: 50 additions & 0 deletions tests/retryer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
require("@testing-library/jest-dom");
const retryer = require("../src/retryer");

const fetcher = jest.fn((variables, token) => {
console.log(variables, token);
return new Promise((res, rej) => res({ data: "ok" }));
});

const fetcherFail = jest.fn(() => {
return new Promise((res, rej) =>
res({ data: { errors: [{ type: "RATE_LIMITED" }] } })
);
});

const fetcherFailOnSecondTry = jest.fn((_vars, _token, retries) => {
return new Promise((res, rej) => {
// faking rate limit
if (retries < 1) {
return res({ data: { errors: [{ type: "RATE_LIMITED" }] } });
}
return res({ data: "ok" });
});
});

describe("Test Retryer", () => {
it("retryer should return value and have zero retries on first try", async () => {
let res = await retryer(fetcher, {});

expect(fetcher).toBeCalledTimes(1);
expect(res).toStrictEqual({ data: "ok" });
});

it("retryer should return value and have 2 retries", async () => {
let res = await retryer(fetcherFailOnSecondTry, {});

expect(fetcherFailOnSecondTry).toBeCalledTimes(2);
expect(res).toStrictEqual({ data: "ok" });
});

it("retryer should throw error if maximum retries reached", async () => {
let res;

try {
res = await retryer(fetcherFail, {});
} catch (err) {
expect(fetcherFail).toBeCalledTimes(8);
expect(err.message).toBe("Maximum retries exceeded");
}
});
});

0 comments on commit 9328a57

Please sign in to comment.