diff --git a/.changeset/mean-glasses-play.md b/.changeset/mean-glasses-play.md new file mode 100644 index 0000000000..e8a0337843 --- /dev/null +++ b/.changeset/mean-glasses-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add "rate limit exceeded" to error from GithubUrlReader if that is the cause of a read failure diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index e70444cf2c..c76364976d 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -221,6 +221,32 @@ describe('GithubUrlReader', () => { ).rejects.toThrow(NotModifiedError); }); + it('should throw Error with ratelimit exceeded if GitHub responds with 403 and rate limit is exceeded', async () => { + expect.assertions(1); + + worker.use( + rest.get( + 'https://ghe.github.com/api/v3/repos/backstage/mock/tree/contents/', + (_req, res, ctx) => { + return res( + ctx.status(403), + ctx.set('X-RateLimit-Remaining', '0'), + ctx.body( + '{"message": "API rate limit exceeded for xxx.xxx.xxx.xxx..."}', + ), + ); + }, + ), + ); + + await expect( + gheProcessor.readUrl( + 'https://github.com/backstage/mock/tree/blob/main', + { etag: 'foo' }, + ), + ).rejects.toThrow(/rate limit exceeded/); + }); + it('should return etag from the response', async () => { (mockCredentialsProvider.getCredentials as jest.Mock).mockResolvedValue({ headers: { diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 1eed5764ef..ca16e23fec 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -126,10 +126,21 @@ export class GithubUrlReader implements UrlReader { }; } - const message = `${url} could not be read as ${ghUrl}, ${response.status} ${response.statusText}`; + let message = `${url} could not be read as ${ghUrl}, ${response.status} ${response.statusText}`; if (response.status === 404) { throw new NotFoundError(message); } + + // GitHub returns a 403 response with a couple of headers indicating rate + // limit status. See more in the GitHub docs: + // https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting + if ( + response.status === 403 && + response.headers.get('X-RateLimit-Remaining') === '0' + ) { + message += ' (rate limit exceeded)'; + } + throw new Error(message); }