Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Retry + delay on HTTP 429 #36

Merged
merged 2 commits into from
Jul 18, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 44 additions & 16 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,36 @@ import { type Article, type RemoteArticleData, type ArticleStats } from './model
const debug = Debug('devto');
const apiUrl = 'https://dev.to/api';
const paginationLimit = 1000;
const maxRetries = 3;
const retryDelay = 1000; // 1 second delay before retrying

// There's a limit of 10 articles created each 30 seconds by the same user,
// so we need to throttle the API calls in that case.
// The insane type casting is due to typing issues with p-throttle.
const throttledPostForCreate = pThrottle({ limit: 10, interval: 30_500 })(got.post) as any as Got['post'];

// There's a limit of 30 requests each 30 seconds by the same user, so we need to throttle the API calls in that case too.
const throttledPutForUpdate = pThrottle({ limit: 30, interval: 30_500 })(
async (url: string, options: any) => got.put(url, options)
) as any as Got['put'];

async function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

async function retryRequest(fn: () => Promise<RemoteArticleData>, retries: number): Promise<RemoteArticleData> {
try {
return await fn();
} catch (error) {
if (retries === 0 || !(error instanceof RequestError && error.response?.statusCode === 429)) {
throw error;
}
debug('Rate limited, retrying in %s ms', retryDelay);
await delay(retryDelay);
return retryRequest(fn, retries - 1);
}
}

export async function getAllArticles(devtoKey: string): Promise<RemoteArticleData[]> {
try {
const articles = [];
Expand Down Expand Up @@ -69,22 +93,26 @@ export async function getLastArticlesStats(devtoKey: string, number: number): Pr
}

export async function updateRemoteArticle(article: Article, devtoKey: string): Promise<RemoteArticleData> {
try {
const markdown = matter.stringify(article, article.data, { lineWidth: -1 } as any);
const { id } = article.data;
// Throttle API calls in case of article creation
const get = id ? got.put : throttledPostForCreate;
const result = await get(`${apiUrl}/articles${id ? `/${id}` : ''}`, {
headers: { 'api-key': devtoKey },
json: { article: { title: article.data.title, body_markdown: markdown } },
responseType: 'json'
});
return result.body as RemoteArticleData;
} catch (error) {
if (error instanceof RequestError && error.response) {
debug('Debug infos: %O', error.response.body);
const update = async (): Promise<RemoteArticleData> => {
try {
const markdown = matter.stringify(article, article.data, { lineWidth: -1 } as any);
const { id } = article.data;
// Throttle API calls in case of article creation or update
const get = id ? throttledPutForUpdate : throttledPostForCreate;
const result = await get(`${apiUrl}/articles${id ? `/${id}` : ''}`, {
headers: { 'api-key': devtoKey },
json: { article: { title: article.data.title, body_markdown: markdown } },
responseType: 'json'
});
return result.body as RemoteArticleData;
} catch (error) {
if (error instanceof RequestError && error.response) {
debug('Debug infos: %O', error.response.body);
}

throw error;
}
};

throw error;
}
return retryRequest(update, maxRetries);
}
Loading