diff --git a/bin/good-first-issue.js b/bin/good-first-issue.js index 1e8c4c1..e4b9875 100755 --- a/bin/good-first-issue.js +++ b/bin/good-first-issue.js @@ -6,6 +6,7 @@ const opn = require('open') const gfi = require('libgfi') const packageJSON = require('../package.json') +const formatError = require('../lib/format-error') const log = require('../lib/log') const prompt = require('../lib/prompt') const projects = require('../data/projects.json') @@ -54,7 +55,7 @@ cli process.exitCode = 0 } } catch (err) { - console.error(err) + console.error(`\n${formatError(err, input)}\n`) process.exitCode = 1 } }) diff --git a/lib/format-error.js b/lib/format-error.js new file mode 100644 index 0000000..15d1253 --- /dev/null +++ b/lib/format-error.js @@ -0,0 +1,63 @@ +function formatError (err, input) { + const target = input ? `"${input}"` : 'the requested project' + const prefix = `Error: Unable to fetch issues for ${target}.` + + if (isRateLimitError(err)) { + return `${prefix}\nGitHub API rate limit reached. Retry later or pass --auth for higher limits.` + } + + if (isInvalidProjectError(err)) { + return `${prefix}\nPlease check the project name or repository path and try again.` + } + + if (isNetworkError(err)) { + return `${prefix}\nPlease check your network connection and try again.` + } + + return `${prefix}\n${getErrorDetail(err)}` +} + +function getErrorDetail (err) { + if (err && typeof err.message === 'string' && err.message.trim()) { + return err.message.trim() + } + + return 'An unexpected error occurred while querying the GitHub API.' +} + +function isInvalidProjectError (err) { + if (!err || err.status !== 422) { + return false + } + + const errors = err.response && err.response.data && err.response.data.errors + return Array.isArray(errors) && errors.some(error => error && error.code === 'invalid') +} + +function isRateLimitError (err) { + if (!err) { + return false + } + + const headers = getResponseHeaders(err) + const remaining = headers && headers['x-ratelimit-remaining'] + return err.status === 403 && remaining === '0' +} + +function isNetworkError (err) { + if (!err || err.status) { + return false + } + + return Boolean(err.code) || /network|timed? out|fetch failed|socket/i.test(getErrorDetail(err)) +} + +function getResponseHeaders (err) { + if (err.response && err.response.headers) { + return err.response.headers + } + + return err.headers || null +} + +module.exports = formatError diff --git a/tests/format-error.spec.js b/tests/format-error.spec.js new file mode 100644 index 0000000..00155a1 --- /dev/null +++ b/tests/format-error.spec.js @@ -0,0 +1,50 @@ +const formatError = require('../lib/format-error') + +test('formats invalid project errors clearly', () => { + const actual = formatError({ + status: 422, + response: { + data: { + errors: [{ code: 'invalid' }] + } + } + }, 'thisisntarealprojectorgithuborg') + + expect(actual).toBe( + 'Error: Unable to fetch issues for "thisisntarealprojectorgithuborg".\nPlease check the project name or repository path and try again.' + ) +}) + +test('formats rate limit errors clearly', () => { + const actual = formatError({ + status: 403, + response: { + headers: { + 'x-ratelimit-remaining': '0' + } + } + }, 'facebook/react') + + expect(actual).toBe( + 'Error: Unable to fetch issues for "facebook/react".\nGitHub API rate limit reached. Retry later or pass --auth for higher limits.' + ) +}) + +test('formats network errors clearly', () => { + const actual = formatError({ + code: 'ENOTFOUND', + message: 'getaddrinfo ENOTFOUND api.github.com' + }, 'nodejs/node') + + expect(actual).toBe( + 'Error: Unable to fetch issues for "nodejs/node".\nPlease check your network connection and try again.' + ) +}) + +test('falls back to the original error message for unknown failures', () => { + const actual = formatError(new Error('Something odd happened'), 'vercel/next.js') + + expect(actual).toBe( + 'Error: Unable to fetch issues for "vercel/next.js".\nSomething odd happened' + ) +})